From b9d284036a5983ec09e85a766131e208479d9d17 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Mon, 4 May 2026 14:39:10 -0400 Subject: [PATCH 1/8] Add static commands for opening Agent Host chat sessions The dynamic per-agent commands (registered as workbench.action.chat.openNewSessionSidebar.agent-host-${provider}) are only registered after the agent host starts and surfaces an AgentInfo, which is asynchronous. This makes them unusable for automation that needs to open a session immediately on startup (e.g. evals). Add stable command ids tied to the umbrella AgentHostCopilot scheme that automation can invoke before the dynamic registration has occurred: - workbench.action.chat.openNewSessionSidebar.agent-host-copilot - workbench.action.chat.openNewSessionEditor.agent-host-copilot Both mirror the existing openNewChatSessionInPlace.agent-host-copilot command body but with fixed sidebar/editor positioning. --- .../electron-browser/chat.contribution.ts | 62 ++++++++++++------- 1 file changed, 41 insertions(+), 21 deletions(-) diff --git a/src/vs/workbench/contrib/chat/electron-browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/electron-browser/chat.contribution.ts index 3620b9a1c7bd7e..82739e0e560a99 100644 --- a/src/vs/workbench/contrib/chat/electron-browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/electron-browser/chat.contribution.ts @@ -17,6 +17,7 @@ import { IContextKeyService } from '../../../../platform/contextkey/common/conte import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js'; import { ILocalGitService } from '../../../../platform/git/common/localGitService.js'; import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; +import { ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; import { registerSharedProcessRemoteService } from '../../../../platform/ipc/electron-browser/services.js'; import { ILogService } from '../../../../platform/log/common/log.js'; import { INativeHostService } from '../../../../platform/native/common/native.js'; @@ -255,29 +256,48 @@ registerWorkbenchContribution2(ChatLifecycleHandler.ID, ChatLifecycleHandler, Wo registerWorkbenchContribution2(AgentHostContribution.ID, AgentHostContribution, WorkbenchPhase.AfterRestored); registerWorkbenchContribution2(AgentHostTerminalContribution.ID, AgentHostTerminalContribution, WorkbenchPhase.AfterRestored); +// Open a new Agent Host session at the given position. Shared by the session +// type picker command and the static sidebar/editor commands below. +async function openNewAgentHostSession(accessor: ServicesAccessor, chatSessionPosition: 'editor' | 'sidebar'): Promise { + const resource = URI.from({ + scheme: AgentSessionProviders.AgentHostCopilot, + path: `/untitled-${generateUuid()}`, + }); + + if (chatSessionPosition === 'editor') { + const editorService = accessor.get(IEditorService); + await editorService.openEditor({ + resource, + options: { + override: ChatEditorInput.EditorID, + pinned: true, + }, + }); + } else { + const viewsService = accessor.get(IViewsService); + const view = await viewsService.openView(ChatViewId) as ChatViewPane; + await view.loadSession(resource); + view.focus(); + } +} + // Register command for opening a new Agent Host session from the session type picker CommandsRegistry.registerCommand( `workbench.action.chat.openNewChatSessionInPlace.${AgentSessionProviders.AgentHostCopilot}`, - async (accessor, chatSessionPosition: string) => { - const viewsService = accessor.get(IViewsService); - const resource = URI.from({ - scheme: AgentSessionProviders.AgentHostCopilot, - path: `/untitled-${generateUuid()}`, - }); + (accessor, chatSessionPosition: string) => + openNewAgentHostSession(accessor, chatSessionPosition === 'editor' ? 'editor' : 'sidebar') +); - if (chatSessionPosition === 'editor') { - const editorService = accessor.get(IEditorService); - await editorService.openEditor({ - resource, - options: { - override: ChatEditorInput.EditorID, - pinned: true, - }, - }); - } else { - const view = await viewsService.openView(ChatViewId) as ChatViewPane; - await view.loadSession(resource); - view.focus(); - } - } +// Static sidebar/editor open commands for the Agent Host umbrella scheme. +// The dynamic per-agent commands (e.g. `agent-host-copilot`) are only +// registered after the agent host starts and surfaces an AgentInfo, which +// is asynchronous. Provide stable command ids that automation (evals) can +// invoke before the dynamic registration has occurred. +CommandsRegistry.registerCommand( + `workbench.action.chat.openNewSessionSidebar.${AgentSessionProviders.AgentHostCopilot}`, + accessor => openNewAgentHostSession(accessor, 'sidebar') +); +CommandsRegistry.registerCommand( + `workbench.action.chat.openNewSessionEditor.${AgentSessionProviders.AgentHostCopilot}`, + accessor => openNewAgentHostSession(accessor, 'editor') ); From 7799ecaed4233a873ee298d59ef35656c2760c9b Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Mon, 4 May 2026 16:39:35 -0400 Subject: [PATCH 2/8] refactor to update agent host session handling --- .../chatSessions/chatSessions.contribution.ts | 2 +- .../electron-browser/chat.contribution.ts | 101 ++++++++++++------ 2 files changed, 71 insertions(+), 32 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.ts b/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.ts index c9ed88f7342470..68e57c4bd9038c 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.ts @@ -1321,7 +1321,7 @@ export type NewChatSessionOpenOptions = { readonly replaceEditor?: boolean; }; -async function openChatSession(accessor: ServicesAccessor, openOptions: NewChatSessionOpenOptions, chatSendOptions?: NewChatSessionSendOptions): Promise { +export async function openChatSession(accessor: ServicesAccessor, openOptions: NewChatSessionOpenOptions, chatSendOptions?: NewChatSessionSendOptions): Promise { const viewsService = accessor.get(IViewsService); const chatService = accessor.get(IChatService); const chatSessionService = accessor.get(IChatSessionsService); diff --git a/src/vs/workbench/contrib/chat/electron-browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/electron-browser/chat.contribution.ts index 82739e0e560a99..a1a03446520a4d 100644 --- a/src/vs/workbench/contrib/chat/electron-browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/electron-browser/chat.contribution.ts @@ -4,11 +4,11 @@ *--------------------------------------------------------------------------------------------*/ import { Disposable } from '../../../../base/common/lifecycle.js'; +import { timeout } from '../../../../base/common/async.js'; import { autorun } from '../../../../base/common/observable.js'; import { resolve } from '../../../../base/common/path.js'; import { isMacintosh } from '../../../../base/common/platform.js'; import { URI } from '../../../../base/common/uri.js'; -import { generateUuid } from '../../../../base/common/uuid.js'; import { ipcRenderer } from '../../../../base/parts/sandbox/electron-browser/globals.js'; import { localize } from '../../../../nls.js'; import { registerAction2 } from '../../../../platform/actions/common/actions.js'; @@ -17,7 +17,7 @@ import { IContextKeyService } from '../../../../platform/contextkey/common/conte import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js'; import { ILocalGitService } from '../../../../platform/git/common/localGitService.js'; import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; -import { ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; +import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; import { registerSharedProcessRemoteService } from '../../../../platform/ipc/electron-browser/services.js'; import { ILogService } from '../../../../platform/log/common/log.js'; import { INativeHostService } from '../../../../platform/native/common/native.js'; @@ -25,21 +25,20 @@ import { IWorkspaceTrustRequestService } from '../../../../platform/workspace/co import { WorkbenchPhase, registerWorkbenchContribution2 } from '../../../common/contributions.js'; import { ViewContainerLocation } from '../../../common/views.js'; import { IChatEntitlementService } from '../../../services/chat/common/chatEntitlementService.js'; -import { IEditorService } from '../../../services/editor/common/editorService.js'; import { INativeWorkbenchEnvironmentService } from '../../../services/environment/electron-browser/environmentService.js'; import { IExtensionService } from '../../../services/extensions/common/extensions.js'; import { IWorkbenchLayoutService } from '../../../services/layout/browser/layoutService.js'; import { ILifecycleService, ShutdownReason } from '../../../services/lifecycle/common/lifecycle.js'; -import { IViewsService } from '../../../services/views/common/viewsService.js'; import { ACTION_ID_NEW_CHAT, CHAT_OPEN_ACTION_ID, IChatViewOpenOptions } from '../browser/actions/chatActions.js'; import { AgentHostContribution } from '../browser/agentSessions/agentHost/agentHostChatContribution.js'; import { AgentHostTerminalContribution } from '../browser/agentSessions/agentHost/agentHostTerminalContribution.js'; import { AgentSessionProviders } from '../browser/agentSessions/agentSessions.js'; import { isSessionInProgressStatus } from '../browser/agentSessions/agentSessionsModel.js'; import { IAgentSessionsService } from '../browser/agentSessions/agentSessionsService.js'; -import { ChatViewId, ChatViewPaneTarget, IChatWidgetService } from '../browser/chat.js'; -import { ChatEditorInput } from '../browser/widgetHosts/editor/chatEditorInput.js'; -import { ChatViewPane } from '../browser/widgetHosts/viewPane/chatViewPane.js'; +import { ChatViewPaneTarget, IChatWidgetService } from '../browser/chat.js'; +import { ChatSessionPosition, openChatSession } from '../browser/chatSessions/chatSessions.contribution.js'; +import { IAgentHostService } from '../../../../platform/agentHost/common/agentService.js'; +import { type AgentInfo, type RootState } from '../../../../platform/agentHost/common/state/sessionState.js'; import { ChatContextKeys } from '../common/actions/chatContextKeys.js'; import { IChatService } from '../common/chatService/chatService.js'; import { ChatModeKind } from '../common/constants.js'; @@ -256,36 +255,76 @@ registerWorkbenchContribution2(ChatLifecycleHandler.ID, ChatLifecycleHandler, Wo registerWorkbenchContribution2(AgentHostContribution.ID, AgentHostContribution, WorkbenchPhase.AfterRestored); registerWorkbenchContribution2(AgentHostTerminalContribution.ID, AgentHostTerminalContribution, WorkbenchPhase.AfterRestored); -// Open a new Agent Host session at the given position. Shared by the session -// type picker command and the static sidebar/editor commands below. -async function openNewAgentHostSession(accessor: ServicesAccessor, chatSessionPosition: 'editor' | 'sidebar'): Promise { - const resource = URI.from({ - scheme: AgentSessionProviders.AgentHostCopilot, - path: `/untitled-${generateUuid()}`, - }); +// How long to wait for the agent host to surface an AgentInfo before giving +// up and falling back to the umbrella scheme. Long enough for normal startup, +// short enough to avoid hanging automation indefinitely if the agent host is +// disabled or fails to start. +const AGENT_HOST_REGISTRATION_TIMEOUT_MS = 30_000; - if (chatSessionPosition === 'editor') { - const editorService = accessor.get(IEditorService); - await editorService.openEditor({ - resource, - options: { - override: ChatEditorInput.EditorID, - pinned: true, - }, - }); - } else { - const viewsService = accessor.get(IViewsService); - const view = await viewsService.openView(ChatViewId) as ChatViewPane; - await view.loadSession(resource); - view.focus(); +function getCopilotAgentInfo(rootState: RootState | Error | undefined): AgentInfo | undefined { + if (!rootState || rootState instanceof Error) { + return undefined; } + // Prefer the canonical `copilotcli` provider if present; fall back to the + // first agent so this also works for forks that use a different provider id. + return rootState.agents.find(a => a.provider === 'copilotcli') ?? rootState.agents[0]; +} + +/** + * Resolve the actual session-content-provider scheme registered by the local + * agent host. The agent host registers chat sessions under + * `agent-host-${agent.provider}` (e.g. `agent-host-copilotcli`) only after it + * surfaces an `AgentInfo` via `rootState`. This is asynchronous, so the static + * `agent-host-copilot` umbrella commands need to wait for that registration + * before opening a session — otherwise we'd build a URI with a scheme that has + * no content provider and fall back to a fresh local chat session. + */ +async function resolveAgentHostSessionType(agentHostService: IAgentHostService): Promise { + const agent = getCopilotAgentInfo(agentHostService.rootState.value); + if (agent) { + return `agent-host-${agent.provider}`; + } + + // Wait for the first non-empty root state, capped by a timeout. + const waitForAgent = new Promise(res => { + const sub = agentHostService.rootState.onDidChange(state => { + const found = getCopilotAgentInfo(state); + if (found) { + sub.dispose(); + res(found); + } + }); + }); + const resolved = await Promise.race([ + waitForAgent, + timeout(AGENT_HOST_REGISTRATION_TIMEOUT_MS).then(() => undefined), + ]); + return resolved ? `agent-host-${resolved.provider}` : AgentSessionProviders.AgentHostCopilot; +} + +// Open a new Agent Host session at the given position. Shared by the session +// type picker command and the static sidebar/editor commands below. +// Delegates to `openChatSession` so the session type picker, context keys, +// and welcome flows all stay in sync with the dynamic per-agent path. +async function openNewAgentHostSession(accessor: ServicesAccessor, position: ChatSessionPosition): Promise { + // Snapshot the services we need synchronously — `accessor` is only valid + // before the first `await`. Use the instantiation service to mint a fresh + // accessor for the downstream `openChatSession` call. + const agentHostService = accessor.get(IAgentHostService); + const instantiationService = accessor.get(IInstantiationService); + const sessionType = await resolveAgentHostSessionType(agentHostService); + return instantiationService.invokeFunction(innerAccessor => openChatSession(innerAccessor, { + type: sessionType, + displayName: sessionType, + position, + })); } // Register command for opening a new Agent Host session from the session type picker CommandsRegistry.registerCommand( `workbench.action.chat.openNewChatSessionInPlace.${AgentSessionProviders.AgentHostCopilot}`, (accessor, chatSessionPosition: string) => - openNewAgentHostSession(accessor, chatSessionPosition === 'editor' ? 'editor' : 'sidebar') + openNewAgentHostSession(accessor, chatSessionPosition === 'editor' ? ChatSessionPosition.Editor : ChatSessionPosition.Sidebar) ); // Static sidebar/editor open commands for the Agent Host umbrella scheme. @@ -295,9 +334,9 @@ CommandsRegistry.registerCommand( // invoke before the dynamic registration has occurred. CommandsRegistry.registerCommand( `workbench.action.chat.openNewSessionSidebar.${AgentSessionProviders.AgentHostCopilot}`, - accessor => openNewAgentHostSession(accessor, 'sidebar') + accessor => openNewAgentHostSession(accessor, ChatSessionPosition.Sidebar) ); CommandsRegistry.registerCommand( `workbench.action.chat.openNewSessionEditor.${AgentSessionProviders.AgentHostCopilot}`, - accessor => openNewAgentHostSession(accessor, 'editor') + accessor => openNewAgentHostSession(accessor, ChatSessionPosition.Editor) ); From e13f730667ec5d07d7968424579bcf1e3e168ba3 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Mon, 4 May 2026 16:51:08 -0400 Subject: [PATCH 3/8] Pre-seed agent host token from GITHUB_OAUTH_TOKEN in scenario automation --- .../platform/agentHost/node/copilot/copilotAgent.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/vs/platform/agentHost/node/copilot/copilotAgent.ts b/src/vs/platform/agentHost/node/copilot/copilotAgent.ts index e9b0aa1c283018..3750f3f4c8f39a 100644 --- a/src/vs/platform/agentHost/node/copilot/copilotAgent.ts +++ b/src/vs/platform/agentHost/node/copilot/copilotAgent.ts @@ -281,6 +281,19 @@ export class CopilotAgent extends Disposable implements IAgent { super(); this._plugins = this._register(this._instantiationService.createInstance(PluginController)); this.onDidCustomizationsChange = this._plugins.onDidChange; + + // Scenario-automation escape hatch (mirrors the Copilot Chat extension's + // `VSCODE_COPILOT_CHAT_TOKEN` / `GITHUB_OAUTH_TOKEN` handling — see + // `extensions/copilot/src/platform/authentication/node/copilotTokenManager.ts`). + // When the eval harness pre-authenticates the host (`IS_SCENARIO_AUTOMATION=1` + // + a `GITHUB_OAUTH_TOKEN` in env), seed our token directly so the renderer + // never needs to drive an interactive GitHub sign-in (which would surface a + // device-code modal and fail the run with `X_BLOCKING_UI_ERROR`). + if (process.env.IS_SCENARIO_AUTOMATION === '1' && process.env.GITHUB_OAUTH_TOKEN) { + this._githubToken = process.env.GITHUB_OAUTH_TOKEN; + this._logService.info('[Copilot] Seeded auth token from GITHUB_OAUTH_TOKEN env var (scenario automation)'); + void this._refreshModels(); + } } protected _createCopilotClient(options: CopilotClientOptions): ICopilotClient { From f6634b88ec8f1ec21a00a68560c215c272ddaf98 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Mon, 4 May 2026 17:03:50 -0400 Subject: [PATCH 4/8] Bypass GitHub auth dialog in scenario automation for agent host --- .../agentSessions/agentHost/agentHostAuth.ts | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostAuth.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostAuth.ts index fafe9633d851e6..9ebbd6e303c535 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostAuth.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostAuth.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { env as processEnv } from '../../../../../../base/common/process.js'; import { URI } from '../../../../../../base/common/uri.js'; import { type ProtectedResourceMetadata } from '../../../../../../platform/agentHost/common/state/protocol/state.js'; import { type AgentInfo } from '../../../../../../platform/agentHost/common/state/sessionState.js'; @@ -160,6 +161,21 @@ export async function authenticateProtectedResources( } } +/** + * Reads `GITHUB_OAUTH_TOKEN` from the host process environment when the + * harness has flagged the run as `IS_SCENARIO_AUTOMATION=1`. Returns + * `undefined` in any environment where `process.env` is unavailable + * (e.g. pure web) or the markers are not set. Mirrors the Copilot Chat + * extension's `isScenarioAutomation` + `GITHUB_OAUTH_TOKEN` handling in + * `extensions/copilot/src/platform/authentication/node/copilotTokenManager.ts`. + */ +function getScenarioAutomationGitHubToken(): string | undefined { + if (processEnv['IS_SCENARIO_AUTOMATION'] === '1' && processEnv['GITHUB_OAUTH_TOKEN']) { + return processEnv['GITHUB_OAUTH_TOKEN']; + } + return undefined; +} + /** * Prompts the user to authenticate one of the provided protected resources and * forwards the resulting token to the agent host connection. @@ -168,6 +184,22 @@ export async function resolveAuthenticationInteractively( protectedResources: readonly ProtectedResourceMetadata[], options: IAgentHostAuthenticationOptions, ): Promise { + // Scenario-automation escape hatch — mirror the Copilot Chat extension's + // behavior of consuming `GITHUB_OAUTH_TOKEN` directly when running under + // `IS_SCENARIO_AUTOMATION=1`. Without this, the eval harness would surface + // a device-code modal here (since no `IAuthenticationService` session has + // been created in the headless run) and trip the harness's + // `X_BLOCKING_UI_ERROR` guard. + const automationToken = getScenarioAutomationGitHubToken(); + if (automationToken) { + for (const resource of protectedResources) { + await options.authenticate({ resource: resource.resource, token: automationToken }); + options.authTokenCache?.updateAndIsChanged(resource.resource, automationToken); + options.logService.info(`${options.logPrefix} Authenticated ${resource.resource} from GITHUB_OAUTH_TOKEN env var (scenario automation)`); + return true; + } + } + for (const resource of protectedResources) { const resourceUri = URI.parse(resource.resource); const token = await resolveTokenForResource( From e3d509ef973e968684651f8bfd12e86032ac56d4 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Mon, 4 May 2026 17:11:14 -0400 Subject: [PATCH 5/8] Hide protected resources when scenario-automation token is seeded --- src/vs/platform/agentHost/node/copilot/copilotAgent.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/vs/platform/agentHost/node/copilot/copilotAgent.ts b/src/vs/platform/agentHost/node/copilot/copilotAgent.ts index 3750f3f4c8f39a..2ead5ca11ebfc3 100644 --- a/src/vs/platform/agentHost/node/copilot/copilotAgent.ts +++ b/src/vs/platform/agentHost/node/copilot/copilotAgent.ts @@ -311,6 +311,15 @@ export class CopilotAgent extends Disposable implements IAgent { } getProtectedResources(): ProtectedResourceMetadata[] { + // In scenario automation we have already seeded `_githubToken` from + // `GITHUB_OAUTH_TOKEN` in the environment, so the renderer must not + // drive an interactive sign-in for this resource (which would surface + // a device-code modal and trip the harness's `X_BLOCKING_UI_ERROR`). + // Hide the resource entirely; the in-process token is sufficient to + // start the SDK client. + if (process.env.IS_SCENARIO_AUTOMATION === '1' && this._githubToken) { + return []; + } return [GITHUB_COPILOT_PROTECTED_RESOURCE]; } From 422eda2a6eba28ebf565d10fb1a0dd571dd0d7b3 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Mon, 4 May 2026 17:29:23 -0400 Subject: [PATCH 6/8] revert auth changes --- .../agentHost/node/copilot/copilotAgent.ts | 22 ------------------- 1 file changed, 22 deletions(-) diff --git a/src/vs/platform/agentHost/node/copilot/copilotAgent.ts b/src/vs/platform/agentHost/node/copilot/copilotAgent.ts index 2ead5ca11ebfc3..e9b0aa1c283018 100644 --- a/src/vs/platform/agentHost/node/copilot/copilotAgent.ts +++ b/src/vs/platform/agentHost/node/copilot/copilotAgent.ts @@ -281,19 +281,6 @@ export class CopilotAgent extends Disposable implements IAgent { super(); this._plugins = this._register(this._instantiationService.createInstance(PluginController)); this.onDidCustomizationsChange = this._plugins.onDidChange; - - // Scenario-automation escape hatch (mirrors the Copilot Chat extension's - // `VSCODE_COPILOT_CHAT_TOKEN` / `GITHUB_OAUTH_TOKEN` handling — see - // `extensions/copilot/src/platform/authentication/node/copilotTokenManager.ts`). - // When the eval harness pre-authenticates the host (`IS_SCENARIO_AUTOMATION=1` - // + a `GITHUB_OAUTH_TOKEN` in env), seed our token directly so the renderer - // never needs to drive an interactive GitHub sign-in (which would surface a - // device-code modal and fail the run with `X_BLOCKING_UI_ERROR`). - if (process.env.IS_SCENARIO_AUTOMATION === '1' && process.env.GITHUB_OAUTH_TOKEN) { - this._githubToken = process.env.GITHUB_OAUTH_TOKEN; - this._logService.info('[Copilot] Seeded auth token from GITHUB_OAUTH_TOKEN env var (scenario automation)'); - void this._refreshModels(); - } } protected _createCopilotClient(options: CopilotClientOptions): ICopilotClient { @@ -311,15 +298,6 @@ export class CopilotAgent extends Disposable implements IAgent { } getProtectedResources(): ProtectedResourceMetadata[] { - // In scenario automation we have already seeded `_githubToken` from - // `GITHUB_OAUTH_TOKEN` in the environment, so the renderer must not - // drive an interactive sign-in for this resource (which would surface - // a device-code modal and trip the harness's `X_BLOCKING_UI_ERROR`). - // Hide the resource entirely; the in-process token is sufficient to - // start the SDK client. - if (process.env.IS_SCENARIO_AUTOMATION === '1' && this._githubToken) { - return []; - } return [GITHUB_COPILOT_PROTECTED_RESOURCE]; } From 51061c7ca2f55bd92ba3aa90d8f9db836c3bcac6 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Mon, 4 May 2026 17:30:09 -0400 Subject: [PATCH 7/8] revert change --- .../agentSessions/agentHost/agentHostAuth.ts | 32 ------------------- 1 file changed, 32 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostAuth.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostAuth.ts index 9ebbd6e303c535..fafe9633d851e6 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostAuth.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostAuth.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { env as processEnv } from '../../../../../../base/common/process.js'; import { URI } from '../../../../../../base/common/uri.js'; import { type ProtectedResourceMetadata } from '../../../../../../platform/agentHost/common/state/protocol/state.js'; import { type AgentInfo } from '../../../../../../platform/agentHost/common/state/sessionState.js'; @@ -161,21 +160,6 @@ export async function authenticateProtectedResources( } } -/** - * Reads `GITHUB_OAUTH_TOKEN` from the host process environment when the - * harness has flagged the run as `IS_SCENARIO_AUTOMATION=1`. Returns - * `undefined` in any environment where `process.env` is unavailable - * (e.g. pure web) or the markers are not set. Mirrors the Copilot Chat - * extension's `isScenarioAutomation` + `GITHUB_OAUTH_TOKEN` handling in - * `extensions/copilot/src/platform/authentication/node/copilotTokenManager.ts`. - */ -function getScenarioAutomationGitHubToken(): string | undefined { - if (processEnv['IS_SCENARIO_AUTOMATION'] === '1' && processEnv['GITHUB_OAUTH_TOKEN']) { - return processEnv['GITHUB_OAUTH_TOKEN']; - } - return undefined; -} - /** * Prompts the user to authenticate one of the provided protected resources and * forwards the resulting token to the agent host connection. @@ -184,22 +168,6 @@ export async function resolveAuthenticationInteractively( protectedResources: readonly ProtectedResourceMetadata[], options: IAgentHostAuthenticationOptions, ): Promise { - // Scenario-automation escape hatch — mirror the Copilot Chat extension's - // behavior of consuming `GITHUB_OAUTH_TOKEN` directly when running under - // `IS_SCENARIO_AUTOMATION=1`. Without this, the eval harness would surface - // a device-code modal here (since no `IAuthenticationService` session has - // been created in the headless run) and trip the harness's - // `X_BLOCKING_UI_ERROR` guard. - const automationToken = getScenarioAutomationGitHubToken(); - if (automationToken) { - for (const resource of protectedResources) { - await options.authenticate({ resource: resource.resource, token: automationToken }); - options.authTokenCache?.updateAndIsChanged(resource.resource, automationToken); - options.logService.info(`${options.logPrefix} Authenticated ${resource.resource} from GITHUB_OAUTH_TOKEN env var (scenario automation)`); - return true; - } - } - for (const resource of protectedResources) { const resourceUri = URI.parse(resource.resource); const token = await resolveTokenForResource( From 9cfb09be6437d1a10421ffd99a897595a0efd576 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Mon, 4 May 2026 17:40:43 -0400 Subject: [PATCH 8/8] Fix listener leak, broken fallback, and wrong-agent fallback in static agent host commands - Fix listener leak: dispose onDidChange subscription on timeout via CancellationTokenSource, preventing accumulated leaked listeners on repeated calls - Fix broken timeout fallback: throw an error instead of silently falling back to AgentHostCopilot umbrella scheme which has no registered content provider - Fix wrong-agent fallback: remove `?? rootState.agents[0]` so we only match the copilotcli provider, not an unrelated agent - Fix display name: use getAgentSessionProviderName() instead of raw scheme string for editor tab titles Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../electron-browser/chat.contribution.ts | 34 +++++++++++++------ 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/src/vs/workbench/contrib/chat/electron-browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/electron-browser/chat.contribution.ts index a1a03446520a4d..ce000e508240b8 100644 --- a/src/vs/workbench/contrib/chat/electron-browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/electron-browser/chat.contribution.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Disposable } from '../../../../base/common/lifecycle.js'; +import { CancellationTokenSource } from '../../../../base/common/cancellation.js'; import { timeout } from '../../../../base/common/async.js'; import { autorun } from '../../../../base/common/observable.js'; import { resolve } from '../../../../base/common/path.js'; @@ -32,7 +33,7 @@ import { ILifecycleService, ShutdownReason } from '../../../services/lifecycle/c import { ACTION_ID_NEW_CHAT, CHAT_OPEN_ACTION_ID, IChatViewOpenOptions } from '../browser/actions/chatActions.js'; import { AgentHostContribution } from '../browser/agentSessions/agentHost/agentHostChatContribution.js'; import { AgentHostTerminalContribution } from '../browser/agentSessions/agentHost/agentHostTerminalContribution.js'; -import { AgentSessionProviders } from '../browser/agentSessions/agentSessions.js'; +import { AgentSessionProviders, getAgentSessionProviderName } from '../browser/agentSessions/agentSessions.js'; import { isSessionInProgressStatus } from '../browser/agentSessions/agentSessionsModel.js'; import { IAgentSessionsService } from '../browser/agentSessions/agentSessionsService.js'; import { ChatViewPaneTarget, IChatWidgetService } from '../browser/chat.js'; @@ -255,19 +256,17 @@ registerWorkbenchContribution2(ChatLifecycleHandler.ID, ChatLifecycleHandler, Wo registerWorkbenchContribution2(AgentHostContribution.ID, AgentHostContribution, WorkbenchPhase.AfterRestored); registerWorkbenchContribution2(AgentHostTerminalContribution.ID, AgentHostTerminalContribution, WorkbenchPhase.AfterRestored); -// How long to wait for the agent host to surface an AgentInfo before giving -// up and falling back to the umbrella scheme. Long enough for normal startup, -// short enough to avoid hanging automation indefinitely if the agent host is -// disabled or fails to start. +// How long to wait for the agent host to surface an AgentInfo before +// throwing an error. Long enough for normal startup, short enough to avoid +// hanging automation indefinitely if the agent host is disabled or fails +// to start. const AGENT_HOST_REGISTRATION_TIMEOUT_MS = 30_000; function getCopilotAgentInfo(rootState: RootState | Error | undefined): AgentInfo | undefined { if (!rootState || rootState instanceof Error) { return undefined; } - // Prefer the canonical `copilotcli` provider if present; fall back to the - // first agent so this also works for forks that use a different provider id. - return rootState.agents.find(a => a.provider === 'copilotcli') ?? rootState.agents[0]; + return rootState.agents.find(a => a.provider === 'copilotcli'); } /** @@ -286,6 +285,8 @@ async function resolveAgentHostSessionType(agentHostService: IAgentHostService): } // Wait for the first non-empty root state, capped by a timeout. + // The subscription must be disposed on both success and timeout to avoid leaks. + const cts = new CancellationTokenSource(); const waitForAgent = new Promise(res => { const sub = agentHostService.rootState.onDidChange(state => { const found = getCopilotAgentInfo(state); @@ -294,12 +295,23 @@ async function resolveAgentHostSessionType(agentHostService: IAgentHostService): res(found); } }); + cts.token.onCancellationRequested(() => { + sub.dispose(); + res(undefined); + }); }); const resolved = await Promise.race([ waitForAgent, - timeout(AGENT_HOST_REGISTRATION_TIMEOUT_MS).then(() => undefined), + timeout(AGENT_HOST_REGISTRATION_TIMEOUT_MS).then(() => { + cts.cancel(); + cts.dispose(); + return undefined; + }), ]); - return resolved ? `agent-host-${resolved.provider}` : AgentSessionProviders.AgentHostCopilot; + if (!resolved) { + throw new Error('Agent host did not register a copilotcli agent within the timeout period. Ensure the agent host is enabled and running.'); + } + return `agent-host-${resolved.provider}`; } // Open a new Agent Host session at the given position. Shared by the session @@ -315,7 +327,7 @@ async function openNewAgentHostSession(accessor: ServicesAccessor, position: Cha const sessionType = await resolveAgentHostSessionType(agentHostService); return instantiationService.invokeFunction(innerAccessor => openChatSession(innerAccessor, { type: sessionType, - displayName: sessionType, + displayName: getAgentSessionProviderName(sessionType), position, })); }