From 2ae7b4ca853095e9781b4fb380052086512f949b Mon Sep 17 00:00:00 2001 From: Alex Dima Date: Mon, 7 Sep 2026 16:31:05 +0200 Subject: [PATCH 1/2] chat: reveal local Agent Host session in Agents Window When enabled by the experimental setting, make the title bar Open in Agents action reveal the current persisted local Agent Host session. Preserve the new-session fallback for all other cases. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../chat/browser/chat.shared.contribution.ts | 7 ++ .../contrib/chat/common/constants.ts | 1 + .../agentSessions/agentSessionsActions.ts | 16 +++- .../agentSessionsActions.test.ts | 80 ++++++++++++++++++- 4 files changed, 101 insertions(+), 3 deletions(-) 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 a71ea05007d6e..4dc3f87ba0067 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.shared.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.shared.contribution.ts @@ -2389,6 +2389,13 @@ configurationRegistry.registerConfiguration({ description: nls.localize('chat.titleBar.openInAgentsWindow.enabled', "Controls whether the Open in Agents Window button is shown in the title bar."), default: true, }, + [ChatConfiguration.OpenInAgentsWindowRevealCurrentSession]: { + type: 'boolean', + description: nls.localize('chat.experimental.openInAgentsWindow.revealCurrentSession', "Controls whether Open in Agents Window reveals the current local Agent Host session instead of opening a new session."), + default: false, + tags: ['experimental'], + experiment: { mode: 'auto' }, + }, 'chat.approvedAccountOrganizations': { type: 'array', items: { type: 'string' }, diff --git a/src/vs/workbench/contrib/chat/common/constants.ts b/src/vs/workbench/contrib/chat/common/constants.ts index c89f0ee2f6f01..e7444a3c6ada3 100644 --- a/src/vs/workbench/contrib/chat/common/constants.ts +++ b/src/vs/workbench/contrib/chat/common/constants.ts @@ -103,6 +103,7 @@ export enum ChatConfiguration { GrowthNotificationEnabled = 'chat.growthNotification.enabled', TitleBarSignInEnabled = 'chat.titleBar.signIn.enabled', TitleBarOpenInAgentsWindowEnabled = 'chat.titleBar.openInAgentsWindow.enabled', + OpenInAgentsWindowRevealCurrentSession = 'chat.experimental.openInAgentsWindow.revealCurrentSession', ChatCustomizationsStructuredPreviewEnabled = 'chat.customizations.structuredPreview.enabled', ChatCustomizationsPromptMigrationEnabled = 'chat.customizations.promptMigration.enabled', diff --git a/src/vs/workbench/contrib/chat/electron-browser/agentSessions/agentSessionsActions.ts b/src/vs/workbench/contrib/chat/electron-browser/agentSessions/agentSessionsActions.ts index d3c7f201ffdae..5a41c3fa41237 100644 --- a/src/vs/workbench/contrib/chat/electron-browser/agentSessions/agentSessionsActions.ts +++ b/src/vs/workbench/contrib/chat/electron-browser/agentSessions/agentSessionsActions.ts @@ -30,7 +30,7 @@ import { IWorkbenchContribution } from '../../../../common/contributions.js'; import { CHAT_CATEGORY } from '../../browser/actions/chatActions.js'; import { IChatWidgetService } from '../../browser/chat.js'; import { ChatContextKeys } from '../../common/actions/chatContextKeys.js'; -import { SessionType } from '../../common/chatSessionsService.js'; +import { isLocalAgentHostTarget, SessionType } from '../../common/chatSessionsService.js'; import { IChatViewTitleActionContext } from '../../common/actions/chatActions.js'; import { getChatSessionType, isUntitledChatSession } from '../../common/model/chatUri.js'; import { ChatInputNotificationActionKind, ChatInputNotificationSeverity, IChatInputNotificationService } from '../../browser/widget/input/chatInputNotificationService.js'; @@ -115,6 +115,20 @@ export class OpenWorkspaceInAgentsWindowTitleBarAction extends Action2 { } async run(accessor: ServicesAccessor): Promise { + const configurationService = accessor.get(IConfigurationService); + const sessionResource = accessor.get(IChatWidgetService).lastFocusedWidget?.viewModel?.sessionResource; + if (configurationService.getValue(ChatConfiguration.OpenInAgentsWindowRevealCurrentSession) === true + && sessionResource + && !isUntitledChatSession(sessionResource) + && isLocalAgentHostTarget(getChatSessionType(sessionResource))) { + await accessor.get(ICommandService).executeCommand( + OpenChatSessionInAgentsWindowAction.ID, + { agentsWindowOpenSource: AgentsWindowOpenSource.TitleBar }, + sessionResource, + ); + return; + } + await accessor.get(ICommandService).executeCommand(OPEN_WORKSPACE_IN_AGENTS_WINDOW_COMMAND_ID, { source: AgentsWindowOpenSource.TitleBar }); } } diff --git a/src/vs/workbench/contrib/chat/test/electron-browser/agentSessionsActions.test.ts b/src/vs/workbench/contrib/chat/test/electron-browser/agentSessionsActions.test.ts index ffaea1eb0df2f..1ca5c950660bc 100644 --- a/src/vs/workbench/contrib/chat/test/electron-browser/agentSessionsActions.test.ts +++ b/src/vs/workbench/contrib/chat/test/electron-browser/agentSessionsActions.test.ts @@ -8,13 +8,28 @@ import { encodeHex, VSBuffer } from '../../../../../base/common/buffer.js'; import { DisposableStore } from '../../../../../base/common/lifecycle.js'; import { Schemas } from '../../../../../base/common/network.js'; import { URI } from '../../../../../base/common/uri.js'; -import { upcastPartial } from '../../../../../base/test/common/mock.js'; +import { mock, upcastPartial } from '../../../../../base/test/common/mock.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; +import { ICommandService } from '../../../../../platform/commands/common/commands.js'; +import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; +import { TestConfigurationService } from '../../../../../platform/configuration/test/common/testConfigurationService.js'; import { TestInstantiationService } from '../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; import { INativeHostService, IOpenAgentsWindowOptions } from '../../../../../platform/native/common/native.js'; import { AgentsWindowOpenSource } from '../../../../../platform/window/common/window.js'; import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js'; -import { OpenWorkspaceInAgentsWindowAction } from '../../electron-browser/agentSessions/agentSessionsActions.js'; +import { IChatWidget, IChatWidgetService } from '../../browser/chat.js'; +import { ChatConfiguration, OPEN_WORKSPACE_IN_AGENTS_WINDOW_COMMAND_ID } from '../../common/constants.js'; +import { IChatViewModel } from '../../common/model/chatViewModel.js'; +import { OpenChatSessionInAgentsWindowAction, OpenWorkspaceInAgentsWindowAction, OpenWorkspaceInAgentsWindowTitleBarAction } from '../../electron-browser/agentSessions/agentSessionsActions.js'; + +class TestCommandService extends mock() { + readonly calls: { readonly commandId: string; readonly args: readonly unknown[] }[] = []; + + override async executeCommand(commandId: string, ...args: unknown[]): Promise { + this.calls.push({ commandId, args }); + return undefined; + } +} suite('OpenWorkspaceInAgentsWindowAction', () => { const disposables = ensureNoDisposablesAreLeakedInTestSuite(); @@ -64,3 +79,64 @@ suite('OpenWorkspaceInAgentsWindowAction', () => { }]); }); }); + +suite('OpenWorkspaceInAgentsWindowTitleBarAction', () => { + const disposables = ensureNoDisposablesAreLeakedInTestSuite(); + + async function run(sessionResource: URI | undefined, revealCurrentSession = true) { + const instantiationService = disposables.add(new TestInstantiationService()); + const commandService = new TestCommandService(); + const configurationService = new TestConfigurationService({ + [ChatConfiguration.OpenInAgentsWindowRevealCurrentSession]: revealCurrentSession, + }); + instantiationService.stub(ICommandService, commandService); + instantiationService.stub(IConfigurationService, configurationService); + instantiationService.stub(IChatWidgetService, upcastPartial({ + lastFocusedWidget: sessionResource ? upcastPartial({ + viewModel: upcastPartial({ sessionResource }), + }) : undefined, + })); + + const action = new OpenWorkspaceInAgentsWindowTitleBarAction(); + await instantiationService.invokeFunction(accessor => action.run(accessor)); + return commandService.calls; + } + + test('reveals a persisted local Agent Host session and otherwise opens a workspace draft', async () => { + const localSession = URI.from({ scheme: 'agent-host-claude', path: '/session' }); + + assert.deepStrictEqual({ + localSession: await run(localSession), + disabled: await run(localSession, false), + untitledLocalSession: await run(URI.from({ scheme: 'agent-host-claude', path: '/untitled-session' })), + remoteSession: await run(URI.from({ scheme: 'remote-host-claude', path: '/session' })), + regularSession: await run(URI.from({ scheme: 'vscode-chat-session', path: '/session' })), + noSession: await run(undefined), + }, { + localSession: [{ + commandId: OpenChatSessionInAgentsWindowAction.ID, + args: [{ agentsWindowOpenSource: AgentsWindowOpenSource.TitleBar }, localSession], + }], + disabled: [{ + commandId: OPEN_WORKSPACE_IN_AGENTS_WINDOW_COMMAND_ID, + args: [{ source: AgentsWindowOpenSource.TitleBar }], + }], + untitledLocalSession: [{ + commandId: OPEN_WORKSPACE_IN_AGENTS_WINDOW_COMMAND_ID, + args: [{ source: AgentsWindowOpenSource.TitleBar }], + }], + remoteSession: [{ + commandId: OPEN_WORKSPACE_IN_AGENTS_WINDOW_COMMAND_ID, + args: [{ source: AgentsWindowOpenSource.TitleBar }], + }], + regularSession: [{ + commandId: OPEN_WORKSPACE_IN_AGENTS_WINDOW_COMMAND_ID, + args: [{ source: AgentsWindowOpenSource.TitleBar }], + }], + noSession: [{ + commandId: OPEN_WORKSPACE_IN_AGENTS_WINDOW_COMMAND_ID, + args: [{ source: AgentsWindowOpenSource.TitleBar }], + }], + }); + }); +}); From 7261473875f6229f13630235ae69e733d4f3c482 Mon Sep 17 00:00:00 2001 From: Alex Dima Date: Mon, 7 Sep 2026 19:18:37 +0200 Subject: [PATCH 2/2] sessions: avoid delayed Agents handoff Start resolving an existing-session handoff as soon as initial UI restore begins instead of waiting for the deferred Eventually lifecycle phase. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../contrib/chat/electron-browser/chat.contribution.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/vs/sessions/contrib/chat/electron-browser/chat.contribution.ts b/src/vs/sessions/contrib/chat/electron-browser/chat.contribution.ts index 4a4f1ab768ed9..faf35588d1af6 100644 --- a/src/vs/sessions/contrib/chat/electron-browser/chat.contribution.ts +++ b/src/vs/sessions/contrib/chat/electron-browser/chat.contribution.ts @@ -134,10 +134,10 @@ class SelectAgentsFolderContribution extends Disposable implements IWorkbenchCon private async openExistingSession(sessionResource: URI): Promise { this.logService.info(`[AgentsHandoff] openExistingSession: target=${sessionResource.toString()}`); - // Wait for the workbench to be ready so the session list / providers - // have populated. Otherwise openSession can't find the session. - await this.lifecycleService.when(LifecyclePhase.Eventually); - this.logService.info('[AgentsHandoff] reached LifecyclePhase.Eventually'); + // Wait until initial restore has started so opening the target can cancel it, + // without delaying the handoff until the intentionally deferred Eventually phase. + await this.lifecycleService.when(LifecyclePhase.Restored); + this.logService.info('[AgentsHandoff] reached LifecyclePhase.Restored'); // Fast path — already on the target session. const current = this.sessionsService.activeSession.get();