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 4a4f1ab768ed9a..faf35588d1af6e 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(); 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 a71ea05007d6e5..4dc3f87ba0067c 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 c89f0ee2f6f017..e7444a3c6ada3e 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 d3c7f201ffdaed..5a41c3fa412376 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 ffaea1eb0df2ff..1ca5c950660bc0 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 }], + }], + }); + }); +});