agents: add "Debug Local Agent Host Process In Dev Tools" command (#312246)

agents: add 'Debug Local Agent Host Process In Dev Tools' command

Adds a Developer command, available in both VS Code and the Agents app,
that opens a Chrome DevTools window attached to the local agent host
utility  analogous to the existing 'Debug Extension Host Inprocess
Dev Tools' command.

The agent host process enables its inspector live via node:inspector
on demand, so no restart is required. The renderer reaches the
inspector URL through the existing agent-host IPC channel.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Rob Lourens
2026-04-23 16:29:33 -07:00
committed by GitHub
parent 03beef50f2
commit 5daa98185c
8 changed files with 128 additions and 3 deletions

View File

@@ -1509,6 +1509,7 @@ export default tseslint.config(
'fs/promises', 'fs/promises',
'http', 'http',
'https', 'https',
'inspector',
'minimist', 'minimist',
'node:module', 'node:module',
'native-keymap', 'native-keymap',

View File

@@ -7,7 +7,7 @@ import { Event } from '../../../base/common/event.js';
import { IReference } from '../../../base/common/lifecycle.js'; import { IReference } from '../../../base/common/lifecycle.js';
import { constObservable, IObservable } from '../../../base/common/observable.js'; import { constObservable, IObservable } from '../../../base/common/observable.js';
import { URI } from '../../../base/common/uri.js'; import { URI } from '../../../base/common/uri.js';
import type { IAgentCreateSessionConfig, IAgentHostService, IAgentHostSocketInfo, IAgentResolveSessionConfigParams, IAgentSessionConfigCompletionsParams, IAgentSessionMetadata, AuthenticateParams, AuthenticateResult } from '../common/agentService.js'; import type { IAgentCreateSessionConfig, IAgentHostInspectInfo, IAgentHostService, IAgentHostSocketInfo, IAgentResolveSessionConfigParams, IAgentSessionConfigCompletionsParams, IAgentSessionMetadata, AuthenticateParams, AuthenticateResult } from '../common/agentService.js';
import type { IAgentSubscription } from '../common/state/agentSubscription.js'; import type { IAgentSubscription } from '../common/state/agentSubscription.js';
import type { CreateTerminalParams, ResolveSessionConfigResult, SessionConfigCompletionsResult } from '../common/state/protocol/commands.js'; import type { CreateTerminalParams, ResolveSessionConfigResult, SessionConfigCompletionsResult } from '../common/state/protocol/commands.js';
import type { ActionEnvelope, INotification, SessionAction, TerminalAction } from '../common/state/sessionActions.js'; import type { ActionEnvelope, INotification, SessionAction, TerminalAction } from '../common/state/sessionActions.js';
@@ -45,6 +45,7 @@ export class NullAgentHostService implements IAgentHostService {
async resolveSessionConfig(_params: IAgentResolveSessionConfigParams): Promise<ResolveSessionConfigResult> { return notSupported(); } async resolveSessionConfig(_params: IAgentResolveSessionConfigParams): Promise<ResolveSessionConfigResult> { return notSupported(); }
async sessionConfigCompletions(_params: IAgentSessionConfigCompletionsParams): Promise<SessionConfigCompletionsResult> { return notSupported(); } async sessionConfigCompletions(_params: IAgentSessionConfigCompletionsParams): Promise<SessionConfigCompletionsResult> { return notSupported(); }
async startWebSocketServer(): Promise<IAgentHostSocketInfo> { return notSupported(); } async startWebSocketServer(): Promise<IAgentHostSocketInfo> { return notSupported(); }
async getInspectInfo(_tryEnable: boolean): Promise<IAgentHostInspectInfo | undefined> { return undefined; }
async disposeSession(_session: URI): Promise<void> { } async disposeSession(_session: URI): Promise<void> { }
async createTerminal(_params: CreateTerminalParams): Promise<void> { notSupported(); } async createTerminal(_params: CreateTerminalParams): Promise<void> { notSupported(); }
async disposeTerminal(_terminal: URI): Promise<void> { } async disposeTerminal(_terminal: URI): Promise<void> { }

View File

@@ -41,6 +41,14 @@ export interface IAgentHostSocketInfo {
readonly socketPath: string; readonly socketPath: string;
} }
/** Inspector listener information for the agent host process. */
export interface IAgentHostInspectInfo {
readonly host: string;
readonly port: number;
/** A `devtools://` URL that can be opened with `INativeHostService.openDevToolsWindow`. */
readonly devtoolsUrl: string;
}
/** /**
* IPC service exposed on the {@link AgentHostIpcChannels.ConnectionTracker} * IPC service exposed on the {@link AgentHostIpcChannels.ConnectionTracker}
* channel. Used by the server process for lifetime management and by the * channel. Used by the server process for lifetime management and by the
@@ -55,6 +63,14 @@ export interface IConnectionTrackerService {
* If a server is already running, returns the existing info. * If a server is already running, returns the existing info.
*/ */
startWebSocketServer(): Promise<IAgentHostSocketInfo>; startWebSocketServer(): Promise<IAgentHostSocketInfo>;
/**
* Get inspector listener info for the agent host process. If the inspector
* is not currently active and `tryEnable` is true, opens the inspector on
* a random local port. Returns `undefined` if the inspector cannot be
* enabled (e.g. running in an environment without `node:inspector`).
*/
getInspectInfo(tryEnable: boolean): Promise<IAgentHostInspectInfo | undefined>;
} }
// ---- IPC data types (serializable across MessagePort) ----------------------- // ---- IPC data types (serializable across MessagePort) -----------------------
@@ -708,4 +724,12 @@ export interface IAgentHostService extends IAgentConnection {
restartAgentHost(): Promise<void>; restartAgentHost(): Promise<void>;
startWebSocketServer(): Promise<IAgentHostSocketInfo>; startWebSocketServer(): Promise<IAgentHostSocketInfo>;
/**
* Get inspector listener info for the agent host process. If the inspector
* is not currently active and `tryEnable` is true, opens the inspector on
* a random local port. Returns `undefined` if the inspector cannot be
* enabled.
*/
getInspectInfo(tryEnable: boolean): Promise<IAgentHostInspectInfo | undefined>;
} }

View File

@@ -14,7 +14,7 @@ import { acquirePort } from '../../../base/parts/ipc/electron-browser/ipc.mp.js'
import { InstantiationType, registerSingleton } from '../../instantiation/common/extensions.js'; import { InstantiationType, registerSingleton } from '../../instantiation/common/extensions.js';
import { IConfigurationService } from '../../configuration/common/configuration.js'; import { IConfigurationService } from '../../configuration/common/configuration.js';
import { ILogService } from '../../log/common/log.js'; import { ILogService } from '../../log/common/log.js';
import { AgentHostEnabledSettingId, AgentHostIpcChannels, IAgentCreateSessionConfig, IAgentHostService, IAgentResolveSessionConfigParams, IAgentService, IAgentSessionConfigCompletionsParams, IAgentSessionMetadata, AuthenticateParams, AuthenticateResult, IAgentHostSocketInfo, IConnectionTrackerService } from '../common/agentService.js'; import { AgentHostEnabledSettingId, AgentHostIpcChannels, IAgentCreateSessionConfig, IAgentHostInspectInfo, IAgentHostService, IAgentResolveSessionConfigParams, IAgentService, IAgentSessionConfigCompletionsParams, IAgentSessionMetadata, AuthenticateParams, AuthenticateResult, IAgentHostSocketInfo, IConnectionTrackerService } from '../common/agentService.js';
import { AgentSubscriptionManager, type IAgentSubscription } from '../common/state/agentSubscription.js'; import { AgentSubscriptionManager, type IAgentSubscription } from '../common/state/agentSubscription.js';
import type { CreateTerminalParams, ResolveSessionConfigResult, SessionConfigCompletionsResult } from '../common/state/protocol/commands.js'; import type { CreateTerminalParams, ResolveSessionConfigResult, SessionConfigCompletionsResult } from '../common/state/protocol/commands.js';
import type { ActionEnvelope, INotification, SessionAction, TerminalAction } from '../common/state/sessionActions.js'; import type { ActionEnvelope, INotification, SessionAction, TerminalAction } from '../common/state/sessionActions.js';
@@ -210,6 +210,10 @@ class AgentHostServiceClient extends Disposable implements IAgentHostService {
startWebSocketServer(): Promise<IAgentHostSocketInfo> { startWebSocketServer(): Promise<IAgentHostSocketInfo> {
return this._connectionTracker.startWebSocketServer(); return this._connectionTracker.startWebSocketServer();
} }
getInspectInfo(tryEnable: boolean): Promise<IAgentHostInspectInfo | undefined> {
return this._connectionTracker.getInspectInfo(tryEnable);
}
} }
registerSingleton(IAgentHostService, AgentHostServiceClient, InstantiationType.Delayed); registerSingleton(IAgentHostService, AgentHostServiceClient, InstantiationType.Delayed);

View File

@@ -13,7 +13,8 @@ import { isWindows } from '../../../base/common/platform.js';
import { URI } from '../../../base/common/uri.js'; import { URI } from '../../../base/common/uri.js';
import { generateUuid } from '../../../base/common/uuid.js'; import { generateUuid } from '../../../base/common/uuid.js';
import * as os from 'os'; import * as os from 'os';
import { AgentHostIpcChannels, IAgentHostSocketInfo, IConnectionTrackerService } from '../common/agentService.js'; import * as inspector from 'inspector';
import { AgentHostIpcChannels, IAgentHostInspectInfo, IAgentHostSocketInfo, IConnectionTrackerService } from '../common/agentService.js';
import { AgentService } from './agentService.js'; import { AgentService } from './agentService.js';
import { IAgentHostTerminalManager } from './agentHostTerminalManager.js'; import { IAgentHostTerminalManager } from './agentHostTerminalManager.js';
import { CopilotAgent } from './copilot/copilotAgent.js'; import { CopilotAgent } from './copilot/copilotAgent.js';
@@ -148,6 +149,52 @@ function startAgentHost(): void {
dynamicSocketInfo = { socketPath }; dynamicSocketInfo = { socketPath };
return dynamicSocketInfo; return dynamicSocketInfo;
}, },
async getInspectInfo(tryEnable: boolean): Promise<IAgentHostInspectInfo | undefined> {
let url = inspector.url();
if (!url && tryEnable) {
try {
inspector.open(0, '127.0.0.1', false);
} catch (err) {
logService.error('[AgentHost] Failed to open inspector', err);
return undefined;
}
url = inspector.url();
}
if (!url) {
return undefined;
}
// Inspector URL looks like: ws://host:port/uuid (host may be IPv6 in brackets)
try {
const parsedUrl = new URL(url);
if (parsedUrl.protocol !== 'ws:') {
logService.warn(`[AgentHost] Unexpected inspector URL: ${url}`);
return undefined;
}
const port = Number(parsedUrl.port);
const auth = parsedUrl.pathname.replace(/^\/+/, '');
if (!Number.isInteger(port) || !auth) {
logService.warn(`[AgentHost] Unexpected inspector URL: ${url}`);
return undefined;
}
const host = parsedUrl.hostname === '0.0.0.0'
? '127.0.0.1'
: parsedUrl.hostname === '::'
? '::1'
: parsedUrl.hostname;
const devtoolsHost = host.includes(':') ? `[${host}]` : host;
return {
host,
port,
devtoolsUrl: `devtools://devtools/bundled/js_app.html?v8only=true&ws=${devtoolsHost}:${parsedUrl.port}/${auth}`,
};
} catch {
logService.warn(`[AgentHost] Unexpected inspector URL: ${url}`);
return undefined;
}
},
}; };
const connectionTrackerChannel = ProxyChannel.fromService(connectionTrackerService, disposables); const connectionTrackerChannel = ProxyChannel.fromService(connectionTrackerService, disposables);
server.registerChannel(AgentHostIpcChannels.ConnectionTracker, connectionTrackerChannel); server.registerChannel(AgentHostIpcChannels.ConnectionTracker, connectionTrackerChannel);

View File

@@ -23,6 +23,7 @@ import { CopilotCLISessionType } from '../../../services/sessions/common/session
import { ISessionsManagementService } from '../../../services/sessions/common/sessionsManagement.js'; import { ISessionsManagementService } from '../../../services/sessions/common/sessionsManagement.js';
import { ISessionsProvidersService } from '../../../services/sessions/browser/sessionsProvidersService.js'; import { ISessionsProvidersService } from '../../../services/sessions/browser/sessionsProvidersService.js';
import { resolveRemoteAuthority } from '../browser/openInVSCodeUtils.js'; import { resolveRemoteAuthority } from '../browser/openInVSCodeUtils.js';
import { DebugAgentHostInDevToolsAction } from '../../../../workbench/contrib/chat/electron-browser/actions/debugAgentHostAction.js';
/** /**
* Desktop version of the "Open in VS Code" action. * Desktop version of the "Open in VS Code" action.
@@ -89,3 +90,5 @@ registerAction2(class OpenSessionWorktreeInVSCodeAction extends Action2 {
await nativeHostService.launchSiblingApp(args); await nativeHostService.launchSiblingApp(args);
} }
}); });
registerAction2(DebugAgentHostInDevToolsAction);

View File

@@ -10,9 +10,11 @@ import { Action2, registerAction2 } from '../../../../../platform/actions/common
import { INativeHostService } from '../../../../../platform/native/common/native.js'; import { INativeHostService } from '../../../../../platform/native/common/native.js';
import { ChatContextKeys } from '../../common/actions/chatContextKeys.js'; import { ChatContextKeys } from '../../common/actions/chatContextKeys.js';
import { IChatService } from '../../common/chatService/chatService.js'; import { IChatService } from '../../common/chatService/chatService.js';
import { DebugAgentHostInDevToolsAction } from './debugAgentHostAction.js';
export function registerChatDeveloperActions() { export function registerChatDeveloperActions() {
registerAction2(OpenChatStorageFolderAction); registerAction2(OpenChatStorageFolderAction);
registerAction2(DebugAgentHostInDevToolsAction);
} }
class OpenChatStorageFolderAction extends Action2 { class OpenChatStorageFolderAction extends Action2 {

View File

@@ -0,0 +1,43 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Codicon } from '../../../../../base/common/codicons.js';
import { ServicesAccessor } from '../../../../../editor/browser/editorExtensions.js';
import { localize, localize2 } from '../../../../../nls.js';
import { Categories } from '../../../../../platform/action/common/actionCommonCategories.js';
import { Action2 } from '../../../../../platform/actions/common/actions.js';
import { IAgentHostService } from '../../../../../platform/agentHost/common/agentService.js';
import { INativeHostService } from '../../../../../platform/native/common/native.js';
import { INotificationService } from '../../../../../platform/notification/common/notification.js';
import { ChatContextKeys } from '../../common/actions/chatContextKeys.js';
export class DebugAgentHostInDevToolsAction extends Action2 {
static readonly ID = 'workbench.action.chat.debugAgentHostInDevTools';
constructor() {
super({
id: DebugAgentHostInDevToolsAction.ID,
title: localize2('debugAgentHostInDevTools', "Debug Local Agent Host Process In Dev Tools"),
category: Categories.Developer,
f1: true,
icon: Codicon.debugStart,
precondition: ChatContextKeys.enabled,
});
}
override async run(accessor: ServicesAccessor): Promise<void> {
const agentHostService = accessor.get(IAgentHostService);
const nativeHostService = accessor.get(INativeHostService);
const notificationService = accessor.get(INotificationService);
const info = await agentHostService.getInspectInfo(true);
if (!info) {
notificationService.warn(localize('debugAgentHost.noInspectPort', "Could not enable the Node.js inspector for the agent host process."));
return;
}
nativeHostService.openDevToolsWindow(info.devtoolsUrl);
}
}