diff --git a/src/vs/platform/agentHost/common/agentService.ts b/src/vs/platform/agentHost/common/agentService.ts index e8111ebbc90094..4eeb119960f4a6 100644 --- a/src/vs/platform/agentHost/common/agentService.ts +++ b/src/vs/platform/agentHost/common/agentService.ts @@ -48,9 +48,6 @@ export function isAgentHostEnabled(configurationService: IConfigurationService): return !isWeb && !!configurationService.getValue(AgentHostEnabledSettingId); } -/** Configuration key that controls whether per-host IPC traffic output channels are created. */ -export const AgentHostIpcLoggingSettingId = 'chat.agentHost.ipcLoggingEnabled'; - /** Configuration key that controls whether AHP JSONL logs are written for agent host transports. */ export const AgentHostAhpJsonlLoggingSettingId = 'chat.agentHost.ahpJsonlLoggingEnabled'; diff --git a/src/vs/platform/agentHost/common/remoteAgentHostService.ts b/src/vs/platform/agentHost/common/remoteAgentHostService.ts index 3622e74c91c690..6ac254056a9e57 100644 --- a/src/vs/platform/agentHost/common/remoteAgentHostService.ts +++ b/src/vs/platform/agentHost/common/remoteAgentHostService.ts @@ -176,6 +176,10 @@ export function getEntryAddress(entry: IRemoteAgentHostEntry): string { } } +export function remoteAgentHostLogOutputChannelId(address: string): string { + return `agentHost.otlp.${address}`; +} + export const enum RemoteAgentHostInputValidationError { Empty = 'empty', Invalid = 'invalid', diff --git a/src/vs/sessions/common/agentHostSessionsProvider.ts b/src/vs/sessions/common/agentHostSessionsProvider.ts index eca1153d9b83b8..f674a71eeab262 100644 --- a/src/vs/sessions/common/agentHostSessionsProvider.ts +++ b/src/vs/sessions/common/agentHostSessionsProvider.ts @@ -22,8 +22,6 @@ export interface IAgentHostSessionsProvider extends ISessionsProvider { readonly connectionStatus?: IObservable; /** Remote address string, present on remote providers. */ readonly remoteAddress?: string; - /** Output channel ID for remote provider logs. */ - outputChannelId?: string; /** * Establish (or re-establish) the connection for this host on demand. * Tears down any existing connection first. Present on remote providers diff --git a/src/vs/sessions/contrib/providers/remoteAgentHost/REMOTE_AGENT_HOST_SESSIONS_PROVIDER.md b/src/vs/sessions/contrib/providers/remoteAgentHost/REMOTE_AGENT_HOST_SESSIONS_PROVIDER.md index 9e5c25973db72e..b3099a5c996e6d 100644 --- a/src/vs/sessions/contrib/providers/remoteAgentHost/REMOTE_AGENT_HOST_SESSIONS_PROVIDER.md +++ b/src/vs/sessions/contrib/providers/remoteAgentHost/REMOTE_AGENT_HOST_SESSIONS_PROVIDER.md @@ -71,10 +71,11 @@ Decoupling these allows copilot sessions from different providers (local CLI, re ## Connection Management -- `setConnection(connection, defaultDirectory?)` — Wires a live agent host connection; dynamically discovers session types from the host's root state agents +- `setConnection(connection, defaultDirectory?)` — Wires a live agent host connection directly; dynamically discovers session types from the host's root state agents - `clearConnection()` — Clears the connection when the host disconnects - Handles session notifications (`notify/sessionAdded`, `notify/sessionRemoved`) and state changes - Fires `onDidChangeSessionTypes` when the host's agent list changes +- Remote-host management options do not expose an IPC output channel; remote diagnostics use the host's forwarded logs when available. - SSH connection progress notifications are closed when the connect promise settles; keyboard-interactive prompt cancellation rejects the connect promise as cancellation and does not show an error notification. - SSH config host connections use resolved `IdentityFile` and `IdentityAgent` values from `ssh -G`; encrypted private keys are prompted for a passphrase through the same quick-input bridge as keyboard-interactive auth. - Startup SSH auto-reconnect treats keyboard-interactive cancellation as an intentional pause and does not schedule another reconnect attempt. diff --git a/src/vs/sessions/contrib/providers/remoteAgentHost/browser/remoteAgentHost.contribution.ts b/src/vs/sessions/contrib/providers/remoteAgentHost/browser/remoteAgentHost.contribution.ts index 957d9e7c1aac04..513ad870e12700 100644 --- a/src/vs/sessions/contrib/providers/remoteAgentHost/browser/remoteAgentHost.contribution.ts +++ b/src/vs/sessions/contrib/providers/remoteAgentHost/browser/remoteAgentHost.contribution.ts @@ -31,7 +31,6 @@ import { authenticateProtectedResources, AgentHostAuthTokenCache, resolveAuthent import { AgentHostLanguageModelProvider } from '../../../../../workbench/contrib/chat/browser/agentSessions/agentHost/agentHostLanguageModelProvider.js'; import { AgentHostSessionHandler } from '../../../../../workbench/contrib/chat/browser/agentSessions/agentHost/agentHostSessionHandler.js'; import { IAgentHostActiveClientService } from '../../../../../workbench/contrib/chat/browser/agentSessions/agentHost/agentHostActiveClientService.js'; -import { LoggingAgentConnection } from '../../../../../workbench/contrib/chat/browser/agentSessions/agentHost/loggingAgentConnection.js'; import { IChatSessionsService } from '../../../../../workbench/contrib/chat/common/chatSessionsService.js'; import { ICustomizationHarnessService } from '../../../../../workbench/contrib/chat/common/customizationHarnessService.js'; import { ILanguageModelsService } from '../../../../../workbench/contrib/chat/common/languageModels.js'; @@ -163,19 +162,14 @@ class ConnectionState extends Disposable { readonly store = this._register(new DisposableStore()); readonly agents = this._register(new DisposableMap()); readonly modelProviders = new Map(); - readonly loggedConnection: LoggingAgentConnection; /** Dedupes redundant `authenticate` RPCs when the resolved token hasn't changed. */ readonly authTokenCache = new AgentHostAuthTokenCache(); constructor( readonly name: string | undefined, - connection: IAgentConnection, - channelId: string, - channelLabel: string, - @IInstantiationService instantiationService: IInstantiationService, + readonly connection: IAgentConnection, ) { super(); - this.loggedConnection = this._register(instantiationService.createInstance(LoggingAgentConnection, connection, channelId, channelLabel)); } } @@ -267,17 +261,14 @@ export class RemoteAgentHostContribution extends Disposable implements IWorkbenc this._reconcileConnections(); this._reconnectSSHEntries(); - // Ensure every live connection is wired to its provider. - // This covers the case where a provider was recreated (e.g. name - // change) while a connection for that address already existed — - // we need to re-expose both the connection and the output channel, - // otherwise `Show Output` on the recreated provider would break. + // Ensure every live connection is wired to its provider. This covers + // the case where a provider was recreated (e.g. name change) while a + // connection for that address already existed. for (const [address, connState] of this._connections) { const connectionInfo = this._remoteAgentHostService.connections.find(c => c.address === address); const provider = this._providerInstances.get(address); if (provider) { - provider.setConnection(connState.loggedConnection, connectionInfo?.defaultDirectory); - provider.setOutputChannelId(connState.loggedConnection.channelId); + provider.setConnection(connState.connection, connectionInfo?.defaultDirectory); } } @@ -591,12 +582,12 @@ export class RemoteAgentHostContribution extends Disposable implements IWorkbenc const existing = this._connections.get(connectionInfo.address); if (existing) { const nameChanged = existing.name !== connectionInfo.name; - const clientIdChanged = existing.loggedConnection.clientId !== connectionInfo.clientId; + const clientIdChanged = existing.connection.clientId !== connectionInfo.clientId; // If the name or clientId changed, tear down and re-register if (nameChanged || clientIdChanged) { - this._logService.info(`[RemoteAgentHost] Reconnecting contribution for ${connectionInfo.address}: oldClientId=${existing.loggedConnection.clientId}, newClientId=${connectionInfo.clientId}, nameChanged=${nameChanged}`); - const oldClientId = existing.loggedConnection.clientId; + this._logService.info(`[RemoteAgentHost] Reconnecting contribution for ${connectionInfo.address}: oldClientId=${existing.connection.clientId}, newClientId=${connectionInfo.clientId}, nameChanged=${nameChanged}`); + const oldClientId = existing.connection.clientId; this._connections.deleteAndDispose(connectionInfo.address); this._setupConnection(connectionInfo); @@ -632,9 +623,7 @@ export class RemoteAgentHostContribution extends Disposable implements IWorkbenc } const { address, name } = connectionInfo; - const channelLabel = `Agent Host IPC (${name || address})`; - const connState = this._instantiationService.createInstance(ConnectionState, name, connection, `agenthost.${connection.clientId}`, channelLabel); - const loggedConnection = connState.loggedConnection; + const connState = this._instantiationService.createInstance(ConnectionState, name, connection); this._connections.set(address, connState); const store = connState.store; @@ -657,26 +646,24 @@ export class RemoteAgentHostContribution extends Disposable implements IWorkbenc store.add(this._agentHostFileSystemService.registerAuthority(authority, connection)); // React to root state changes (agent discovery) - store.add(loggedConnection.rootState.onDidChange(rootState => { - this._handleRootStateChange(address, loggedConnection, rootState); + store.add(connection.rootState.onDidChange(rootState => { + this._handleRootStateChange(address, connection, rootState); })); // If root state is already available, process it immediately - const initialRootState = loggedConnection.rootState.value; + const initialRootState = connection.rootState.value; if (initialRootState && !(initialRootState instanceof Error)) { - this._handleRootStateChange(address, loggedConnection, initialRootState); + this._handleRootStateChange(address, connection, initialRootState); } // Wire connection to existing sessions provider const provider = this._providerInstances.get(address); if (provider) { - provider.setConnection(loggedConnection, connectionInfo.defaultDirectory); - // Expose the output channel ID so the workspace picker can offer "Show Output" - provider.setOutputChannelId(loggedConnection.channelId); + provider.setConnection(connection, connectionInfo.defaultDirectory); } } - private _handleRootStateChange(address: string, loggedConnection: LoggingAgentConnection, rootState: RootState): void { + private _handleRootStateChange(address: string, connection: IAgentConnection, rootState: RootState): void { const connState = this._connections.get(address); if (!connState) { return; @@ -693,13 +680,13 @@ export class RemoteAgentHostContribution extends Disposable implements IWorkbenc } // Authenticate using protectedResources from agent info - this._authenticateWithConnection(address, loggedConnection, rootState.agents) + this._authenticateWithConnection(address, connection, rootState.agents) .catch(() => { /* best-effort */ }); // Register new agents, push model updates to existing ones for (const agent of rootState.agents) { if (!connState.agents.has(agent.provider)) { - this._registerAgent(address, loggedConnection, agent, connState.name); + this._registerAgent(address, connection, agent, connState.name); } else { const modelProvider = connState.modelProviders.get(agent.provider); modelProvider?.updateModels(agent.models); @@ -707,7 +694,7 @@ export class RemoteAgentHostContribution extends Disposable implements IWorkbenc } } - private _registerAgent(address: string, loggedConnection: LoggingAgentConnection, agent: AgentInfo, configuredName: string | undefined): void { + private _registerAgent(address: string, connection: IAgentConnection, agent: AgentInfo, configuredName: string | undefined): void { const connState = this._connections.get(address); if (!connState) { return; @@ -776,7 +763,7 @@ export class RemoteAgentHostContribution extends Disposable implements IWorkbenc const pluginController = agentStore.add(this._instantiationService.createInstance(RemoteAgentPluginController, hostLabel, sanitized, - loggedConnection, + connection, )); const itemProvider = agentStore.add(this._instantiationService.createInstance(AgentCustomizationItemProvider, sanitized, @@ -808,13 +795,13 @@ export class RemoteAgentHostContribution extends Disposable implements IWorkbenc sessionType, fullName: displayName, description: agent.description, - connection: loggedConnection, + connection, connectionAuthority: sanitized, extensionId: 'vscode.remote-agent-host', extensionDisplayName: 'Remote Agent Host', resolveWorkingDirectory, isNewSession, - resolveAuthentication: (resources) => this._resolveAuthenticationInteractively(address, loggedConnection, resources), + resolveAuthentication: (resources) => this._resolveAuthenticationInteractively(address, connection, resources), })); agentStore.add(this._chatSessionsService.registerChatSessionContentProvider(sessionType, sessionHandler)); @@ -835,9 +822,9 @@ export class RemoteAgentHostContribution extends Disposable implements IWorkbenc private _authenticateAllConnections(): void { for (const [address, connState] of this._connections) { - const rootState = connState.loggedConnection.rootState.value; + const rootState = connState.connection.rootState.value; if (rootState && !(rootState instanceof Error)) { - this._authenticateWithConnection(address, connState.loggedConnection, rootState.agents).catch(() => { /* best-effort */ }); + this._authenticateWithConnection(address, connState.connection, rootState.agents).catch(() => { /* best-effort */ }); } } } @@ -849,7 +836,7 @@ export class RemoteAgentHostContribution extends Disposable implements IWorkbenc * Marks the matching provider's `authenticationPending` observable while * the auth pass is in flight so that sessions surface as still loading. */ - private async _authenticateWithConnection(address: string, loggedConnection: LoggingAgentConnection, agents: readonly AgentInfo[]): Promise { + private async _authenticateWithConnection(address: string, connection: IAgentConnection, agents: readonly AgentInfo[]): Promise { const providerId = `agenthost-${agentHostAuthority(address)}`; const provider = this._sessionsProvidersService.getProvider(providerId); const authTokenCache = this._connections.get(address)?.authTokenCache; @@ -860,11 +847,10 @@ export class RemoteAgentHostContribution extends Disposable implements IWorkbenc authenticationService: this._authenticationService, logPrefix: '[RemoteAgentHost]', logService: this._logService, - authenticate: request => loggedConnection.authenticate(request), + authenticate: request => connection.authenticate(request), }); } catch (err) { this._logService.error('[RemoteAgentHost] Failed to authenticate with connection', err); - loggedConnection.logError('authenticateWithConnection', err); } finally { provider?.setAuthenticationPending(false); } @@ -874,7 +860,7 @@ export class RemoteAgentHostContribution extends Disposable implements IWorkbenc * Interactively prompt the user to authenticate when the server requires it. * Returns true if authentication succeeded. */ - private async _resolveAuthenticationInteractively(address: string, loggedConnection: LoggingAgentConnection, protectedResources: readonly ProtectedResourceMetadata[]): Promise { + private async _resolveAuthenticationInteractively(address: string, connection: IAgentConnection, protectedResources: readonly ProtectedResourceMetadata[]): Promise { const authTokenCache = this._connections.get(address)?.authTokenCache; try { return await resolveAuthenticationInteractively(protectedResources, { @@ -882,11 +868,10 @@ export class RemoteAgentHostContribution extends Disposable implements IWorkbenc authenticationService: this._authenticationService, logPrefix: '[RemoteAgentHost]', logService: this._logService, - authenticate: request => loggedConnection.authenticate(request), + authenticate: request => connection.authenticate(request), }); } catch (err) { this._logService.error('[RemoteAgentHost] Interactive authentication failed', err); - loggedConnection.logError('resolveAuthenticationInteractively', err); } return false; } diff --git a/src/vs/sessions/contrib/providers/remoteAgentHost/browser/remoteAgentHostLogForwarder.ts b/src/vs/sessions/contrib/providers/remoteAgentHost/browser/remoteAgentHostLogForwarder.ts index 878ec76cb41362..05f296bb16e446 100644 --- a/src/vs/sessions/contrib/providers/remoteAgentHost/browser/remoteAgentHostLogForwarder.ts +++ b/src/vs/sessions/contrib/providers/remoteAgentHost/browser/remoteAgentHostLogForwarder.ts @@ -10,6 +10,7 @@ import { ILogService, LogLevel } from '../../../../../platform/log/common/log.js import { Registry } from '../../../../../platform/registry/common/platform.js'; import { iterateOtlpLogRecords, logLevelToOtlpLevelName, severityNumberToLogLevel, type IOtlpLogRecord, type OtlpLogLevelName } from '../../../../../platform/agentHost/common/otlp/otlpLogEmitter.js'; import { AgentHostClientState, type RemoteAgentHostProtocolClient } from '../../../../../platform/agentHost/browser/remoteAgentHostProtocolClient.js'; +import { remoteAgentHostLogOutputChannelId } from '../../../../../platform/agentHost/common/remoteAgentHostService.js'; import { Extensions, IOutputChannel, IOutputChannelRegistry, IOutputService } from '../../../../../workbench/services/output/common/output.js'; /** @@ -57,7 +58,7 @@ export class RemoteAgentHostLogForwarder extends Disposable { ) { super(); - this._channelId = `agentHost.otlp.${address}`; + this._channelId = remoteAgentHostLogOutputChannelId(address); this._channelLabel = `Agent Host (${displayName})`; // Wire up subscribe/teardown around the client's connection state. diff --git a/src/vs/sessions/contrib/providers/remoteAgentHost/browser/remoteAgentHostSessionsProvider.ts b/src/vs/sessions/contrib/providers/remoteAgentHost/browser/remoteAgentHostSessionsProvider.ts index d88635632fb9c9..93174aeb145968 100644 --- a/src/vs/sessions/contrib/providers/remoteAgentHost/browser/remoteAgentHostSessionsProvider.ts +++ b/src/vs/sessions/contrib/providers/remoteAgentHost/browser/remoteAgentHostSessionsProvider.ts @@ -137,9 +137,6 @@ export class RemoteAgentHostSessionsProvider extends BaseAgentHostSessionsProvid readonly remoteAddress: string; readonly browseActions: readonly ISessionWorkspaceBrowseAction[]; - private _outputChannelId: string | undefined; - get outputChannelId(): string | undefined { return this._outputChannelId; } - private readonly _connectionStatus = observableValue('connectionStatus', RemoteAgentHostConnectionStatus.disconnected); readonly connectionStatus: IObservable = this._connectionStatus; @@ -353,11 +350,6 @@ export class RemoteAgentHostSessionsProvider extends BaseAgentHostSessionsProvid this._connectionStatus.set(status, undefined); } - /** Set the output channel ID for this provider's IPC log. */ - setOutputChannelId(id: string): void { - this._outputChannelId = id; - } - setAuthenticationPending(pending: boolean): void { // Sticky: once the first authentication pass settles, never surface // pending again. Subsequent re-auths happen silently in the background. diff --git a/src/vs/sessions/contrib/providers/remoteAgentHost/browser/remoteAgentHostTerminal.contribution.ts b/src/vs/sessions/contrib/providers/remoteAgentHost/browser/remoteAgentHostTerminal.contribution.ts index 25579531e5e77c..beb8f751c87d5b 100644 --- a/src/vs/sessions/contrib/providers/remoteAgentHost/browser/remoteAgentHostTerminal.contribution.ts +++ b/src/vs/sessions/contrib/providers/remoteAgentHost/browser/remoteAgentHostTerminal.contribution.ts @@ -4,11 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import { Disposable, DisposableMap } from '../../../../../base/common/lifecycle.js'; -import { localize } from '../../../../../nls.js'; import { IRemoteAgentHostService, RemoteAgentHostConnectionStatus } from '../../../../../platform/agentHost/common/remoteAgentHostService.js'; -import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; import { registerWorkbenchContribution2, WorkbenchPhase } from '../../../../../workbench/common/contributions.js'; -import { LoggingAgentConnection } from '../../../../../workbench/contrib/chat/browser/agentSessions/agentHost/loggingAgentConnection.js'; import { IAgentHostTerminalService } from '../../../../../workbench/contrib/terminal/browser/agentHostTerminalService.js'; /** @@ -21,7 +18,6 @@ class RemoteAgentHostTerminalContribution extends Disposable { constructor( @IRemoteAgentHostService private readonly _remoteAgentHostService: IRemoteAgentHostService, @IAgentHostTerminalService private readonly _agentHostTerminalService: IAgentHostTerminalService, - @IInstantiationService private readonly _instantiationService: IInstantiationService, ) { super(); @@ -45,12 +41,7 @@ class RemoteAgentHostTerminalContribution extends Disposable { this._remoteEntries.set(info.address, this._agentHostTerminalService.registerEntry({ name: info.name || info.address, address: info.address, - getConnection: () => this._instantiationService.createInstance( - LoggingAgentConnection, - connection, - `agenthost.${connection.clientId}`, - localize('agentHostTerminal.channelRemote', "Agent Host Terminal ({0})", info.address), - ), + getConnection: () => connection, })); } } diff --git a/src/vs/sessions/contrib/providers/remoteAgentHost/browser/remoteHostOptions.ts b/src/vs/sessions/contrib/providers/remoteAgentHost/browser/remoteHostOptions.ts index 81d571225b412e..dbe4b3cd0386a3 100644 --- a/src/vs/sessions/contrib/providers/remoteAgentHost/browser/remoteHostOptions.ts +++ b/src/vs/sessions/contrib/providers/remoteAgentHost/browser/remoteHostOptions.ts @@ -13,7 +13,6 @@ import { IRemoteAgentHostService, RemoteAgentHostConnectionStatus } from '../../ import { IClipboardService } from '../../../../../platform/clipboard/common/clipboardService.js'; import { IInstantiationService, ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js'; import { IQuickInputService, IQuickPickItem } from '../../../../../platform/quickinput/common/quickInput.js'; -import { IOutputService } from '../../../../../workbench/services/output/common/output.js'; import { IPreferencesService } from '../../../../../workbench/services/preferences/common/preferences.js'; import { IAgentHostSessionsProvider } from '../../../../common/agentHostSessionsProvider.js'; import { IProductService } from '../../../../../platform/product/common/productService.js'; @@ -236,7 +235,7 @@ export interface IShowRemoteHostOptionsOptions { /** * Show the per-remote management options quickpick (Reconnect / Remove / - * Copy Address / Open Settings / Show Output) for the given provider. + * Copy Address / Open Settings) for the given provider. * * Used by both the Workspace Picker's Manage submenu and the F1 * "Manage Remote Agent Hosts..." command, so both surfaces drive the @@ -256,7 +255,6 @@ export async function showRemoteHostOptions(accessor: ServicesAccessor, provider const remoteAgentHostService = accessor.get(IRemoteAgentHostService); const clipboardService = accessor.get(IClipboardService); const preferencesService = accessor.get(IPreferencesService); - const outputService = accessor.get(IOutputService); const productService = accessor.get(IProductService); const instantiationService = accessor.get(IInstantiationService); @@ -277,9 +275,6 @@ export async function showRemoteHostOptions(accessor: ServicesAccessor, provider { label: '$(copy) ' + localize('workspacePicker.copyAddress', "Copy Address"), id: 'copy' }, { label: '$(settings-gear) ' + localize('workspacePicker.openSettings', "Open Settings"), id: 'settings' }, ); - if (provider.outputChannelId) { - items.push({ label: '$(output) ' + localize('workspacePicker.showOutput', "Show Output"), id: 'output' }); - } const result = await new Promise<'back' | RemoteOptionPickItem | undefined>((resolve) => { const store = new DisposableStore(); @@ -343,11 +338,6 @@ export async function showRemoteHostOptions(accessor: ServicesAccessor, provider case 'settings': await preferencesService.openSettings({ query: 'chat.remoteAgentHosts' }); break; - case 'output': - if (provider.outputChannelId) { - outputService.showChannel(provider.outputChannelId, true); - } - break; } return undefined; } diff --git a/src/vs/workbench/contrib/chat/browser/actions/exportAgentHostDebugLogsAction.ts b/src/vs/workbench/contrib/chat/browser/actions/exportAgentHostDebugLogsAction.ts index 30c6222e623aa8..96f20cc63efc97 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/exportAgentHostDebugLogsAction.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/exportAgentHostDebugLogsAction.ts @@ -12,7 +12,7 @@ import { Categories } from '../../../../../platform/action/common/actionCommonCa import { Action2 } from '../../../../../platform/actions/common/actions.js'; import { agentHostAuthority, toAgentHostUri } from '../../../../../platform/agentHost/common/agentHostUri.js'; import { AgentHostEnabledSettingId, IAgentHostService } from '../../../../../platform/agentHost/common/agentService.js'; -import { IRemoteAgentHostConnectionInfo, IRemoteAgentHostService } from '../../../../../platform/agentHost/common/remoteAgentHostService.js'; +import { IRemoteAgentHostConnectionInfo, IRemoteAgentHostService, remoteAgentHostLogOutputChannelId } from '../../../../../platform/agentHost/common/remoteAgentHostService.js'; import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js'; import { IsWebContext } from '../../../../../platform/contextkey/common/contextkeys.js'; import { IFileDialogService } from '../../../../../platform/dialogs/common/dialogs.js'; @@ -77,8 +77,9 @@ export class BrowserAgentHostDebugLogsExportService implements IAgentHostDebugLo /** * Shared implementation of "Export Agent Host Debug Logs". Collects the - * Copilot CLI session events file (if available), the window/shared/agent-host - * output channel logs, and the AHP transport JSONL logs. + * Copilot CLI session events file (if available), the window/shared/local + * agent-host output channel logs, remote forwarded logs, and the AHP + * transport JSONL logs. * * Both the workbench-side action (resolves the active session via * `IChatWidgetService`) and the sessions-app-side action (resolves it via @@ -118,20 +119,18 @@ export async function collectAgentHostDebugLogs( if (activeSession.isLocal) { // Agent host process logger (forwarded from the utility process) channelIds.add(AGENT_HOST_LOGGER_CHANNEL_ID); - channelIds.add(`agenthost.${agentHostService.clientId}`); const localClientId = sanitizeFilePart(agentHostService.clientId); ahpLogNameFilter = name => name.includes(localClientId); } else { remoteConnection = getRemoteConnectionForSession(activeSession.resource, remoteAgentHostService.connections); if (remoteConnection) { - channelIds.add(`agenthost.${remoteConnection.clientId}`); + channelIds.add(remoteAgentHostLogOutputChannelId(remoteConnection.address)); } } } else { channelIds.add(AGENT_HOST_LOGGER_CHANNEL_ID); - channelIds.add(`agenthost.${agentHostService.clientId}`); for (const connection of remoteAgentHostService.connections) { - channelIds.add(`agenthost.${connection.clientId}`); + channelIds.add(remoteAgentHostLogOutputChannelId(connection.address)); } } diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostChatContribution.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostChatContribution.ts index b94ca59adc032d..818711d132bbe2 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostChatContribution.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostChatContribution.ts @@ -28,7 +28,6 @@ import { AgentHostLanguageModelProvider } from './agentHostLanguageModelProvider import { AgentHostSessionHandler } from './agentHostSessionHandler.js'; import { IAgentHostActiveClientService } from './agentHostActiveClientService.js'; import { AgentHostSessionListController } from './agentHostSessionListController.js'; -import { LoggingAgentConnection } from './loggingAgentConnection.js'; import { AICustomizationSources } from '../../../common/aiCustomizationWorkspaceService.js'; export { AgentHostSessionHandler } from './agentHostSessionHandler.js'; @@ -45,8 +44,6 @@ export class AgentHostContribution extends Disposable implements IWorkbenchContr static readonly ID = 'workbench.contrib.agentHostContribution'; - private _loggedConnection: LoggingAgentConnection | undefined; - private readonly _agentRegistrations = this._register(new DisposableMap()); /** Model providers keyed by agent provider, for pushing model updates. */ private readonly _modelProviders = new Map(); @@ -80,13 +77,6 @@ export class AgentHostContribution extends Disposable implements IWorkbenchContr return; } - // Wrap the agent host service with logging to a dedicated output channel - this._loggedConnection = this._register(this._instantiationService.createInstance( - LoggingAgentConnection, - this._agentHostService, - `agenthost.${this._agentHostService.clientId}`, - 'Agent Host (Local)')); - this._register(_agentHostFileSystemService.registerAuthority('local', this._agentHostService)); // React to root state changes (agent discovery / removal) @@ -175,7 +165,7 @@ export class AgentHostContribution extends Disposable implements IWorkbenchContr })); // Session list controller - const listController = store.add(this._instantiationService.createInstance(AgentHostSessionListController, sessionType, agent.provider, this._loggedConnection!, undefined, 'local')); + const listController = store.add(this._instantiationService.createInstance(AgentHostSessionListController, sessionType, agent.provider, this._agentHostService, undefined, 'local')); this._listControllers.set(agent.provider, listController); store.add({ dispose: () => this._listControllers.delete(agent.provider) }); store.add(this._chatSessionsService.registerChatSessionItemController(sessionType, listController)); @@ -203,7 +193,7 @@ export class AgentHostContribution extends Disposable implements IWorkbenchContr sessionType, fullName: agent.displayName, description: agent.description, - connection: this._loggedConnection!, + connection: this._agentHostService, connectionAuthority: 'local', isNewSession: sessionResource => listController.isNewSession(sessionResource), resolveAuthentication: (resources) => this._resolveAuthenticationInteractively(resources), @@ -250,11 +240,10 @@ export class AgentHostContribution extends Disposable implements IWorkbenchContr authenticationService: this._authenticationService, logPrefix: '[AgentHost]', logService: this._logService, - authenticate: request => this._loggedConnection!.authenticate(request), + authenticate: request => this._agentHostService.authenticate(request), }); } catch (err) { this._logService.error('[AgentHost] Failed to authenticate with server', err); - this._loggedConnection!.logError('authenticateWithServer', err); } finally { this._agentHostService.setAuthenticationPending(false); } @@ -273,11 +262,10 @@ export class AgentHostContribution extends Disposable implements IWorkbenchContr authenticationService: this._authenticationService, logPrefix: '[AgentHost]', logService: this._logService, - authenticate: request => this._loggedConnection!.authenticate(request), + authenticate: request => this._agentHostService.authenticate(request), }); } catch (err) { this._logService.error('[AgentHost] Interactive authentication failed', err); - this._loggedConnection!.logError('resolveAuthenticationInteractively', err); } return false; } diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostTerminalContribution.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostTerminalContribution.ts index 56e48f4c63488a..c2bf3db0c1cb12 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostTerminalContribution.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostTerminalContribution.ts @@ -11,10 +11,8 @@ import { AgentHostConfigKey } from '../../../../../../platform/agentHost/common/ import { ActionType } from '../../../../../../platform/agentHost/common/state/protocol/actions.js'; import { ROOT_STATE_URI } from '../../../../../../platform/agentHost/common/state/sessionState.js'; import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; -import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; import { TerminalSettingId } from '../../../../../../platform/terminal/common/terminal.js'; import { IWorkbenchContribution } from '../../../../../../workbench/common/contributions.js'; -import { LoggingAgentConnection } from '../../../../../../workbench/contrib/chat/browser/agentSessions/agentHost/loggingAgentConnection.js'; import { ITerminalProfileResolverService, ITerminalProfileService } from '../../../../../../workbench/contrib/terminal/common/terminal.js'; import { IAgentHostTerminalService } from '../../../../../../workbench/contrib/terminal/browser/agentHostTerminalService.js'; @@ -46,7 +44,6 @@ export class AgentHostTerminalContribution extends Disposable implements IWorkbe constructor( @IAgentHostService private readonly _agentHostService: IAgentHostService, @IAgentHostTerminalService private readonly _agentHostTerminalService: IAgentHostTerminalService, - @IInstantiationService private readonly _instantiationService: IInstantiationService, @IConfigurationService private readonly _configurationService: IConfigurationService, @ITerminalProfileService private readonly _terminalProfileService: ITerminalProfileService, @ITerminalProfileResolverService private readonly _terminalProfileResolverService: ITerminalProfileResolverService, @@ -98,12 +95,7 @@ export class AgentHostTerminalContribution extends Disposable implements IWorkbe this._localEntry.value = this._agentHostTerminalService.registerEntry({ name: localize('agentHostTerminal.local', "Local"), address: '__local__', - getConnection: () => this._instantiationService.createInstance( - LoggingAgentConnection, - this._agentHostService, - `agenthost.${this._agentHostService.clientId}`, - localize('agentHostTerminal.channelLocal', "Agent Host Terminal (Local)"), - ), + getConnection: () => this._agentHostService, }); } this._pushDefaultShell(); diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/loggingAgentConnection.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/loggingAgentConnection.ts deleted file mode 100644 index 94cc022b51c63a..00000000000000 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/loggingAgentConnection.ts +++ /dev/null @@ -1,316 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { Emitter, Event } from '../../../../../../base/common/event.js'; -import { Disposable, DisposableStore, IDisposable, IReference, toDisposable } from '../../../../../../base/common/lifecycle.js'; -import { URI, UriComponents } from '../../../../../../base/common/uri.js'; -import { Registry } from '../../../../../../platform/registry/common/platform.js'; -import { IAgentConnection, IAgentCreateSessionConfig, IAgentResolveSessionConfigParams, IAgentSessionConfigCompletionsParams, IAgentSessionMetadata, AuthenticateParams, AuthenticateResult, AgentHostIpcLoggingSettingId } from '../../../../../../platform/agentHost/common/agentService.js'; -import type { IAgentSubscription } from '../../../../../../platform/agentHost/common/state/agentSubscription.js'; -import { StateComponents, type ComponentToState, type RootState } from '../../../../../../platform/agentHost/common/state/sessionState.js'; -import type { ActionEnvelope, IRootConfigChangedAction, SessionAction, TerminalAction, INotification } from '../../../../../../platform/agentHost/common/state/sessionActions.js'; -import type { CompletionsParams, CompletionsResult, CreateTerminalParams, ResolveSessionConfigResult, SessionConfigCompletionsResult } from '../../../../../../platform/agentHost/common/state/protocol/commands.js'; -import type { IRemoteWatchHandle } from '../../../../../../platform/agentHost/common/agentHostFileSystemProvider.js'; -import type { CreateResourceWatchParams, CreateResourceWatchResult, ResourceCopyParams, ResourceCopyResult, ResourceDeleteParams, ResourceDeleteResult, ResourceListResult, ResourceMkdirParams, ResourceMkdirResult, ResourceMoveParams, ResourceMoveResult, ResourceReadResult, ResourceResolveParams, ResourceResolveResult, ResourceWriteParams, ResourceWriteResult } from '../../../../../../platform/agentHost/common/state/sessionProtocol.js'; -import { Extensions, IOutputChannel, IOutputChannelRegistry, IOutputService } from '../../../../../services/output/common/output.js'; -import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; - -/** - * JSON replacer that serializes revived URI objects to their string form, - * keeping the rest of the payload intact. - */ -function uriReplacer(_key: string, value: unknown): unknown { - if (value && typeof value === 'object' && (value as { $mid?: unknown }).$mid !== undefined && (value as { scheme?: unknown }).scheme !== undefined) { - return URI.revive(value as UriComponents).toString(); - } - return value; -} - -function formatPayload(data: unknown): string { - if (data === undefined) { - return ''; - } - try { - return JSON.stringify(data, uriReplacer, 2); - } catch { - return String(data); - } -} - -class LoggingAgentSubscription extends Disposable implements IAgentSubscription { - - private readonly _onDidChange = this._register(new Emitter()); - readonly onDidChange: Event = this._onDidChange.event; - - readonly onWillApplyAction: Event; - readonly onDidApplyAction: Event; - - constructor( - private readonly _label: string, - private readonly _inner: IAgentSubscription, - logCurrentValue: boolean, - private readonly _log: (arrow: string, method: string, data?: unknown) => void, - ) { - super(); - - this.onWillApplyAction = _inner.onWillApplyAction; - this.onDidApplyAction = _inner.onDidApplyAction; - - if (logCurrentValue && _inner.value !== undefined) { - this._log('**', `${this._label}.current`, _inner.value); - } - - this._register(_inner.onDidChange(value => { - this._log('**', `${this._label}.onDidChange`, value); - this._onDidChange.fire(value); - })); - } - - get value(): T | Error | undefined { - return this._inner.value; - } - - get verifiedValue(): T | undefined { - return this._inner.verifiedValue; - } -} - -/** - * A logging wrapper around an {@link IAgentConnection} that writes all IPC - * traffic to a dedicated output channel. Used by both local and remote agent - * host contributions to provide per-host IPC tracing. - * - * The output channel is registered on first construction for a given channel - * ID and ref-counted across instances, so it survives reconnections and is - * only removed when the last instance for that ID is disposed. - * - * All method calls, results, errors, and events are logged with arrows: - * - `>>` for outgoing calls - * - `<<` for results - * - `!!` for errors - * - `**` for events (onDidAction, onDidNotification) - */ -export class LoggingAgentConnection extends Disposable implements IAgentConnection { - - declare readonly _serviceBrand: undefined; - - /** Ref-count per channel ID so the output channel survives reconnections. */ - private static readonly _channelRefCounts = new Map(); - private static readonly _currentRootStateLogKeys = new Set(); - /** - * Shared event-log subscription per channel ID. Multiple wrappers may - * exist for the same underlying connection (e.g. one for chat, one for - * terminal); we only want each event to appear once in the channel. - */ - private static readonly _sharedEventLog = new Map(); - - private _outputChannel: IOutputChannel | undefined; - private readonly _enabled: boolean; - - readonly clientId: string; - readonly onDidAction: Event; - readonly onDidNotification: Event; - private readonly _rootState: IAgentSubscription; - - constructor( - private readonly _inner: IAgentConnection, - public readonly channelId: string, - private readonly _channelLabel: string, - @IOutputService private readonly _outputService: IOutputService, - @IConfigurationService configurationService: IConfigurationService, - ) { - super(); - this.clientId = _inner.clientId; - this._enabled = !!configurationService.getValue(AgentHostIpcLoggingSettingId); - const currentRootStateLogKey = `${this.channelId}:rootState.current`; - let logCurrentRootState = false; - - if (this._enabled) { - const registry = Registry.as(Extensions.OutputChannels); - const refs = LoggingAgentConnection._channelRefCounts.get(this.channelId) ?? 0; - if (refs === 0) { - registry.registerChannel({ - id: this.channelId, - label: this._channelLabel, - log: false, - languageId: 'log', - }); - const eventLogStore = new DisposableStore(); - eventLogStore.add(_inner.onDidAction(e => this._log('**', 'onDidAction', e))); - eventLogStore.add(_inner.onDidNotification(e => this._log('**', 'onDidNotification', e))); - LoggingAgentConnection._sharedEventLog.set(this.channelId, eventLogStore); - } - LoggingAgentConnection._channelRefCounts.set(this.channelId, refs + 1); - logCurrentRootState = !LoggingAgentConnection._currentRootStateLogKeys.has(currentRootStateLogKey); - if (logCurrentRootState) { - LoggingAgentConnection._currentRootStateLogKeys.add(currentRootStateLogKey); - } - this._register(toDisposable(() => { - const current = LoggingAgentConnection._channelRefCounts.get(this.channelId)! - 1; - if (current <= 0) { - LoggingAgentConnection._channelRefCounts.delete(this.channelId); - LoggingAgentConnection._currentRootStateLogKeys.delete(currentRootStateLogKey); - LoggingAgentConnection._sharedEventLog.get(this.channelId)?.dispose(); - LoggingAgentConnection._sharedEventLog.delete(this.channelId); - registry.removeChannel(this.channelId); - } else { - LoggingAgentConnection._channelRefCounts.set(this.channelId, current); - } - })); - } - - // Expose the inner events directly. Logging happens once per channel - // via the shared subscription registered above; wrappers must not - // add their own logging listener or events would be logged N times - // (once per wrapper for the same channel). - this.onDidAction = _inner.onDidAction; - this.onDidNotification = _inner.onDidNotification; - - this._rootState = this._register(new LoggingAgentSubscription('rootState', _inner.rootState, logCurrentRootState, (arrow, method, data) => this._log(arrow, method, data))); - } - - // ---- IAgentConnection method proxies with logging ----------------------- - - async authenticate(params: AuthenticateParams): Promise { - return this._logCall('authenticate', params, () => this._inner.authenticate(params)); - } - - async listSessions(): Promise { - return this._logCall('listSessions', undefined, () => this._inner.listSessions()); - } - - async createSession(config?: IAgentCreateSessionConfig): Promise { - return this._logCall('createSession', config, () => this._inner.createSession(config)); - } - - async resolveSessionConfig(params: IAgentResolveSessionConfigParams): Promise { - return this._logCall('resolveSessionConfig', params, () => this._inner.resolveSessionConfig(params)); - } - - async sessionConfigCompletions(params: IAgentSessionConfigCompletionsParams): Promise { - return this._logCall('sessionConfigCompletions', params, () => this._inner.sessionConfigCompletions(params)); - } - - async completions(params: CompletionsParams): Promise { - return this._logCall('completions', params, () => this._inner.completions(params)); - } - - async getCompletionTriggerCharacters(): Promise { - return this._inner.getCompletionTriggerCharacters(); - } - - async disposeSession(session: URI): Promise { - return this._logCall('disposeSession', session, () => this._inner.disposeSession(session)); - } - - async createTerminal(params: CreateTerminalParams): Promise { - return this._logCall('createTerminal', params, () => this._inner.createTerminal(params)); - } - - async disposeTerminal(terminal: URI): Promise { - return this._logCall('disposeTerminal', terminal, () => this._inner.disposeTerminal(terminal)); - } - - get rootState(): IAgentSubscription { - return this._rootState; - } - - getSubscription(kind: T, resource: URI): IReference> { - return this._inner.getSubscription(kind, resource); - } - - getSubscriptionUnmanaged(kind: T, resource: URI): IAgentSubscription | undefined { - return this._inner.getSubscriptionUnmanaged(kind, resource); - } - - dispatch(channel: string, action: SessionAction | TerminalAction | IRootConfigChangedAction): void { - this._log('>>', 'dispatch', { channel, action }); - this._inner.dispatch(channel, action); - } - - async resourceList(uri: URI): Promise { - return this._logCall('resourceList', uri, () => this._inner.resourceList(uri)); - } - - async resourceRead(uri: URI): Promise { - return this._logCall('resourceRead', uri, () => this._inner.resourceRead(uri)); - } - - async resourceWrite(params: ResourceWriteParams): Promise { - return this._logCall('resourceWrite', params, () => this._inner.resourceWrite(params)); - } - - async resourceCopy(params: ResourceCopyParams): Promise { - return this._logCall('resourceCopy', params, () => this._inner.resourceCopy(params)); - } - - async resourceDelete(params: ResourceDeleteParams): Promise { - return this._logCall('resourceDelete', params, () => this._inner.resourceDelete(params)); - } - - async resourceMove(params: ResourceMoveParams): Promise { - return this._logCall('resourceMove', params, () => this._inner.resourceMove(params)); - } - - async resourceResolve(params: ResourceResolveParams): Promise { - return this._logCall('resourceResolve', params, () => this._inner.resourceResolve(params)); - } - - async resourceMkdir(params: ResourceMkdirParams): Promise { - return this._logCall('resourceMkdir', params, () => this._inner.resourceMkdir(params)); - } - - async createResourceWatch(params: CreateResourceWatchParams): Promise { - return this._logCall('createResourceWatch', params, () => this._inner.createResourceWatch(params)); - } - - watchResource(params: CreateResourceWatchParams): Promise { - // Watcher setup is a single round-trip; the running watcher - // produces events on its own channel which is logged separately - // via the action stream, so no need to wrap the handle. - return this._logCall('watchResource', params, () => this._inner.watchResource(params)); - } - - // ---- Public logging API for callers' catch blocks ----------------------- - - /** - * Log an error to the output channel. Use this from caller catch blocks - * so connection errors appear in the per-host channel. - */ - logError(context: string, error: unknown): void { - this._log('!!', context, error instanceof Error ? error.message : String(error)); - } - - // ---- Internal helpers --------------------------------------------------- - - private async _logCall(method: string, params: unknown, fn: () => Promise): Promise { - this._log('>>', method, params); - try { - const result = await fn(); - this._log('<<', method, result); - return result; - } catch (err) { - this._log('!!', method, err instanceof Error ? err.message : String(err)); - throw err; - } - } - - private _log(arrow: string, method: string, data?: unknown): void { - if (!this._enabled) { - return; - } - - if (!this._outputChannel) { - this._outputChannel = this._outputService.getChannel(this.channelId); - if (!this._outputChannel) { - return; - } - } - - const timestamp = new Date().toISOString(); - const payload = formatPayload(data); - this._outputChannel.append(`[${timestamp}] ${arrow} ${method}${payload ? `\n${payload}` : ''}\n`); - } -} diff --git a/src/vs/workbench/contrib/chat/browser/chat.shared.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.shared.contribution.ts index f97bccf1c5d89f..52b537d070c6b4 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.shared.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.shared.contribution.ts @@ -11,7 +11,7 @@ import { isMacintosh } from '../../../../base/common/platform.js'; import { PolicyCategory } from '../../../../base/common/policy.js'; import '../../../../platform/agentHost/common/agentHost.config.contribution.js'; import '../../../../platform/agentHost/common/agentHostStarter.config.contribution.js'; -import { AgentHostAhpJsonlLoggingSettingId, AgentHostCustomTerminalToolEnabledSettingId, AgentHostIpcLoggingSettingId } from '../../../../platform/agentHost/common/agentService.js'; +import { AgentHostAhpJsonlLoggingSettingId, AgentHostCustomTerminalToolEnabledSettingId } from '../../../../platform/agentHost/common/agentService.js'; import { AgentNetworkFilterService, IAgentNetworkFilterService } from '../../../../platform/networkFilter/common/networkFilterService.js'; import { AgentNetworkDomainSettingId } from '../../../../platform/networkFilter/common/settings.js'; import { AgentSandboxEnabledValue, AgentSandboxSettingId } from '../../../../platform/sandbox/common/settings.js'; @@ -1102,12 +1102,6 @@ configurationRegistry.registerConfiguration({ description: nls.localize('chat.newSession.defaultMode', "The default mode for new chat sessions. When empty, the chat view's default mode is used."), default: '', }, - [AgentHostIpcLoggingSettingId]: { - type: 'boolean', - description: nls.localize('chat.agentHost.ipcLogging', "When enabled, logs all IPC traffic for each agent host to a dedicated output channel."), - default: product.quality !== 'stable', - tags: ['experimental', 'advanced'], - }, [AgentHostAhpJsonlLoggingSettingId]: { type: 'boolean', description: nls.localize('chat.agentHost.ahpJsonlLogging', "When enabled, logs all AHP transport messages for agent host connections to JSONL files under the window's log directory."),