From 45d35e4cc47ef759f7a0bbbb1b6e33802530f3c7 Mon Sep 17 00:00:00 2001 From: Christof Marti Date: Thu, 3 Sep 2026 15:06:40 +0200 Subject: [PATCH 1/8] Open Dev Container workspaces in Agents Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/vs/platform/native/common/native.ts | 1 + .../electron-main/nativeHostMainService.ts | 2 +- .../platform/windows/electron-main/windows.ts | 2 +- .../electron-main/windowsMainService.ts | 4 +- .../common/agentHostSessionsProvider.ts | 2 + .../chat/browser/agentsWindowOpenIntent.ts | 33 ++++++ .../electron-browser/chat.contribution.ts | 36 ++++-- .../browser/agentsWindowOpenIntent.test.ts | 106 ++++++++++++++++++ .../browser/localAgentHostSessionsProvider.ts | 22 ++++ .../localAgentHostSessionsProvider.test.ts | 88 +++++++++++++++ .../sessions/browser/sessionsService.ts | 5 + .../browser/sessionsManagementService.test.ts | 51 +++++++++ .../agentSessions/agentSessionsActions.ts | 29 ++++- .../agentSessionsActions.test.ts | 69 ++++++++++++ 14 files changed, 433 insertions(+), 17 deletions(-) create mode 100644 src/vs/sessions/contrib/chat/browser/agentsWindowOpenIntent.ts create mode 100644 src/vs/sessions/contrib/chat/test/browser/agentsWindowOpenIntent.test.ts create mode 100644 src/vs/workbench/contrib/chat/test/electron-browser/agentSessionsActions.test.ts diff --git a/src/vs/platform/native/common/native.ts b/src/vs/platform/native/common/native.ts index 1c62eb7cfef77d..f9edffda183bff 100644 --- a/src/vs/platform/native/common/native.ts +++ b/src/vs/platform/native/common/native.ts @@ -84,6 +84,7 @@ export interface IOpenAgentsWindowOptions { readonly folderUri?: UriComponents; readonly sessionResource?: UriComponents; readonly source?: AgentsWindowOpenSource; + readonly preferDevContainer?: boolean; } export interface ICPUProperties { diff --git a/src/vs/platform/native/electron-main/nativeHostMainService.ts b/src/vs/platform/native/electron-main/nativeHostMainService.ts index b1fb89e25a3acb..ee9391c77bfd92 100644 --- a/src/vs/platform/native/electron-main/nativeHostMainService.ts +++ b/src/vs/platform/native/electron-main/nativeHostMainService.ts @@ -319,7 +319,7 @@ export class NativeHostMainService extends Disposable implements INativeHostMain context: OpenContext.API, contextWindowId: windowId, cli: this.environmentMainService.args, - }, options?.folderUri ? URI.revive(options.folderUri) : undefined, options?.sessionResource ? URI.revive(options.sessionResource) : undefined, options?.source); + }, options?.folderUri ? URI.revive(options.folderUri) : undefined, options?.sessionResource ? URI.revive(options.sessionResource) : undefined, options?.source, options?.preferDevContainer); if (windows.length > 0) { windows[0].focus(); } diff --git a/src/vs/platform/windows/electron-main/windows.ts b/src/vs/platform/windows/electron-main/windows.ts index 7c4f02efdfa374..20ed11a74000e4 100644 --- a/src/vs/platform/windows/electron-main/windows.ts +++ b/src/vs/platform/windows/electron-main/windows.ts @@ -41,7 +41,7 @@ export interface IWindowsMainService { openExtensionDevelopmentHostWindow(extensionDevelopmentPath: string[], openConfig: IOpenConfiguration): Promise; openExistingWindow(window: ICodeWindow, openConfig: IOpenConfiguration): void; - openAgentsWindow(openConfig: IOpenConfiguration, folderUri?: URI, sessionResource?: URI, source?: AgentsWindowOpenSource): Promise; + openAgentsWindow(openConfig: IOpenConfiguration, folderUri?: URI, sessionResource?: URI, source?: AgentsWindowOpenSource, preferDevContainer?: boolean): Promise; sendToFocused(channel: string, ...args: unknown[]): void; sendToOpeningWindow(channel: string, ...args: unknown[]): void; diff --git a/src/vs/platform/windows/electron-main/windowsMainService.ts b/src/vs/platform/windows/electron-main/windowsMainService.ts index d8428b529b26fa..3e6957e60d2604 100644 --- a/src/vs/platform/windows/electron-main/windowsMainService.ts +++ b/src/vs/platform/windows/electron-main/windowsMainService.ts @@ -292,7 +292,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic this.handleChatRequest(openConfig, [window]); } - async openAgentsWindow(openConfig: IOpenConfiguration, folderUri?: URI, sessionResource?: URI, source?: AgentsWindowOpenSource): Promise { + async openAgentsWindow(openConfig: IOpenConfiguration, folderUri?: URI, sessionResource?: URI, source?: AgentsWindowOpenSource, preferDevContainer?: boolean): Promise { this.logService.trace('windowsManager#openAgentsWindow'); // Open in a new browser window with the agent sessions workspace @@ -304,7 +304,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic // folder-resolve. if (windows.length > 0) { const openSource = source ?? (openConfig.cli.agents ? AgentsWindowOpenSource.CommandLine : AgentsWindowOpenSource.Unknown); - windows[0].sendWhenReady('vscode:selectAgentsFolder', CancellationToken.None, folderUri?.toJSON(), sessionResource?.toJSON(), openSource); + windows[0].sendWhenReady('vscode:selectAgentsFolder', CancellationToken.None, folderUri?.toJSON(), sessionResource?.toJSON(), openSource, preferDevContainer); } return windows; diff --git a/src/vs/sessions/common/agentHostSessionsProvider.ts b/src/vs/sessions/common/agentHostSessionsProvider.ts index 5a47d165a3349c..295a8136bc8b37 100644 --- a/src/vs/sessions/common/agentHostSessionsProvider.ts +++ b/src/vs/sessions/common/agentHostSessionsProvider.ts @@ -171,6 +171,8 @@ export interface IAgentHostSessionsProvider extends ISessionsProvider { isDevContainerEnabled?(sessionId: string): boolean; /** Set whether this draft should run on a Dev Container Agent Host. */ setDevContainerEnabled?(sessionId: string, enabled: boolean): void; + /** Enable Dev Container execution once availability resolves for this draft. */ + preferDevContainer?(sessionId: string): void; // -- Dynamic Session Config -- diff --git a/src/vs/sessions/contrib/chat/browser/agentsWindowOpenIntent.ts b/src/vs/sessions/contrib/chat/browser/agentsWindowOpenIntent.ts new file mode 100644 index 00000000000000..5d7a33a2e2dc63 --- /dev/null +++ b/src/vs/sessions/contrib/chat/browser/agentsWindowOpenIntent.ts @@ -0,0 +1,33 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { URI } from '../../../../base/common/uri.js'; +import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; +import { isAgentHostProvider } from '../../../common/agentHostSessionsProvider.js'; +import { DevContainerAgentHostEnabledSettingId } from '../../../common/devContainerAgentHostService.js'; +import { ISessionsService } from '../../../services/sessions/browser/sessionsService.js'; +import { ISessionsProvidersService } from '../../../services/sessions/browser/sessionsProvidersService.js'; + +export function shouldPreferDevContainer(requested: unknown, configurationService: IConfigurationService): boolean { + return requested === true + && configurationService.getValue(DevContainerAgentHostEnabledSettingId) === true; +} + +export async function openNewSessionWithDevContainerPreference( + folderUri: URI, + providerId: string, + sessionsService: ISessionsService, + sessionsProvidersService: ISessionsProvidersService, +): Promise { + const result = await sessionsService.openNewSession({ folderUri, providerId, cancelRestore: true }); + const session = result.session; + if (!session) { + return; + } + const provider = sessionsProvidersService.getProvider(session.providerId); + if (provider && isAgentHostProvider(provider)) { + provider.preferDevContainer?.(session.sessionId); + } +} 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 e8371d55cece66..b77f969fe6b691 100644 --- a/src/vs/sessions/contrib/chat/electron-browser/chat.contribution.ts +++ b/src/vs/sessions/contrib/chat/electron-browser/chat.contribution.ts @@ -8,6 +8,7 @@ import { URI, UriComponents } from '../../../../base/common/uri.js'; import { Disposable, DisposableStore, MutableDisposable } from '../../../../base/common/lifecycle.js'; import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; import { IAgentHostByokLmHandler } from '../../../../platform/agentHost/common/agentHostByokLm.js'; +import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { AgentHostByokLmHandler } from '../../../../workbench/contrib/chat/browser/agentSessions/agentHost/agentHostByokLmHandler.js'; import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../../workbench/common/contributions.js'; import { ISessionsManagementService } from '../../../services/sessions/common/sessionsManagement.js'; @@ -27,6 +28,7 @@ import { ITelemetryService } from '../../../../platform/telemetry/common/telemet import { TOTAL_SESSIONS_KEY } from '../../sessions/browser/sessionsLifecycleTracker.js'; import { ISessionsWindowOpenViewState, SessionsWindowOpenTelemetry, SessionsWindowSessionStartTelemetry } from '../../sessions/browser/sessionsWindowOpenTelemetry.js'; import { INewSessionComposerService, NewSessionWorkspacePreselectionSource } from '../browser/newSessionComposerService.js'; +import { openNewSessionWithDevContainerPreference, shouldPreferDevContainer } from '../browser/agentsWindowOpenIntent.js'; class SelectAgentsFolderContribution extends Disposable implements IWorkbenchContribution { @@ -46,16 +48,18 @@ class SelectAgentsFolderContribution extends Disposable implements IWorkbenchCon @IStorageService private readonly storageService: IStorageService, @ITelemetryService private readonly telemetryService: ITelemetryService, @INewSessionComposerService private readonly newSessionComposerService: INewSessionComposerService, + @IConfigurationService private readonly configurationService: IConfigurationService, ) { super(); const handleSelectAgentsFolder = (_: unknown, ...args: unknown[]) => { const folderUri = args[0] ? URI.revive(args[0] as UriComponents) : undefined; const sessionResource = args[1] ? URI.revive(args[1] as UriComponents) : undefined; const source = isAgentsWindowOpenSource(args[2]) ? args[2] : AgentsWindowOpenSource.Unknown; + const preferDevContainer = shouldPreferDevContainer(args[3], this.configurationService); this.logService.info(`[AgentsHandoff] IPC received: folderUri=${folderUri?.toString() ?? '(none)'} sessionResource=${sessionResource?.toString() ?? '(none)'}`); this._startWindowOpenTelemetry(source); - this._handleOpenIntentAndCaptureInitialState(folderUri, sessionResource) + this._handleOpenIntentAndCaptureInitialState(folderUri, sessionResource, preferDevContainer) .catch(err => this.logService.error('[AgentsHandoff] handleOpenIntent failed', err)); }; ipcRenderer.on('vscode:selectAgentsFolder', handleSelectAgentsFolder); @@ -87,9 +91,9 @@ class SelectAgentsFolderContribution extends Disposable implements IWorkbenchCon this._windowOpenTelemetry.value?.captureInitialViewState(); } - private async _handleOpenIntentAndCaptureInitialState(folderUri: URI | undefined, sessionResource: URI | undefined): Promise { + private async _handleOpenIntentAndCaptureInitialState(folderUri: URI | undefined, sessionResource: URI | undefined, preferDevContainer: boolean): Promise { try { - await this.handleOpenIntent(folderUri, sessionResource); + await this.handleOpenIntent(folderUri, sessionResource, preferDevContainer); } finally { await this._captureInitialWindowViewState(); } @@ -114,7 +118,7 @@ class SelectAgentsFolderContribution extends Disposable implements IWorkbenchCon }; } - private async handleOpenIntent(folderUri: URI | undefined, sessionResource: URI | undefined): Promise { + private async handleOpenIntent(folderUri: URI | undefined, sessionResource: URI | undefined, preferDevContainer: boolean): Promise { // Opening an existing session establishes its own workspace context, so // the folder selection is only needed for the folder-only handoff (no // session to restore). @@ -123,7 +127,7 @@ class SelectAgentsFolderContribution extends Disposable implements IWorkbenchCon return; } if (folderUri) { - await this.selectFolder(folderUri); + await this.selectFolder(folderUri, preferDevContainer); } } @@ -186,11 +190,11 @@ class SelectAgentsFolderContribution extends Disposable implements IWorkbenchCon }); } - private async selectFolder(folderUri: URI): Promise { + private async selectFolder(folderUri: URI, preferDevContainer: boolean): Promise { // Wait for the welcome/setup flow to complete before selecting the folder await this.sessionsSetUpService.whenWelcomeDone(); - this.sessionsService.openNewSession(); + await this.sessionsService.openNewSession({ cancelRestore: true }); // Tell the sessions list this folder is the open-window source folder // so it ranks the matching folder section first. Get the view if it @@ -198,30 +202,42 @@ class SelectAgentsFolderContribution extends Disposable implements IWorkbenchCon const sessionsView = this.viewsService.getViewWithId(SessionsListViewId); sessionsView?.sessionsControl?.setOpenWindowSourceFolder(folderUri); - if (this.tryResolveAndSelect(folderUri)) { + if (this.tryResolveAndSelect(folderUri, preferDevContainer)) { return; } // Provider not registered yet — wait for it, but give up at Eventually phase const disposable = this.sessionsProvidersService.onDidChangeProviders(() => { - if (this.tryResolveAndSelect(folderUri)) { + if (this.tryResolveAndSelect(folderUri, preferDevContainer)) { disposable.dispose(); } }); this.lifecycleService.when(LifecyclePhase.Eventually).then(() => disposable.dispose()); } - private tryResolveAndSelect(folderUri: URI): boolean { + private tryResolveAndSelect(folderUri: URI, preferDevContainer: boolean): boolean { const resolved = this.sessionsManagementService.resolveWorkspace(folderUri); if (!resolved) { return false; } + if (preferDevContainer) { + void this.openNewDevContainerSession(folderUri, resolved.providerId); + return true; + } const activeSession = this.sessionsService.activeSession.get(); if (activeSession === undefined || activeSession.status.get() === SessionStatus.Untitled) { this.sessionsPartService.getSessionView(activeSession?.sessionId)?.selectWorkspace(folderUri, resolved.providerId); } return true; } + + private async openNewDevContainerSession(folderUri: URI, providerId: string): Promise { + try { + await openNewSessionWithDevContainerPreference(folderUri, providerId, this.sessionsService, this.sessionsProvidersService); + } catch (error) { + this.logService.error('[AgentsHandoff] Failed to select Dev Container workspace', error); + } + } } registerWorkbenchContribution2(SelectAgentsFolderContribution.ID, SelectAgentsFolderContribution, WorkbenchPhase.BlockStartup); diff --git a/src/vs/sessions/contrib/chat/test/browser/agentsWindowOpenIntent.test.ts b/src/vs/sessions/contrib/chat/test/browser/agentsWindowOpenIntent.test.ts new file mode 100644 index 00000000000000..84fea0272f8ad6 --- /dev/null +++ b/src/vs/sessions/contrib/chat/test/browser/agentsWindowOpenIntent.test.ts @@ -0,0 +1,106 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import assert from 'assert'; +import { URI } from '../../../../../base/common/uri.js'; +import { mock, upcastPartial } from '../../../../../base/test/common/mock.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; +import { TestConfigurationService } from '../../../../../platform/configuration/test/common/testConfigurationService.js'; +import { LOCAL_AGENT_HOST_PROVIDER_ID, IAgentHostSessionsProvider } from '../../../../common/agentHostSessionsProvider.js'; +import { DevContainerAgentHostEnabledSettingId } from '../../../../common/devContainerAgentHostService.js'; +import { ISession } from '../../../../services/sessions/common/session.js'; +import { ISessionsProvider } from '../../../../services/sessions/common/sessionsProvider.js'; +import { ISessionsService, IOpenNewSessionOptions } from '../../../../services/sessions/browser/sessionsService.js'; +import { ISessionsProvidersService } from '../../../../services/sessions/browser/sessionsProvidersService.js'; +import { openNewSessionWithDevContainerPreference, shouldPreferDevContainer } from '../../browser/agentsWindowOpenIntent.js'; + +suite('Agents Window open intent', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + + function providersService(provider: ISessionsProvider): ISessionsProvidersService { + return new class extends mock() { + override getProvider(providerId: string): T | undefined { + return providerId === provider.id ? provider as T : undefined; + } + }(); + } + + test('gates the Dev Container preference on the Agents Window setting', () => { + const configurationService = (enabled: boolean) => new TestConfigurationService({ + [DevContainerAgentHostEnabledSettingId]: enabled, + }); + + assert.deepStrictEqual({ + requestedAndEnabled: shouldPreferDevContainer(true, configurationService(true)), + requestedAndDisabled: shouldPreferDevContainer(true, configurationService(false)), + notRequestedAndEnabled: shouldPreferDevContainer(false, configurationService(true)), + invalidRequestAndEnabled: shouldPreferDevContainer('true', configurationService(true)), + }, { + requestedAndEnabled: true, + requestedAndDisabled: false, + notRequestedAndEnabled: false, + invalidRequestAndEnabled: false, + }); + }); + + test('opens the local folder and applies the Dev Container preference to its draft', async () => { + const folderUri = URI.file('/workspace'); + const session = upcastPartial({ + sessionId: 'new-session', + providerId: LOCAL_AGENT_HOST_PROVIDER_ID, + }); + let openOptions: IOpenNewSessionOptions | undefined; + const sessionsService = upcastPartial({ + openNewSession: async options => { + openOptions = options; + return { session, trustDeclined: false }; + }, + }); + const preferred: string[] = []; + const provider = upcastPartial({ + id: LOCAL_AGENT_HOST_PROVIDER_ID, + preferDevContainer: sessionId => preferred.push(sessionId), + }); + const sessionsProvidersService = providersService(provider); + + await openNewSessionWithDevContainerPreference(folderUri, LOCAL_AGENT_HOST_PROVIDER_ID, sessionsService, sessionsProvidersService); + + assert.deepStrictEqual({ + folderUri: openOptions?.folderUri?.toString(), + providerId: openOptions?.providerId, + cancelRestore: openOptions?.cancelRestore, + preferred, + }, { + folderUri: folderUri.toString(), + providerId: LOCAL_AGENT_HOST_PROVIDER_ID, + cancelRestore: true, + preferred: ['new-session'], + }); + }); + + test('does not apply the preference without a local Agent Host draft', async () => { + const preferred: string[] = []; + const provider = upcastPartial({ + id: LOCAL_AGENT_HOST_PROVIDER_ID, + preferDevContainer: sessionId => preferred.push(sessionId), + }); + const sessionsProvidersService = providersService(provider); + const noSessionService = upcastPartial({ + openNewSession: async () => ({ session: undefined, trustDeclined: true }), + }); + await openNewSessionWithDevContainerPreference(URI.file('/declined'), LOCAL_AGENT_HOST_PROVIDER_ID, noSessionService, sessionsProvidersService); + + const otherSessionService = upcastPartial({ + openNewSession: async () => ({ + session: upcastPartial({ sessionId: 'other', providerId: 'other-provider' }), + trustDeclined: false, + }), + }); + const otherProviderService = providersService(upcastPartial({ id: 'other-provider' })); + await openNewSessionWithDevContainerPreference(URI.file('/other'), 'other-provider', otherSessionService, otherProviderService); + + assert.deepStrictEqual(preferred, []); + }); +}); diff --git a/src/vs/sessions/contrib/providers/agentHost/browser/localAgentHostSessionsProvider.ts b/src/vs/sessions/contrib/providers/agentHost/browser/localAgentHostSessionsProvider.ts index 8631d5c598acbe..5fe890615af5c5 100644 --- a/src/vs/sessions/contrib/providers/agentHost/browser/localAgentHostSessionsProvider.ts +++ b/src/vs/sessions/contrib/providers/agentHost/browser/localAgentHostSessionsProvider.ts @@ -123,6 +123,7 @@ export class LocalAgentHostSessionsProvider extends BaseAgentHostSessionsProvide private _automationSessionResources = new ResourceSet(); private readonly _devContainerAvailableDrafts = new Set(); private readonly _devContainerDrafts = new Set(); + private readonly _preferredDevContainerDrafts = new Set(); override get order(): number { return -1; } @@ -308,11 +309,16 @@ export class LocalAgentHostSessionsProvider extends BaseAgentHostSessionsProvide try { const available = await this._devContainerAgentHostService.isAvailable(workspaceUri); if (!available || !this._getNewSession(sessionId)) { + this._preferredDevContainerDrafts.delete(sessionId); return; } this._devContainerAvailableDrafts.add(sessionId); + if (this._preferredDevContainerDrafts.delete(sessionId)) { + this._devContainerDrafts.add(sessionId); + } this._onDidChangeSessionConfig.fire(sessionId); } catch (error) { + this._preferredDevContainerDrafts.delete(sessionId); this._logService.warn(`[${this.id}] Failed to resolve Dev Container availability for ${workspaceUri.toString()}`, error); } } @@ -325,6 +331,18 @@ export class LocalAgentHostSessionsProvider extends BaseAgentHostSessionsProvide return this._devContainerDrafts.has(sessionId); } + preferDevContainer(sessionId: string): void { + if (!this._getNewSession(sessionId)) { + throw new Error(`Cannot configure unknown new session '${sessionId}'.`); + } + if (this._devContainerAvailableDrafts.has(sessionId)) { + this._devContainerDrafts.add(sessionId); + this._onDidChangeSessionConfig.fire(sessionId); + } else { + this._preferredDevContainerDrafts.add(sessionId); + } + } + setDevContainerEnabled(sessionId: string, enabled: boolean): void { if (!this._getNewSession(sessionId)) { throw new Error(`Cannot configure unknown new session '${sessionId}'.`); @@ -333,9 +351,11 @@ export class LocalAgentHostSessionsProvider extends BaseAgentHostSessionsProvide throw new Error(`Cannot enable Dev Container execution for unavailable session '${sessionId}'.`); } if (enabled) { + this._preferredDevContainerDrafts.delete(sessionId); this._devContainerDrafts.add(sessionId); } else { this._devContainerDrafts.delete(sessionId); + this._preferredDevContainerDrafts.delete(sessionId); } this._onDidChangeSessionConfig.fire(sessionId); } @@ -524,12 +544,14 @@ export class LocalAgentHostSessionsProvider extends BaseAgentHostSessionsProvide override deleteNewSession(sessionId: string): void { this._devContainerAvailableDrafts.delete(sessionId); this._devContainerDrafts.delete(sessionId); + this._preferredDevContainerDrafts.delete(sessionId); super.deleteNewSession(sessionId); } protected override _disposeAllNewSessions(): void { this._devContainerAvailableDrafts.clear(); this._devContainerDrafts.clear(); + this._preferredDevContainerDrafts.clear(); super._disposeAllNewSessions(); } diff --git a/src/vs/sessions/contrib/providers/agentHost/test/browser/localAgentHostSessionsProvider.test.ts b/src/vs/sessions/contrib/providers/agentHost/test/browser/localAgentHostSessionsProvider.test.ts index 56ec50ca917ca5..5850af3b4eb246 100644 --- a/src/vs/sessions/contrib/providers/agentHost/test/browser/localAgentHostSessionsProvider.test.ts +++ b/src/vs/sessions/contrib/providers/agentHost/test/browser/localAgentHostSessionsProvider.test.ts @@ -3348,6 +3348,94 @@ suite('LocalAgentHostSessionsProvider', () => { assert.strictEqual(provider.getSessionConfig(session.sessionId), undefined); }); + test('enables a preferred Dev Container after asynchronous availability resolves', async () => { + const availability = new DeferredPromise(); + const provider = createProvider(disposables, agentHost, undefined, { + devContainerAgentHostService: new class extends mock() { + override async isAvailable(): Promise { + return availability.p; + } + }(), + }); + const session = provider.createNewSession( + URI.file('/home/user/project'), + provider.sessionTypes[0].id, + ); + provider.preferDevContainer(session.sessionId); + const beforeResolution = provider.isDevContainerEnabled(session.sessionId); + availability.complete(true); + await timeout(0); + + assert.deepStrictEqual({ + beforeResolution, + available: provider.isDevContainerAvailable(session.sessionId), + enabled: provider.isDevContainerEnabled(session.sessionId), + }, { + beforeResolution: false, + available: true, + enabled: true, + }); + }); + + test('enables a preferred Dev Container immediately after availability resolved', async () => { + const provider = createProvider(disposables, agentHost, undefined, { + devContainerAgentHostService: new class extends mock() { + override async isAvailable(): Promise { + return true; + } + }(), + }); + const session = provider.createNewSession( + URI.file('/home/user/project'), + provider.sessionTypes[0].id, + ); + await timeout(0); + provider.preferDevContainer(session.sessionId); + + assert.deepStrictEqual({ + available: provider.isDevContainerAvailable(session.sessionId), + enabled: provider.isDevContainerEnabled(session.sessionId), + }, { + available: true, + enabled: true, + }); + }); + + test('does not enable a preferred Dev Container when unavailable or canceled', async () => { + const unavailable = new DeferredPromise(); + const canceled = new DeferredPromise(); + let call = 0; + const provider = createProvider(disposables, agentHost, undefined, { + devContainerAgentHostService: new class extends mock() { + override async isAvailable(): Promise { + return ++call === 1 ? unavailable.p : canceled.p; + } + }(), + }); + const unavailableSession = provider.createNewSession(URI.file('/unavailable'), provider.sessionTypes[0].id); + provider.preferDevContainer(unavailableSession.sessionId); + unavailable.complete(false); + await timeout(0); + + const canceledSession = provider.createNewSession(URI.file('/canceled'), provider.sessionTypes[0].id); + provider.preferDevContainer(canceledSession.sessionId); + provider.setDevContainerEnabled(canceledSession.sessionId, false); + canceled.complete(true); + await timeout(0); + + assert.deepStrictEqual({ + unavailableAvailable: provider.isDevContainerAvailable(unavailableSession.sessionId), + unavailableEnabled: provider.isDevContainerEnabled(unavailableSession.sessionId), + canceledAvailable: provider.isDevContainerAvailable(canceledSession.sessionId), + canceledEnabled: provider.isDevContainerEnabled(canceledSession.sessionId), + }, { + unavailableAvailable: false, + unavailableEnabled: false, + canceledAvailable: true, + canceledEnabled: false, + }); + }); + test('prepareNewSession does not start a Dev Container when workspace trust is denied', async () => { let connectCalls = 0; const devContainerAgentHostService = new class extends mock() { diff --git a/src/vs/sessions/services/sessions/browser/sessionsService.ts b/src/vs/sessions/services/sessions/browser/sessionsService.ts index 72b52483b43239..4acfc390376f93 100644 --- a/src/vs/sessions/services/sessions/browser/sessionsService.ts +++ b/src/vs/sessions/services/sessions/browser/sessionsService.ts @@ -62,6 +62,8 @@ export interface IOpenNewSessionOptions extends ICreateNewSessionOptions { * (restoring any pending draft). */ readonly folderUri?: URI; + /** Cancel startup session restoration so this new-session navigation wins. */ + readonly cancelRestore?: boolean; } /** @@ -1050,6 +1052,9 @@ export class SessionsService extends Disposable implements ISessionsService { } private async _openNewSession(options: IOpenNewSessionOptions | undefined, token: CancellationToken, intent: SessionNavigationIntent): Promise { + if (options?.cancelRestore) { + this._cancelRestore(); + } const folderUri = options?.folderUri; if (folderUri) { // Single trust gate for every path that creates a concrete session for diff --git a/src/vs/sessions/services/sessions/test/browser/sessionsManagementService.test.ts b/src/vs/sessions/services/sessions/test/browser/sessionsManagementService.test.ts index 05d954673f50d1..d912e48affe8d7 100644 --- a/src/vs/sessions/services/sessions/test/browser/sessionsManagementService.test.ts +++ b/src/vs/sessions/services/sessions/test/browser/sessionsManagementService.test.ts @@ -817,6 +817,57 @@ suite('SessionsManagementService', () => { }); }); + test('RACE: an explicit new-session handoff cancels startup restoration', async () => { + const targetSession = stubSession({ sessionId: 'target', providerId: 'test' }); + const newSession = stubSession({ sessionId: 'fresh', providerId: 'test' }); + const onDidChangeSessions = disposables.add(new Emitter()); + let sessions: ISession[] = []; + const provider = new class extends TestSessionsProvider { + override readonly onDidChangeSessions = onDidChangeSessions.event; + constructor() { super(targetSession); } + override getSessions(): ISession[] { return sessions; } + override createNewSession(): ISession { return newSession; } + override resolveWorkspace(): ISessionWorkspace { + return { folders: [], requiresWorkspaceTrust: false, isVirtualWorkspace: false } as unknown as ISessionWorkspace; + } + }; + const storage = disposables.add(new InMemoryStorageService()); + storage.store( + 'agentSessions.activeSessionStates', + JSON.stringify([{ sessionResource: targetSession.resource.toString(), visibleOrder: 0, isActive: true }]), + 1 /* StorageScope.WORKSPACE */, + 1 /* StorageTarget.MACHINE */, + ); + const instantiationService = disposables.add(new TestInstantiationService()); + instantiationService.stub(IStorageService, storage); + instantiationService.stub(ILogService, new NullLogService()); + instantiationService.stub(IContextKeyService, disposables.add(new MockContextKeyService())); + instantiationService.stub(ISessionsProvidersService, new TestSessionsProvidersService([provider])); + instantiationService.stub(IUriIdentityService, { extUri: extUriBiasedIgnorePathCase }); + instantiationService.stub(IChatWidgetService, new TestChatWidgetService()); + instantiationService.stub(IProgressService, new TestProgressService()); + instantiationService.stub(IChatService, new class extends mock() { + override readonly onDidSubmitRequest = Event.None; + }); + const service = disposables.add(instantiationService.createInstance(SessionsManagementService)); + const view = createView(instantiationService, service, disposables); + const restorePromise = view.restoreVisibleSessions(); + await Promise.resolve(); + + await view.openNewSession({ folderUri: URI.file('/folder'), cancelRestore: true }); + sessions = [targetSession]; + onDidChangeSessions.fire({ added: [targetSession], removed: [], changed: [] }); + await restorePromise; + + assert.deepStrictEqual({ + hasTarget: view.visibleSessions.get().some(session => session?.sessionId === 'target'), + active: view.activeSession.get()?.sessionId, + }, { + hasTarget: false, + active: 'fresh', + }); + }); + test.skip('openNewSession inherits the active session workspace when requested', async () => { const makeWorkspace = (uri: URI): ISessionWorkspace => ({ uri, 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 5b455ab42cd75a..9280fbd0ea1a73 100644 --- a/src/vs/workbench/contrib/chat/electron-browser/agentSessions/agentSessionsActions.ts +++ b/src/vs/workbench/contrib/chat/electron-browser/agentSessions/agentSessionsActions.ts @@ -7,6 +7,7 @@ import { $, append } from '../../../../../base/browser/dom.js'; import { BaseActionViewItem, IBaseActionViewItemOptions } from '../../../../../base/browser/ui/actionbar/actionViewItems.js'; import { getDefaultHoverDelegate } from '../../../../../base/browser/ui/hover/hoverDelegateFactory.js'; import { IAction } from '../../../../../base/common/actions.js'; +import { decodeHex } from '../../../../../base/common/buffer.js'; import { Disposable } from '../../../../../base/common/lifecycle.js'; import { ServicesAccessor } from '../../../../../editor/browser/editorExtensions.js'; import { localize, localize2 } from '../../../../../nls.js'; @@ -43,12 +44,34 @@ import { AgentsWindowOpenSource, isAgentsWindowOpenSource } from '../../../../.. const OPEN_WORKSPACE_IN_AGENTS_WINDOW_TITLE = localize2('openWorkspaceInAgentsWindow', "Open in Agents"); const OPEN_WORKSPACE_IN_AGENTS_WINDOW_CHAT_TITLE_COMMAND_ID = 'workbench.action.chat.openWorkspaceInAgentsWindow.chatTitle'; const OPEN_WORKSPACE_IN_AGENTS_WINDOW_TITLE_BAR_COMMAND_ID = 'workbench.action.chat.openWorkspaceInAgentsWindow.titleBar'; +const DEV_CONTAINER_REMOTE_AUTHORITY_PREFIX = 'dev-container+'; + +function getLocalAgentsFolderUri(folderUri: URI | undefined): URI | undefined { + if (folderUri?.scheme === Schemas.file) { + return folderUri; + } + if (folderUri?.scheme !== Schemas.vscodeRemote || !folderUri.authority.startsWith(DEV_CONTAINER_REMOTE_AUTHORITY_PREFIX)) { + return undefined; + } + try { + return URI.file(decodeHex(folderUri.authority.slice(DEV_CONTAINER_REMOTE_AUTHORITY_PREFIX.length)).toString()); + } catch (error) { + if (error instanceof SyntaxError) { + return undefined; + } + throw error; + } +} async function openCurrentWorkspaceInAgentsWindow(accessor: ServicesAccessor, source: AgentsWindowOpenSource): Promise { const nativeHostService = accessor.get(INativeHostService); const workspaceContextService = accessor.get(IWorkspaceContextService); - const folderUri = workspaceContextService.getWorkspace().folders[0]?.uri; - await nativeHostService.openAgentsWindow({ folderUri: folderUri?.scheme === Schemas.file ? folderUri : undefined, source }); + const folderUri = getLocalAgentsFolderUri(workspaceContextService.getWorkspace().folders[0]?.uri); + await nativeHostService.openAgentsWindow({ + folderUri, + source, + preferDevContainer: true, + }); } function isOpenChatSessionInAgentsWindowOptions(value: unknown): value is { readonly agentsWindowOpenSource: AgentsWindowOpenSource } { @@ -153,7 +176,7 @@ export class OpenAgentsWindowAction extends Action2 { }); } - async run(accessor: ServicesAccessor, args?: { folderUri?: UriComponents; sessionResource?: UriComponents; source?: AgentsWindowOpenSource }) { + async run(accessor: ServicesAccessor, args?: { folderUri?: UriComponents; sessionResource?: UriComponents; source?: AgentsWindowOpenSource; preferDevContainer?: boolean }) { const nativeHostService = accessor.get(INativeHostService); await nativeHostService.openAgentsWindow({ ...args, source: args?.source ?? AgentsWindowOpenSource.CommandPalette }); } 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 new file mode 100644 index 00000000000000..382e9b805a5ee8 --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/electron-browser/agentSessionsActions.test.ts @@ -0,0 +1,69 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import assert from 'assert'; +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 { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.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'; + +suite('OpenWorkspaceInAgentsWindowAction', () => { + const disposables = ensureNoDisposablesAreLeakedInTestSuite(); + + test('opens the Agents Window with the local folder and Dev Container preference', async () => { + const store = disposables.add(new DisposableStore()); + const instantiationService = store.add(new TestInstantiationService()); + let workspaceFolderUri = URI.file('/workspace'); + const calls: IOpenAgentsWindowOptions[] = []; + instantiationService.stub(IWorkspaceContextService, upcastPartial({ + getWorkspace: () => ({ + id: 'workspace', + folders: [{ + uri: workspaceFolderUri, + name: 'workspace', + index: 0, + toResource: relativePath => URI.joinPath(workspaceFolderUri, relativePath), + }], + }), + })); + instantiationService.stub(INativeHostService, upcastPartial({ + openAgentsWindow: async options => { calls.push(options ?? {}); }, + })); + + await instantiationService.invokeFunction(accessor => new OpenWorkspaceInAgentsWindowAction().run(accessor, { + source: AgentsWindowOpenSource.TitleBar, + })); + const hostFolderUri = URI.file('/host/workspace'); + workspaceFolderUri = URI.from({ + scheme: Schemas.vscodeRemote, + authority: `dev-container+${encodeHex(VSBuffer.fromString(hostFolderUri.fsPath))}`, + path: '/workspaces/project', + }); + await instantiationService.invokeFunction(accessor => new OpenWorkspaceInAgentsWindowAction().run(accessor, { + source: AgentsWindowOpenSource.ChatTitleBar, + })); + + assert.deepStrictEqual(calls.map(call => ({ + folderUri: URI.revive(call.folderUri)?.toString(), + source: call.source, + preferDevContainer: call.preferDevContainer, + })), [{ + folderUri: URI.file('/workspace').toString(), + source: AgentsWindowOpenSource.TitleBar, + preferDevContainer: true, + }, { + folderUri: hostFolderUri.toString(), + source: AgentsWindowOpenSource.ChatTitleBar, + preferDevContainer: true, + }]); + }); +}); From 106ab7fdfec7aafffed78f36c3f85def31103787 Mon Sep 17 00:00:00 2001 From: Christof Marti Date: Thu, 3 Sep 2026 16:24:11 +0200 Subject: [PATCH 2/8] Address Open in Agents review feedback Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../sessions/browser/parts/chatGroupView.ts | 4 +- .../sessions/browser/parts/chatGroupsView.ts | 4 +- src/vs/sessions/browser/parts/chatView.ts | 2 +- src/vs/sessions/browser/parts/sessionView.ts | 4 +- .../chat/browser/agentsWindowOpenIntent.ts | 21 ---- .../sessions/contrib/chat/browser/chatView.ts | 4 +- .../contrib/chat/browser/newChatWidget.ts | 25 ++++- .../electron-browser/chat.contribution.ts | 16 +-- .../browser/agentsWindowOpenIntent.test.ts | 76 +------------ .../chat/test/browser/newChatWidget.test.ts | 103 +++++++++++++++++- .../sessions/browser/sessionsService.ts | 8 +- .../agentSessions/agentSessionsActions.ts | 6 +- .../agentSessionsActions.test.ts | 2 +- 13 files changed, 144 insertions(+), 131 deletions(-) diff --git a/src/vs/sessions/browser/parts/chatGroupView.ts b/src/vs/sessions/browser/parts/chatGroupView.ts index dd6bc1ad8af62d..f66acdf1b6c42a 100644 --- a/src/vs/sessions/browser/parts/chatGroupView.ts +++ b/src/vs/sessions/browser/parts/chatGroupView.ts @@ -333,8 +333,8 @@ export class ChatGroupView extends Disposable implements ISerializableView { return this._currentView.value?.submitInput() ?? Promise.resolve(false); } - selectWorkspace(folderUri: URI, providerId?: string): void { - this._currentView.value?.selectWorkspace(folderUri, providerId); + selectWorkspace(folderUri: URI, providerId?: string, options?: { readonly preferDevContainer?: boolean }): void { + this._currentView.value?.selectWorkspace(folderUri, providerId, options); } prefillInput(text: string): void { diff --git a/src/vs/sessions/browser/parts/chatGroupsView.ts b/src/vs/sessions/browser/parts/chatGroupsView.ts index da55bf5bce6c39..bde821df64ee0e 100644 --- a/src/vs/sessions/browser/parts/chatGroupsView.ts +++ b/src/vs/sessions/browser/parts/chatGroupsView.ts @@ -792,8 +792,8 @@ export class ChatGroupsView extends Themable { return this._activeGroup?.view.submitInput() ?? Promise.resolve(false); } - selectWorkspace(folderUri: URI, providerId?: string): void { - this._activeGroup?.view.selectWorkspace(folderUri, providerId); + selectWorkspace(folderUri: URI, providerId?: string, options?: { readonly preferDevContainer?: boolean }): void { + this._activeGroup?.view.selectWorkspace(folderUri, providerId, options); } prefillInput(text: string): void { diff --git a/src/vs/sessions/browser/parts/chatView.ts b/src/vs/sessions/browser/parts/chatView.ts index 7ad49b776284ec..7b78551dcf36d4 100644 --- a/src/vs/sessions/browser/parts/chatView.ts +++ b/src/vs/sessions/browser/parts/chatView.ts @@ -81,7 +81,7 @@ export abstract class AbstractChatView extends Disposable implements ISerializab * implementation is a no-op; subclasses that host a workspace picker * (e.g. `NewChatView`) override this to forward the selection. */ - selectWorkspace(_folderUri: URI, _providerId?: string): void { + selectWorkspace(_folderUri: URI, _providerId?: string, _options?: { readonly preferDevContainer?: boolean }): void { // no-op by default } diff --git a/src/vs/sessions/browser/parts/sessionView.ts b/src/vs/sessions/browser/parts/sessionView.ts index 7b8e1e96e96411..8645059fd75089 100644 --- a/src/vs/sessions/browser/parts/sessionView.ts +++ b/src/vs/sessions/browser/parts/sessionView.ts @@ -284,9 +284,9 @@ export class SessionView extends Disposable implements ISerializableView { return this._isVisible && this._header.startTitleEditing(); } - selectWorkspace(folderUri: URI, providerId?: string): void { + selectWorkspace(folderUri: URI, providerId?: string, options?: { readonly preferDevContainer?: boolean }): void { const standaloneView = this._standaloneView.value; - standaloneView ? standaloneView.selectWorkspace(folderUri, providerId) : this._groupsView.selectWorkspace(folderUri, providerId); + standaloneView ? standaloneView.selectWorkspace(folderUri, providerId, options) : this._groupsView.selectWorkspace(folderUri, providerId, options); } /** Opens the given chat in a group beside the active one ("open to the side"). */ diff --git a/src/vs/sessions/contrib/chat/browser/agentsWindowOpenIntent.ts b/src/vs/sessions/contrib/chat/browser/agentsWindowOpenIntent.ts index 5d7a33a2e2dc63..00dd2e9610ba97 100644 --- a/src/vs/sessions/contrib/chat/browser/agentsWindowOpenIntent.ts +++ b/src/vs/sessions/contrib/chat/browser/agentsWindowOpenIntent.ts @@ -3,31 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { URI } from '../../../../base/common/uri.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; -import { isAgentHostProvider } from '../../../common/agentHostSessionsProvider.js'; import { DevContainerAgentHostEnabledSettingId } from '../../../common/devContainerAgentHostService.js'; -import { ISessionsService } from '../../../services/sessions/browser/sessionsService.js'; -import { ISessionsProvidersService } from '../../../services/sessions/browser/sessionsProvidersService.js'; export function shouldPreferDevContainer(requested: unknown, configurationService: IConfigurationService): boolean { return requested === true && configurationService.getValue(DevContainerAgentHostEnabledSettingId) === true; } - -export async function openNewSessionWithDevContainerPreference( - folderUri: URI, - providerId: string, - sessionsService: ISessionsService, - sessionsProvidersService: ISessionsProvidersService, -): Promise { - const result = await sessionsService.openNewSession({ folderUri, providerId, cancelRestore: true }); - const session = result.session; - if (!session) { - return; - } - const provider = sessionsProvidersService.getProvider(session.providerId); - if (provider && isAgentHostProvider(provider)) { - provider.preferDevContainer?.(session.sessionId); - } -} diff --git a/src/vs/sessions/contrib/chat/browser/chatView.ts b/src/vs/sessions/contrib/chat/browser/chatView.ts index 0affeda1917ba1..7b4c42e1f17e16 100644 --- a/src/vs/sessions/contrib/chat/browser/chatView.ts +++ b/src/vs/sessions/contrib/chat/browser/chatView.ts @@ -104,9 +104,9 @@ export class NewChatView extends AbstractChatView { this._widget.focusInput(); } - override selectWorkspace(folderUri: URI, providerId?: string): void { + override selectWorkspace(folderUri: URI, providerId?: string, options?: { readonly preferDevContainer?: boolean }): void { if (this._widget instanceof NewChatWidget) { - this._widget.selectWorkspace(folderUri, providerId); + this._widget.selectWorkspace(folderUri, providerId, options); } } diff --git a/src/vs/sessions/contrib/chat/browser/newChatWidget.ts b/src/vs/sessions/contrib/chat/browser/newChatWidget.ts index 3099ac24e9f4dd..456f9b524b249e 100644 --- a/src/vs/sessions/contrib/chat/browser/newChatWidget.ts +++ b/src/vs/sessions/contrib/chat/browser/newChatWidget.ts @@ -25,8 +25,10 @@ import { localize } from '../../../../nls.js'; import { IActiveSession, ISessionsManagementService } from '../../../services/sessions/common/sessionsManagement.js'; import { ISession, SESSION_WORKSPACE_GROUP_GITHUB } from '../../../services/sessions/common/session.js'; import { IOpenNewSessionResult, ISessionsService } from '../../../services/sessions/browser/sessionsService.js'; +import { ISessionsProvidersService } from '../../../services/sessions/browser/sessionsProvidersService.js'; import { isAllowSignedOutWhenUsableEnabled, shouldShowGitHubWorkspaceGroupSignIn } from '../../../browser/sessionsAuthGate.js'; import { AGENTIC_SIGN_IN_COMMAND_ID } from '../../../common/sessionCommands.js'; +import { isAgentHostProvider } from '../../../common/agentHostSessionsProvider.js'; import { IAquariumService, IMountedToggleHandle } from '../../aquarium/browser/aquariumOverlay.js'; import { IWorkspacePickerTrigger, WorkspacePicker } from './sessionWorkspacePicker.js'; import { WebWorkspacePicker } from './webWorkspacePicker.js'; @@ -70,6 +72,7 @@ export class NewChatWidget extends Disposable { /** Recreates the draft once a better/late-registering provider can serve the folder (see {@link _createNewSession}). */ private readonly _pendingPreferredUpgrade = new MutableDisposable(); private readonly _newSessionCreation = new MutableDisposable(); + private _preferredDevContainerFolderUri: URI | undefined; /** * The currently mounted no-agent-host empty state, if any. Set by @@ -111,6 +114,7 @@ export class NewChatWidget extends Disposable { @ILogService private readonly logService: ILogService, @ISessionsManagementService private readonly sessionsManagementService: ISessionsManagementService, @ISessionsService private readonly sessionsService: ISessionsService, + @ISessionsProvidersService private readonly sessionsProvidersService: ISessionsProvidersService, @IAquariumService private readonly aquariumService: IAquariumService, @IAgentHostFilterService private readonly agentHostFilterService: IAgentHostFilterService, @IUriIdentityService private readonly uriIdentityService: IUriIdentityService, @@ -602,13 +606,16 @@ export class NewChatWidget extends Disposable { } else { return result; } + this._applyPreferredDevContainer(result.session, folderUri); if (result.trustDeclined) { + this._preferredDevContainerFolderUri = undefined; // The user explicitly declined trust: don't schedule a retry, which // would silently recreate (and possibly re-prompt) the draft once a // provider registers/changes without any further user action. this._pendingPreferredUpgrade.clear(); return result; } + // Keep the draft in sync with late-registering providers. Agent hosts // connect lazily, so there is no timeout — the listener lives until the // draft is sent or replaced. We watch when: @@ -624,6 +631,18 @@ export class NewChatWidget extends Disposable { return result; } + private _applyPreferredDevContainer(session: ISession | undefined, folderUri: URI): void { + if (!session || !this._preferredDevContainerFolderUri || !this.uriIdentityService.extUri.isEqual(this._preferredDevContainerFolderUri, folderUri)) { + return; + } + const provider = this.sessionsProvidersService.getProvider(session.providerId); + if (!provider || !isAgentHostProvider(provider) || !provider.preferDevContainer) { + return; + } + provider.preferDevContainer(session.sessionId); + this._preferredDevContainerFolderUri = undefined; + } + private async _createSessionNow(folderUri: URI, userPick: IPreferredSessionType | undefined, token: CancellationToken): Promise { // Prefer the user's explicit pick when its provider can serve the // folder; otherwise fall back to the preferred (first) session type. @@ -947,6 +966,9 @@ export class NewChatWidget extends Disposable { private async _onWorkspaceSelected(folderUri: URI | undefined): Promise { // Cancel any in-flight upgrade for a previous selection. this._pendingPreferredUpgrade.clear(); + if (!folderUri || !this._preferredDevContainerFolderUri || !this.uriIdentityService.extUri.isEqual(this._preferredDevContainerFolderUri, folderUri)) { + this._preferredDevContainerFolderUri = undefined; + } const currentFolderUri = this._session.get()?.workspace.get()?.folders[0]?.root; const refreshingPromptOptions = !!currentFolderUri && (!folderUri || !this.uriIdentityService.extUri.isEqual(currentFolderUri, folderUri)) @@ -1004,7 +1026,8 @@ export class NewChatWidget extends Disposable { this._newChatInput.attach(uris); } - selectWorkspace(folderUri: URI, providerId?: string): void { + selectWorkspace(folderUri: URI, providerId?: string, options?: { readonly preferDevContainer?: boolean }): void { + this._preferredDevContainerFolderUri = options?.preferDevContainer ? folderUri : undefined; this._workspacePicker.setSelectedWorkspace(folderUri, { providerId }); } } 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 b77f969fe6b691..ace9b58bafc41b 100644 --- a/src/vs/sessions/contrib/chat/electron-browser/chat.contribution.ts +++ b/src/vs/sessions/contrib/chat/electron-browser/chat.contribution.ts @@ -28,7 +28,7 @@ import { ITelemetryService } from '../../../../platform/telemetry/common/telemet import { TOTAL_SESSIONS_KEY } from '../../sessions/browser/sessionsLifecycleTracker.js'; import { ISessionsWindowOpenViewState, SessionsWindowOpenTelemetry, SessionsWindowSessionStartTelemetry } from '../../sessions/browser/sessionsWindowOpenTelemetry.js'; import { INewSessionComposerService, NewSessionWorkspacePreselectionSource } from '../browser/newSessionComposerService.js'; -import { openNewSessionWithDevContainerPreference, shouldPreferDevContainer } from '../browser/agentsWindowOpenIntent.js'; +import { shouldPreferDevContainer } from '../browser/agentsWindowOpenIntent.js'; class SelectAgentsFolderContribution extends Disposable implements IWorkbenchContribution { @@ -220,24 +220,12 @@ class SelectAgentsFolderContribution extends Disposable implements IWorkbenchCon if (!resolved) { return false; } - if (preferDevContainer) { - void this.openNewDevContainerSession(folderUri, resolved.providerId); - return true; - } const activeSession = this.sessionsService.activeSession.get(); if (activeSession === undefined || activeSession.status.get() === SessionStatus.Untitled) { - this.sessionsPartService.getSessionView(activeSession?.sessionId)?.selectWorkspace(folderUri, resolved.providerId); + this.sessionsPartService.getSessionView(activeSession?.sessionId)?.selectWorkspace(folderUri, resolved.providerId, { preferDevContainer }); } return true; } - - private async openNewDevContainerSession(folderUri: URI, providerId: string): Promise { - try { - await openNewSessionWithDevContainerPreference(folderUri, providerId, this.sessionsService, this.sessionsProvidersService); - } catch (error) { - this.logService.error('[AgentsHandoff] Failed to select Dev Container workspace', error); - } - } } registerWorkbenchContribution2(SelectAgentsFolderContribution.ID, SelectAgentsFolderContribution, WorkbenchPhase.BlockStartup); diff --git a/src/vs/sessions/contrib/chat/test/browser/agentsWindowOpenIntent.test.ts b/src/vs/sessions/contrib/chat/test/browser/agentsWindowOpenIntent.test.ts index 84fea0272f8ad6..2b30d74029c160 100644 --- a/src/vs/sessions/contrib/chat/test/browser/agentsWindowOpenIntent.test.ts +++ b/src/vs/sessions/contrib/chat/test/browser/agentsWindowOpenIntent.test.ts @@ -4,29 +4,14 @@ *--------------------------------------------------------------------------------------------*/ import assert from 'assert'; -import { URI } from '../../../../../base/common/uri.js'; -import { mock, upcastPartial } from '../../../../../base/test/common/mock.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; import { TestConfigurationService } from '../../../../../platform/configuration/test/common/testConfigurationService.js'; -import { LOCAL_AGENT_HOST_PROVIDER_ID, IAgentHostSessionsProvider } from '../../../../common/agentHostSessionsProvider.js'; import { DevContainerAgentHostEnabledSettingId } from '../../../../common/devContainerAgentHostService.js'; -import { ISession } from '../../../../services/sessions/common/session.js'; -import { ISessionsProvider } from '../../../../services/sessions/common/sessionsProvider.js'; -import { ISessionsService, IOpenNewSessionOptions } from '../../../../services/sessions/browser/sessionsService.js'; -import { ISessionsProvidersService } from '../../../../services/sessions/browser/sessionsProvidersService.js'; -import { openNewSessionWithDevContainerPreference, shouldPreferDevContainer } from '../../browser/agentsWindowOpenIntent.js'; +import { shouldPreferDevContainer } from '../../browser/agentsWindowOpenIntent.js'; suite('Agents Window open intent', () => { ensureNoDisposablesAreLeakedInTestSuite(); - function providersService(provider: ISessionsProvider): ISessionsProvidersService { - return new class extends mock() { - override getProvider(providerId: string): T | undefined { - return providerId === provider.id ? provider as T : undefined; - } - }(); - } - test('gates the Dev Container preference on the Agents Window setting', () => { const configurationService = (enabled: boolean) => new TestConfigurationService({ [DevContainerAgentHostEnabledSettingId]: enabled, @@ -44,63 +29,4 @@ suite('Agents Window open intent', () => { invalidRequestAndEnabled: false, }); }); - - test('opens the local folder and applies the Dev Container preference to its draft', async () => { - const folderUri = URI.file('/workspace'); - const session = upcastPartial({ - sessionId: 'new-session', - providerId: LOCAL_AGENT_HOST_PROVIDER_ID, - }); - let openOptions: IOpenNewSessionOptions | undefined; - const sessionsService = upcastPartial({ - openNewSession: async options => { - openOptions = options; - return { session, trustDeclined: false }; - }, - }); - const preferred: string[] = []; - const provider = upcastPartial({ - id: LOCAL_AGENT_HOST_PROVIDER_ID, - preferDevContainer: sessionId => preferred.push(sessionId), - }); - const sessionsProvidersService = providersService(provider); - - await openNewSessionWithDevContainerPreference(folderUri, LOCAL_AGENT_HOST_PROVIDER_ID, sessionsService, sessionsProvidersService); - - assert.deepStrictEqual({ - folderUri: openOptions?.folderUri?.toString(), - providerId: openOptions?.providerId, - cancelRestore: openOptions?.cancelRestore, - preferred, - }, { - folderUri: folderUri.toString(), - providerId: LOCAL_AGENT_HOST_PROVIDER_ID, - cancelRestore: true, - preferred: ['new-session'], - }); - }); - - test('does not apply the preference without a local Agent Host draft', async () => { - const preferred: string[] = []; - const provider = upcastPartial({ - id: LOCAL_AGENT_HOST_PROVIDER_ID, - preferDevContainer: sessionId => preferred.push(sessionId), - }); - const sessionsProvidersService = providersService(provider); - const noSessionService = upcastPartial({ - openNewSession: async () => ({ session: undefined, trustDeclined: true }), - }); - await openNewSessionWithDevContainerPreference(URI.file('/declined'), LOCAL_AGENT_HOST_PROVIDER_ID, noSessionService, sessionsProvidersService); - - const otherSessionService = upcastPartial({ - openNewSession: async () => ({ - session: upcastPartial({ sessionId: 'other', providerId: 'other-provider' }), - trustDeclined: false, - }), - }); - const otherProviderService = providersService(upcastPartial({ id: 'other-provider' })); - await openNewSessionWithDevContainerPreference(URI.file('/other'), 'other-provider', otherSessionService, otherProviderService); - - assert.deepStrictEqual(preferred, []); - }); }); diff --git a/src/vs/sessions/contrib/chat/test/browser/newChatWidget.test.ts b/src/vs/sessions/contrib/chat/test/browser/newChatWidget.test.ts index 6c3f0b6905e144..bfc413c51b2902 100644 --- a/src/vs/sessions/contrib/chat/test/browser/newChatWidget.test.ts +++ b/src/vs/sessions/contrib/chat/test/browser/newChatWidget.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import assert from 'assert'; -import { DeferredPromise } from '../../../../../base/common/async.js'; +import { DeferredPromise, timeout } from '../../../../../base/common/async.js'; import { CancellationToken } from '../../../../../base/common/cancellation.js'; import { Emitter, Event } from '../../../../../base/common/event.js'; import { IDisposable, MutableDisposable, toDisposable } from '../../../../../base/common/lifecycle.js'; @@ -21,6 +21,7 @@ import { NewChatWidget } from '../../browser/newChatWidget.js'; import { IChatRequestVariableEntry, toFileVariableEntry, toPasteVariableEntry } from '../../../../../workbench/contrib/chat/common/attachments/chatVariableEntries.js'; import { Codicon } from '../../../../../base/common/codicons.js'; import { getAdditionalFolderContextId, getAdditionalRepositoryContextId } from '../../common/newChatContextIds.js'; +import { LOCAL_AGENT_HOST_PROVIDER_ID } from '../../../../common/agentHostSessionsProvider.js'; /** The part of the active session `_recreateOnProviderChange` actually reads. */ interface IActiveDraft { @@ -65,6 +66,7 @@ interface INewChatWidgetHarness extends IRecreateHarness { }; }; _createSessionNow(folderUri: URI, userPick: IPreferredSessionType | undefined, token: CancellationToken): Promise; + _applyPreferredDevContainer(session: ISession | undefined, folderUri: URI): void; _scheduleRecreateOnProviderChange(folderUri: URI, userPick: IPreferredSessionType | undefined, created: ISession | undefined, replayMissedChange: boolean): void; _recreateOnProviderChange(folderUri: URI, userPick: IPreferredSessionType | undefined, created: ISession | undefined): void; } @@ -79,6 +81,17 @@ const createSessionNow = Reflect.get(NewChatWidget.prototype, '_createSessionNow userPick: IPreferredSessionType | undefined, token: CancellationToken, ) => Promise; +const applyPreferredDevContainer = Reflect.get(NewChatWidget.prototype, '_applyPreferredDevContainer') as ( + this: { + _preferredDevContainerFolderUri: URI | undefined; + readonly uriIdentityService: { readonly extUri: typeof extUri }; + readonly sessionsProvidersService: { + getProvider(providerId: string): { readonly id: string; preferDevContainer?(sessionId: string): void } | undefined; + }; + }, + session: ISession | undefined, + folderUri: URI, +) => void; const scheduleRecreateOnProviderChange = Reflect.get(NewChatWidget.prototype, '_scheduleRecreateOnProviderChange') as INewChatWidgetHarness['_scheduleRecreateOnProviderChange']; const recreateOnProviderChange = Reflect.get(NewChatWidget.prototype, '_recreateOnProviderChange') as ( this: IRecreateHarness, @@ -153,6 +166,7 @@ function createHarness( }, _isPreferredServable: () => false, _createSessionNow: (_folderUri, _userPick, token) => stubCreateSessionNow(token), + _applyPreferredDevContainer: () => { }, _createNewSession: folderUri => createNewSession.call(harness, folderUri), _scheduleRecreateOnProviderChange: (folderUri, userPick, created, replayMissedChange) => scheduleRecreateOnProviderChange.call(harness, folderUri, userPick, created, replayMissedChange), _recreateOnProviderChange: (folderUri, userPick, created) => recreateOnProviderChange.call(harness, folderUri, userPick, created), @@ -283,21 +297,102 @@ suite('NewChatWidget', () => { assert.strictEqual(createCount, 2); }); - test('waits for another provider change after creation fails', async () => { + test('applies the Dev Container preference when a late provider creates the draft', async () => { const sessionTypesChanged = disposables.add(new Emitter()); const pendingPreferredUpgrade = disposables.add(new MutableDisposable()); const newSessionCreation = disposables.add(new MutableDisposable()); let createCount = 0; + const applied: string[] = []; + const session = upcastPartial({ sessionId: 'draft', providerId: LOCAL_AGENT_HOST_PROVIDER_ID }); const harness = createHarness(pendingPreferredUpgrade, newSessionCreation, sessionTypesChanged.event, async () => { createCount++; - return { session: undefined, trustDeclined: false }; + return { session: createCount === 1 ? undefined : session, trustDeclined: false }; }); + harness._applyPreferredDevContainer = created => { + if (created) { + applied.push(created.sessionId); + } + }; await harness._createNewSession(URI.file('/project')); const countBeforeChange = createCount; sessionTypesChanged.fire(); + await timeout(0); + + assert.deepStrictEqual({ + countBeforeChange, + countAfterChange: createCount, + applied, + }, { + countBeforeChange: 1, + countAfterChange: 2, + applied: ['draft'], + }); + }); + + test('applies the Dev Container preference after the composer creates the requested draft', async () => { + const sessionTypesChanged = disposables.add(new Emitter()); + const pendingPreferredUpgrade = disposables.add(new MutableDisposable()); + const newSessionCreation = disposables.add(new MutableDisposable()); + const folder = URI.file('/project'); + const session = upcastPartial({ sessionId: 'draft', providerId: LOCAL_AGENT_HOST_PROVIDER_ID }); + const applied: Array<{ sessionId: string; folder: string }> = []; + const harness = createHarness(pendingPreferredUpgrade, newSessionCreation, sessionTypesChanged.event, async () => ({ + session, + trustDeclined: false, + })); + harness._applyPreferredDevContainer = (created, createdFolder) => { + if (created) { + applied.push({ sessionId: created.sessionId, folder: createdFolder.toString() }); + } + }; + + await harness._createNewSession(folder); + + assert.deepStrictEqual(applied, [{ sessionId: 'draft', folder: folder.toString() }]); + }); - assert.deepStrictEqual({ countBeforeChange, countAfterChange: createCount }, { countBeforeChange: 1, countAfterChange: 2 }); + test('applies a pending Dev Container preference only to a matching Agent Host draft', () => { + const folder = URI.file('/project'); + const preferred: string[] = []; + const harness = { + _preferredDevContainerFolderUri: folder, + uriIdentityService: { extUri }, + sessionsProvidersService: { + getProvider: (providerId: string) => providerId === LOCAL_AGENT_HOST_PROVIDER_ID + ? { id: providerId, preferDevContainer: (sessionId: string) => preferred.push(sessionId) } + : { id: providerId }, + }, + }; + applyPreferredDevContainer.call( + harness, + upcastPartial({ sessionId: 'other', providerId: 'other-provider' }), + folder, + ); + const pendingAfterOtherProvider = harness._preferredDevContainerFolderUri?.toString(); + applyPreferredDevContainer.call( + harness, + upcastPartial({ sessionId: 'local', providerId: LOCAL_AGENT_HOST_PROVIDER_ID }), + URI.file('/other-project'), + ); + const pendingAfterOtherFolder = harness._preferredDevContainerFolderUri?.toString(); + applyPreferredDevContainer.call( + harness, + upcastPartial({ sessionId: 'local', providerId: LOCAL_AGENT_HOST_PROVIDER_ID }), + folder, + ); + + assert.deepStrictEqual({ + preferred, + pendingAfterOtherProvider, + pendingAfterOtherFolder, + pendingAfterMatch: harness._preferredDevContainerFolderUri, + }, { + preferred: ['local'], + pendingAfterOtherProvider: folder.toString(), + pendingAfterOtherFolder: folder.toString(), + pendingAfterMatch: undefined, + }); }); test('cancels an in-flight creation when a newer one starts', async () => { diff --git a/src/vs/sessions/services/sessions/browser/sessionsService.ts b/src/vs/sessions/services/sessions/browser/sessionsService.ts index 4acfc390376f93..fce727cd2d9bdf 100644 --- a/src/vs/sessions/services/sessions/browser/sessionsService.ts +++ b/src/vs/sessions/services/sessions/browser/sessionsService.ts @@ -345,8 +345,8 @@ export class SessionsService extends Disposable implements ISessionsService { * Cancellation for the in-flight {@link restoreVisibleSessions}. Kept * separate from {@link _openSessionCts} so that additive new-session * operations (the new-chat composer eagerly creating a draft on startup) - * do not abort restoring the previously visible grid. Only an explicit - * navigation to a specific session cancels a restore. + * do not abort restoring the previously visible grid. Explicit navigation + * to a session, or a new-session handoff with `cancelRestore`, cancels it. */ private readonly _restoreCts = this._register(new MutableDisposable()); @@ -720,8 +720,8 @@ export class SessionsService extends Disposable implements ISessionsService { /** * Cancel an in-flight {@link restoreVisibleSessions}. Called when the user - * explicitly navigates to a specific session, so restore stops fighting - * the user's choice. Additive new-session operations do NOT call this. + * explicitly navigates to a session, including a new-session handoff that + * sets `cancelRestore`, so restore stops fighting the user's choice. */ private _cancelRestore(): void { // `cancel()` (not just `clear()`/dispose) so the in-flight restore's 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 9280fbd0ea1a73..72b0283bb567ba 100644 --- a/src/vs/workbench/contrib/chat/electron-browser/agentSessions/agentSessionsActions.ts +++ b/src/vs/workbench/contrib/chat/electron-browser/agentSessions/agentSessionsActions.ts @@ -66,11 +66,13 @@ function getLocalAgentsFolderUri(folderUri: URI | undefined): URI | undefined { async function openCurrentWorkspaceInAgentsWindow(accessor: ServicesAccessor, source: AgentsWindowOpenSource): Promise { const nativeHostService = accessor.get(INativeHostService); const workspaceContextService = accessor.get(IWorkspaceContextService); - const folderUri = getLocalAgentsFolderUri(workspaceContextService.getWorkspace().folders[0]?.uri); + const workspaceFolderUri = workspaceContextService.getWorkspace().folders[0]?.uri; + const folderUri = getLocalAgentsFolderUri(workspaceFolderUri); await nativeHostService.openAgentsWindow({ folderUri, source, - preferDevContainer: true, + preferDevContainer: workspaceFolderUri?.scheme === Schemas.vscodeRemote + && workspaceFolderUri.authority.startsWith(DEV_CONTAINER_REMOTE_AUTHORITY_PREFIX), }); } 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 382e9b805a5ee8..30cbb8689e077f 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 @@ -59,7 +59,7 @@ suite('OpenWorkspaceInAgentsWindowAction', () => { })), [{ folderUri: URI.file('/workspace').toString(), source: AgentsWindowOpenSource.TitleBar, - preferDevContainer: true, + preferDevContainer: false, }, { folderUri: hostFolderUri.toString(), source: AgentsWindowOpenSource.ChatTitleBar, From 5c7dabce3c6fc53f6cd916c7a0bb5c740cf1793b Mon Sep 17 00:00:00 2001 From: Christof Marti Date: Thu, 3 Sep 2026 17:00:38 +0200 Subject: [PATCH 3/8] Use options for Agents window handoff Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../native/electron-main/nativeHostMainService.ts | 7 ++++++- src/vs/platform/windows/electron-main/windows.ts | 9 ++++++++- .../platform/windows/electron-main/windowsMainService.ts | 8 ++++---- 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/src/vs/platform/native/electron-main/nativeHostMainService.ts b/src/vs/platform/native/electron-main/nativeHostMainService.ts index ee9391c77bfd92..70fc48d52a70c7 100644 --- a/src/vs/platform/native/electron-main/nativeHostMainService.ts +++ b/src/vs/platform/native/electron-main/nativeHostMainService.ts @@ -319,7 +319,12 @@ export class NativeHostMainService extends Disposable implements INativeHostMain context: OpenContext.API, contextWindowId: windowId, cli: this.environmentMainService.args, - }, options?.folderUri ? URI.revive(options.folderUri) : undefined, options?.sessionResource ? URI.revive(options.sessionResource) : undefined, options?.source, options?.preferDevContainer); + }, { + folderUri: options?.folderUri ? URI.revive(options.folderUri) : undefined, + sessionResource: options?.sessionResource ? URI.revive(options.sessionResource) : undefined, + source: options?.source, + preferDevContainer: options?.preferDevContainer, + }); if (windows.length > 0) { windows[0].focus(); } diff --git a/src/vs/platform/windows/electron-main/windows.ts b/src/vs/platform/windows/electron-main/windows.ts index 20ed11a74000e4..15d7a48f5eb231 100644 --- a/src/vs/platform/windows/electron-main/windows.ts +++ b/src/vs/platform/windows/electron-main/windows.ts @@ -22,6 +22,13 @@ import { ICodeWindow, IWindowState, WindowMode, defaultWindowState } from '../.. export const IWindowsMainService = createDecorator('windowsMainService'); +export interface IOpenAgentsWindowMainOptions { + readonly folderUri?: URI; + readonly sessionResource?: URI; + readonly source?: AgentsWindowOpenSource; + readonly preferDevContainer?: boolean; +} + export interface IWindowsMainService { readonly _serviceBrand: undefined; @@ -41,7 +48,7 @@ export interface IWindowsMainService { openExtensionDevelopmentHostWindow(extensionDevelopmentPath: string[], openConfig: IOpenConfiguration): Promise; openExistingWindow(window: ICodeWindow, openConfig: IOpenConfiguration): void; - openAgentsWindow(openConfig: IOpenConfiguration, folderUri?: URI, sessionResource?: URI, source?: AgentsWindowOpenSource, preferDevContainer?: boolean): Promise; + openAgentsWindow(openConfig: IOpenConfiguration, options?: IOpenAgentsWindowMainOptions): Promise; sendToFocused(channel: string, ...args: unknown[]): void; sendToOpeningWindow(channel: string, ...args: unknown[]): void; diff --git a/src/vs/platform/windows/electron-main/windowsMainService.ts b/src/vs/platform/windows/electron-main/windowsMainService.ts index 3e6957e60d2604..1ac0d9c37c1c90 100644 --- a/src/vs/platform/windows/electron-main/windowsMainService.ts +++ b/src/vs/platform/windows/electron-main/windowsMainService.ts @@ -39,7 +39,7 @@ import { getRemoteAuthority } from '../../remote/common/remoteHosts.js'; import { IStateService } from '../../state/node/state.js'; import { AgentsWindowOpenSource, IAddRemoveFoldersRequest, INativeOpenFileRequest, INativeWindowConfiguration, IOpenEmptyWindowOptions, IPath, IPathsToWaitFor, isFileToOpen, isFolderToOpen, isWorkspaceToOpen, IWindowOpenable, IWindowSettings } from '../../window/common/window.js'; import { CodeWindow } from './windowImpl.js'; -import { IOpenConfiguration, IOpenEmptyConfiguration, IWindowsCountChangedEvent, IWindowsMainService, OpenContext, getLastFocused } from './windows.js'; +import { IOpenAgentsWindowMainOptions, IOpenConfiguration, IOpenEmptyConfiguration, IWindowsCountChangedEvent, IWindowsMainService, OpenContext, getLastFocused } from './windows.js'; import { findWindowOnExtensionDevelopmentPath, findWindowOnFile, findWindowOnWorkspaceOrFolder } from './windowsFinder.js'; import { IWindowState, WindowsStateHandler } from './windowsStateHandler.js'; import { IRecent } from '../../workspaces/common/workspaces.js'; @@ -292,7 +292,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic this.handleChatRequest(openConfig, [window]); } - async openAgentsWindow(openConfig: IOpenConfiguration, folderUri?: URI, sessionResource?: URI, source?: AgentsWindowOpenSource, preferDevContainer?: boolean): Promise { + async openAgentsWindow(openConfig: IOpenConfiguration, options?: IOpenAgentsWindowMainOptions): Promise { this.logService.trace('windowsManager#openAgentsWindow'); // Open in a new browser window with the agent sessions workspace @@ -303,8 +303,8 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic // them (folder → open session) so the session-open doesn't race the // folder-resolve. if (windows.length > 0) { - const openSource = source ?? (openConfig.cli.agents ? AgentsWindowOpenSource.CommandLine : AgentsWindowOpenSource.Unknown); - windows[0].sendWhenReady('vscode:selectAgentsFolder', CancellationToken.None, folderUri?.toJSON(), sessionResource?.toJSON(), openSource, preferDevContainer); + const openSource = options?.source ?? (openConfig.cli.agents ? AgentsWindowOpenSource.CommandLine : AgentsWindowOpenSource.Unknown); + windows[0].sendWhenReady('vscode:selectAgentsFolder', CancellationToken.None, options?.folderUri?.toJSON(), options?.sessionResource?.toJSON(), openSource, options?.preferDevContainer); } return windows; From 02fa4f78871bd08351dbd2049baf46b07e13322a Mon Sep 17 00:00:00 2001 From: Christof Marti Date: Thu, 3 Sep 2026 17:03:46 +0200 Subject: [PATCH 4/8] Use options for Agents window IPC Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/vs/platform/window/common/window.ts | 7 +++++++ .../windows/electron-main/windowsMainService.ts | 10 ++++++++-- .../chat/electron-browser/chat.contribution.ts | 13 +++++++------ 3 files changed, 22 insertions(+), 8 deletions(-) diff --git a/src/vs/platform/window/common/window.ts b/src/vs/platform/window/common/window.ts index d073d011d56c24..166e70e1c648cb 100644 --- a/src/vs/platform/window/common/window.ts +++ b/src/vs/platform/window/common/window.ts @@ -131,6 +131,13 @@ export function isAgentsWindowOpenSource(value: unknown): value is AgentsWindowO } } +export interface IAgentsWindowOpenRequest { + readonly folderUri?: UriComponents; + readonly sessionResource?: UriComponents; + readonly source: AgentsWindowOpenSource; + readonly preferDevContainer?: boolean; +} + export type IWindowOpenable = IWorkspaceToOpen | IFolderToOpen | IFileToOpen; export interface IBaseWindowOpenable { diff --git a/src/vs/platform/windows/electron-main/windowsMainService.ts b/src/vs/platform/windows/electron-main/windowsMainService.ts index 1ac0d9c37c1c90..14aed4dbf7ccb8 100644 --- a/src/vs/platform/windows/electron-main/windowsMainService.ts +++ b/src/vs/platform/windows/electron-main/windowsMainService.ts @@ -37,7 +37,7 @@ import product from '../../product/common/product.js'; import { IProtocolMainService } from '../../protocol/electron-main/protocol.js'; import { getRemoteAuthority } from '../../remote/common/remoteHosts.js'; import { IStateService } from '../../state/node/state.js'; -import { AgentsWindowOpenSource, IAddRemoveFoldersRequest, INativeOpenFileRequest, INativeWindowConfiguration, IOpenEmptyWindowOptions, IPath, IPathsToWaitFor, isFileToOpen, isFolderToOpen, isWorkspaceToOpen, IWindowOpenable, IWindowSettings } from '../../window/common/window.js'; +import { AgentsWindowOpenSource, IAddRemoveFoldersRequest, IAgentsWindowOpenRequest, INativeOpenFileRequest, INativeWindowConfiguration, IOpenEmptyWindowOptions, IPath, IPathsToWaitFor, isFileToOpen, isFolderToOpen, isWorkspaceToOpen, IWindowOpenable, IWindowSettings } from '../../window/common/window.js'; import { CodeWindow } from './windowImpl.js'; import { IOpenAgentsWindowMainOptions, IOpenConfiguration, IOpenEmptyConfiguration, IWindowsCountChangedEvent, IWindowsMainService, OpenContext, getLastFocused } from './windows.js'; import { findWindowOnExtensionDevelopmentPath, findWindowOnFile, findWindowOnWorkspaceOrFolder } from './windowsFinder.js'; @@ -304,7 +304,13 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic // folder-resolve. if (windows.length > 0) { const openSource = options?.source ?? (openConfig.cli.agents ? AgentsWindowOpenSource.CommandLine : AgentsWindowOpenSource.Unknown); - windows[0].sendWhenReady('vscode:selectAgentsFolder', CancellationToken.None, options?.folderUri?.toJSON(), options?.sessionResource?.toJSON(), openSource, options?.preferDevContainer); + const request: IAgentsWindowOpenRequest = { + folderUri: options?.folderUri?.toJSON(), + sessionResource: options?.sessionResource?.toJSON(), + source: openSource, + preferDevContainer: options?.preferDevContainer, + }; + windows[0].sendWhenReady('vscode:selectAgentsFolder', CancellationToken.None, request); } return windows; 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 ace9b58bafc41b..51833f321cc898 100644 --- a/src/vs/sessions/contrib/chat/electron-browser/chat.contribution.ts +++ b/src/vs/sessions/contrib/chat/electron-browser/chat.contribution.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { ipcRenderer } from '../../../../base/parts/sandbox/electron-browser/globals.js'; -import { URI, UriComponents } from '../../../../base/common/uri.js'; +import { URI } from '../../../../base/common/uri.js'; import { Disposable, DisposableStore, MutableDisposable } from '../../../../base/common/lifecycle.js'; import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; import { IAgentHostByokLmHandler } from '../../../../platform/agentHost/common/agentHostByokLm.js'; @@ -22,7 +22,7 @@ import { ISessionsSetUpService } from '../../../browser/sessionsSetUpService.js' import { ISessionsPartService } from '../../../services/sessions/browser/sessionsPartService.js'; import { SessionStatus } from '../../../services/sessions/common/session.js'; import { SessionsCopilotConfigSlashSubmitHandlerContribution } from '../browser/copilotConfigSlashSubmitHandler.js'; -import { AgentsWindowOpenSource, isAgentsWindowOpenSource } from '../../../../platform/window/common/window.js'; +import { AgentsWindowOpenSource, IAgentsWindowOpenRequest, isAgentsWindowOpenSource } from '../../../../platform/window/common/window.js'; import { IStorageService, StorageScope } from '../../../../platform/storage/common/storage.js'; import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; import { TOTAL_SESSIONS_KEY } from '../../sessions/browser/sessionsLifecycleTracker.js'; @@ -52,10 +52,11 @@ class SelectAgentsFolderContribution extends Disposable implements IWorkbenchCon ) { super(); const handleSelectAgentsFolder = (_: unknown, ...args: unknown[]) => { - const folderUri = args[0] ? URI.revive(args[0] as UriComponents) : undefined; - const sessionResource = args[1] ? URI.revive(args[1] as UriComponents) : undefined; - const source = isAgentsWindowOpenSource(args[2]) ? args[2] : AgentsWindowOpenSource.Unknown; - const preferDevContainer = shouldPreferDevContainer(args[3], this.configurationService); + const request = args[0] as IAgentsWindowOpenRequest | undefined; + const folderUri = request?.folderUri ? URI.revive(request.folderUri) : undefined; + const sessionResource = request?.sessionResource ? URI.revive(request.sessionResource) : undefined; + const source = isAgentsWindowOpenSource(request?.source) ? request.source : AgentsWindowOpenSource.Unknown; + const preferDevContainer = shouldPreferDevContainer(request?.preferDevContainer, this.configurationService); this.logService.info(`[AgentsHandoff] IPC received: folderUri=${folderUri?.toString() ?? '(none)'} sessionResource=${sessionResource?.toString() ?? '(none)'}`); this._startWindowOpenTelemetry(source); From 335f0dc6f84347d4acfd580445e5d4352d106a82 Mon Sep 17 00:00:00 2001 From: Christof Marti Date: Thu, 3 Sep 2026 17:25:55 +0200 Subject: [PATCH 5/8] Parse Dev Container handoff in Agents Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/vs/platform/native/common/native.ts | 1 - .../electron-main/nativeHostMainService.ts | 1 - src/vs/platform/window/common/window.ts | 1 - .../platform/windows/electron-main/windows.ts | 1 - .../electron-main/windowsMainService.ts | 1 - .../chat/browser/agentsWindowOpenIntent.ts | 31 +++++++++++++-- .../electron-browser/chat.contribution.ts | 6 +-- .../browser/agentsWindowOpenIntent.test.ts | 39 ++++++++++++++----- .../agentSessions/agentSessionsActions.ts | 27 +------------ .../agentSessionsActions.test.ts | 5 +-- 10 files changed, 63 insertions(+), 50 deletions(-) diff --git a/src/vs/platform/native/common/native.ts b/src/vs/platform/native/common/native.ts index f9edffda183bff..1c62eb7cfef77d 100644 --- a/src/vs/platform/native/common/native.ts +++ b/src/vs/platform/native/common/native.ts @@ -84,7 +84,6 @@ export interface IOpenAgentsWindowOptions { readonly folderUri?: UriComponents; readonly sessionResource?: UriComponents; readonly source?: AgentsWindowOpenSource; - readonly preferDevContainer?: boolean; } export interface ICPUProperties { diff --git a/src/vs/platform/native/electron-main/nativeHostMainService.ts b/src/vs/platform/native/electron-main/nativeHostMainService.ts index 70fc48d52a70c7..64e8e24a8aaad8 100644 --- a/src/vs/platform/native/electron-main/nativeHostMainService.ts +++ b/src/vs/platform/native/electron-main/nativeHostMainService.ts @@ -323,7 +323,6 @@ export class NativeHostMainService extends Disposable implements INativeHostMain folderUri: options?.folderUri ? URI.revive(options.folderUri) : undefined, sessionResource: options?.sessionResource ? URI.revive(options.sessionResource) : undefined, source: options?.source, - preferDevContainer: options?.preferDevContainer, }); if (windows.length > 0) { windows[0].focus(); diff --git a/src/vs/platform/window/common/window.ts b/src/vs/platform/window/common/window.ts index 166e70e1c648cb..2ae354dba6704e 100644 --- a/src/vs/platform/window/common/window.ts +++ b/src/vs/platform/window/common/window.ts @@ -135,7 +135,6 @@ export interface IAgentsWindowOpenRequest { readonly folderUri?: UriComponents; readonly sessionResource?: UriComponents; readonly source: AgentsWindowOpenSource; - readonly preferDevContainer?: boolean; } export type IWindowOpenable = IWorkspaceToOpen | IFolderToOpen | IFileToOpen; diff --git a/src/vs/platform/windows/electron-main/windows.ts b/src/vs/platform/windows/electron-main/windows.ts index 15d7a48f5eb231..fe02370bba7ed1 100644 --- a/src/vs/platform/windows/electron-main/windows.ts +++ b/src/vs/platform/windows/electron-main/windows.ts @@ -26,7 +26,6 @@ export interface IOpenAgentsWindowMainOptions { readonly folderUri?: URI; readonly sessionResource?: URI; readonly source?: AgentsWindowOpenSource; - readonly preferDevContainer?: boolean; } export interface IWindowsMainService { diff --git a/src/vs/platform/windows/electron-main/windowsMainService.ts b/src/vs/platform/windows/electron-main/windowsMainService.ts index 14aed4dbf7ccb8..c273eb4759121e 100644 --- a/src/vs/platform/windows/electron-main/windowsMainService.ts +++ b/src/vs/platform/windows/electron-main/windowsMainService.ts @@ -308,7 +308,6 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic folderUri: options?.folderUri?.toJSON(), sessionResource: options?.sessionResource?.toJSON(), source: openSource, - preferDevContainer: options?.preferDevContainer, }; windows[0].sendWhenReady('vscode:selectAgentsFolder', CancellationToken.None, request); } diff --git a/src/vs/sessions/contrib/chat/browser/agentsWindowOpenIntent.ts b/src/vs/sessions/contrib/chat/browser/agentsWindowOpenIntent.ts index 00dd2e9610ba97..bdb64ba2424e4f 100644 --- a/src/vs/sessions/contrib/chat/browser/agentsWindowOpenIntent.ts +++ b/src/vs/sessions/contrib/chat/browser/agentsWindowOpenIntent.ts @@ -3,10 +3,35 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { decodeHex } from '../../../../base/common/buffer.js'; +import { Schemas } from '../../../../base/common/network.js'; +import { URI } from '../../../../base/common/uri.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { DevContainerAgentHostEnabledSettingId } from '../../../common/devContainerAgentHostService.js'; -export function shouldPreferDevContainer(requested: unknown, configurationService: IConfigurationService): boolean { - return requested === true - && configurationService.getValue(DevContainerAgentHostEnabledSettingId) === true; +const DEV_CONTAINER_REMOTE_AUTHORITY_PREFIX = 'dev-container+'; + +export interface IAgentsWindowFolderIntent { + readonly folderUri: URI | undefined; + readonly preferDevContainer: boolean; +} + +export function resolveAgentsWindowFolderIntent(workspaceUri: URI | undefined, configurationService: IConfigurationService): IAgentsWindowFolderIntent { + if (workspaceUri?.scheme === Schemas.file) { + return { folderUri: workspaceUri, preferDevContainer: false }; + } + if (workspaceUri?.scheme !== Schemas.vscodeRemote || !workspaceUri.authority.startsWith(DEV_CONTAINER_REMOTE_AUTHORITY_PREFIX)) { + return { folderUri: undefined, preferDevContainer: false }; + } + try { + return { + folderUri: URI.file(decodeHex(workspaceUri.authority.slice(DEV_CONTAINER_REMOTE_AUTHORITY_PREFIX.length)).toString()), + preferDevContainer: configurationService.getValue(DevContainerAgentHostEnabledSettingId) === true, + }; + } catch (error) { + if (error instanceof SyntaxError) { + return { folderUri: undefined, preferDevContainer: false }; + } + throw error; + } } 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 51833f321cc898..1cb95e6dfe2a1a 100644 --- a/src/vs/sessions/contrib/chat/electron-browser/chat.contribution.ts +++ b/src/vs/sessions/contrib/chat/electron-browser/chat.contribution.ts @@ -28,7 +28,7 @@ import { ITelemetryService } from '../../../../platform/telemetry/common/telemet import { TOTAL_SESSIONS_KEY } from '../../sessions/browser/sessionsLifecycleTracker.js'; import { ISessionsWindowOpenViewState, SessionsWindowOpenTelemetry, SessionsWindowSessionStartTelemetry } from '../../sessions/browser/sessionsWindowOpenTelemetry.js'; import { INewSessionComposerService, NewSessionWorkspacePreselectionSource } from '../browser/newSessionComposerService.js'; -import { shouldPreferDevContainer } from '../browser/agentsWindowOpenIntent.js'; +import { resolveAgentsWindowFolderIntent } from '../browser/agentsWindowOpenIntent.js'; class SelectAgentsFolderContribution extends Disposable implements IWorkbenchContribution { @@ -53,10 +53,10 @@ class SelectAgentsFolderContribution extends Disposable implements IWorkbenchCon super(); const handleSelectAgentsFolder = (_: unknown, ...args: unknown[]) => { const request = args[0] as IAgentsWindowOpenRequest | undefined; - const folderUri = request?.folderUri ? URI.revive(request.folderUri) : undefined; + const workspaceUri = request?.folderUri ? URI.revive(request.folderUri) : undefined; + const { folderUri, preferDevContainer } = resolveAgentsWindowFolderIntent(workspaceUri, this.configurationService); const sessionResource = request?.sessionResource ? URI.revive(request.sessionResource) : undefined; const source = isAgentsWindowOpenSource(request?.source) ? request.source : AgentsWindowOpenSource.Unknown; - const preferDevContainer = shouldPreferDevContainer(request?.preferDevContainer, this.configurationService); this.logService.info(`[AgentsHandoff] IPC received: folderUri=${folderUri?.toString() ?? '(none)'} sessionResource=${sessionResource?.toString() ?? '(none)'}`); this._startWindowOpenTelemetry(source); diff --git a/src/vs/sessions/contrib/chat/test/browser/agentsWindowOpenIntent.test.ts b/src/vs/sessions/contrib/chat/test/browser/agentsWindowOpenIntent.test.ts index 2b30d74029c160..cb9c4254ab1bf4 100644 --- a/src/vs/sessions/contrib/chat/test/browser/agentsWindowOpenIntent.test.ts +++ b/src/vs/sessions/contrib/chat/test/browser/agentsWindowOpenIntent.test.ts @@ -4,29 +4,48 @@ *--------------------------------------------------------------------------------------------*/ import assert from 'assert'; +import { encodeHex, VSBuffer } from '../../../../../base/common/buffer.js'; +import { Schemas } from '../../../../../base/common/network.js'; +import { URI } from '../../../../../base/common/uri.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; import { TestConfigurationService } from '../../../../../platform/configuration/test/common/testConfigurationService.js'; import { DevContainerAgentHostEnabledSettingId } from '../../../../common/devContainerAgentHostService.js'; -import { shouldPreferDevContainer } from '../../browser/agentsWindowOpenIntent.js'; +import { resolveAgentsWindowFolderIntent } from '../../browser/agentsWindowOpenIntent.js'; suite('Agents Window open intent', () => { ensureNoDisposablesAreLeakedInTestSuite(); - test('gates the Dev Container preference on the Agents Window setting', () => { + test('resolves local and Dev Container editor workspaces', () => { const configurationService = (enabled: boolean) => new TestConfigurationService({ [DevContainerAgentHostEnabledSettingId]: enabled, }); + const localFolder = URI.file('/workspace'); + const hostFolder = URI.file('/host/workspace'); + const devContainerFolder = URI.from({ + scheme: Schemas.vscodeRemote, + authority: `dev-container+${encodeHex(VSBuffer.fromString(hostFolder.fsPath))}`, + path: '/workspaces/project', + }); + const resolve = (uri: URI | undefined, enabled: boolean) => { + const result = resolveAgentsWindowFolderIntent(uri, configurationService(enabled)); + return { + folderUri: result.folderUri?.toString(), + preferDevContainer: result.preferDevContainer, + }; + }; assert.deepStrictEqual({ - requestedAndEnabled: shouldPreferDevContainer(true, configurationService(true)), - requestedAndDisabled: shouldPreferDevContainer(true, configurationService(false)), - notRequestedAndEnabled: shouldPreferDevContainer(false, configurationService(true)), - invalidRequestAndEnabled: shouldPreferDevContainer('true', configurationService(true)), + local: resolve(localFolder, true), + devContainerEnabled: resolve(devContainerFolder, true), + devContainerDisabled: resolve(devContainerFolder, false), + otherRemote: resolve(URI.parse('vscode-remote://ssh-remote+host/workspace'), true), + invalidDevContainer: resolve(URI.parse('vscode-remote://dev-container+invalid/workspace'), true), }, { - requestedAndEnabled: true, - requestedAndDisabled: false, - notRequestedAndEnabled: false, - invalidRequestAndEnabled: false, + local: { folderUri: localFolder.toString(), preferDevContainer: false }, + devContainerEnabled: { folderUri: hostFolder.toString(), preferDevContainer: true }, + devContainerDisabled: { folderUri: hostFolder.toString(), preferDevContainer: false }, + otherRemote: { folderUri: undefined, preferDevContainer: false }, + invalidDevContainer: { folderUri: undefined, preferDevContainer: false }, }); }); }); 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 72b0283bb567ba..d3c7f201ffdaed 100644 --- a/src/vs/workbench/contrib/chat/electron-browser/agentSessions/agentSessionsActions.ts +++ b/src/vs/workbench/contrib/chat/electron-browser/agentSessions/agentSessionsActions.ts @@ -7,7 +7,6 @@ import { $, append } from '../../../../../base/browser/dom.js'; import { BaseActionViewItem, IBaseActionViewItemOptions } from '../../../../../base/browser/ui/actionbar/actionViewItems.js'; import { getDefaultHoverDelegate } from '../../../../../base/browser/ui/hover/hoverDelegateFactory.js'; import { IAction } from '../../../../../base/common/actions.js'; -import { decodeHex } from '../../../../../base/common/buffer.js'; import { Disposable } from '../../../../../base/common/lifecycle.js'; import { ServicesAccessor } from '../../../../../editor/browser/editorExtensions.js'; import { localize, localize2 } from '../../../../../nls.js'; @@ -44,35 +43,13 @@ import { AgentsWindowOpenSource, isAgentsWindowOpenSource } from '../../../../.. const OPEN_WORKSPACE_IN_AGENTS_WINDOW_TITLE = localize2('openWorkspaceInAgentsWindow', "Open in Agents"); const OPEN_WORKSPACE_IN_AGENTS_WINDOW_CHAT_TITLE_COMMAND_ID = 'workbench.action.chat.openWorkspaceInAgentsWindow.chatTitle'; const OPEN_WORKSPACE_IN_AGENTS_WINDOW_TITLE_BAR_COMMAND_ID = 'workbench.action.chat.openWorkspaceInAgentsWindow.titleBar'; -const DEV_CONTAINER_REMOTE_AUTHORITY_PREFIX = 'dev-container+'; - -function getLocalAgentsFolderUri(folderUri: URI | undefined): URI | undefined { - if (folderUri?.scheme === Schemas.file) { - return folderUri; - } - if (folderUri?.scheme !== Schemas.vscodeRemote || !folderUri.authority.startsWith(DEV_CONTAINER_REMOTE_AUTHORITY_PREFIX)) { - return undefined; - } - try { - return URI.file(decodeHex(folderUri.authority.slice(DEV_CONTAINER_REMOTE_AUTHORITY_PREFIX.length)).toString()); - } catch (error) { - if (error instanceof SyntaxError) { - return undefined; - } - throw error; - } -} async function openCurrentWorkspaceInAgentsWindow(accessor: ServicesAccessor, source: AgentsWindowOpenSource): Promise { const nativeHostService = accessor.get(INativeHostService); const workspaceContextService = accessor.get(IWorkspaceContextService); - const workspaceFolderUri = workspaceContextService.getWorkspace().folders[0]?.uri; - const folderUri = getLocalAgentsFolderUri(workspaceFolderUri); await nativeHostService.openAgentsWindow({ - folderUri, + folderUri: workspaceContextService.getWorkspace().folders[0]?.uri, source, - preferDevContainer: workspaceFolderUri?.scheme === Schemas.vscodeRemote - && workspaceFolderUri.authority.startsWith(DEV_CONTAINER_REMOTE_AUTHORITY_PREFIX), }); } @@ -178,7 +155,7 @@ export class OpenAgentsWindowAction extends Action2 { }); } - async run(accessor: ServicesAccessor, args?: { folderUri?: UriComponents; sessionResource?: UriComponents; source?: AgentsWindowOpenSource; preferDevContainer?: boolean }) { + async run(accessor: ServicesAccessor, args?: { folderUri?: UriComponents; sessionResource?: UriComponents; source?: AgentsWindowOpenSource }) { const nativeHostService = accessor.get(INativeHostService); await nativeHostService.openAgentsWindow({ ...args, source: args?.source ?? AgentsWindowOpenSource.CommandPalette }); } 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 30cbb8689e077f..ffaea1eb0df2ff 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 @@ -55,15 +55,12 @@ suite('OpenWorkspaceInAgentsWindowAction', () => { assert.deepStrictEqual(calls.map(call => ({ folderUri: URI.revive(call.folderUri)?.toString(), source: call.source, - preferDevContainer: call.preferDevContainer, })), [{ folderUri: URI.file('/workspace').toString(), source: AgentsWindowOpenSource.TitleBar, - preferDevContainer: false, }, { - folderUri: hostFolderUri.toString(), + folderUri: workspaceFolderUri.toString(), source: AgentsWindowOpenSource.ChatTitleBar, - preferDevContainer: true, }]); }); }); From beb77812248e807d8351cc17e540a21d72f2cb3b Mon Sep 17 00:00:00 2001 From: Christof Marti Date: Thu, 3 Sep 2026 22:43:39 +0200 Subject: [PATCH 6/8] Keep existing Agents window handoff signatures Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../native/electron-main/nativeHostMainService.ts | 6 +----- src/vs/platform/window/common/window.ts | 6 ------ src/vs/platform/windows/electron-main/windows.ts | 8 +------- .../windows/electron-main/windowsMainService.ts | 15 +++++---------- .../chat/electron-browser/chat.contribution.ts | 11 +++++------ 5 files changed, 12 insertions(+), 34 deletions(-) diff --git a/src/vs/platform/native/electron-main/nativeHostMainService.ts b/src/vs/platform/native/electron-main/nativeHostMainService.ts index 64e8e24a8aaad8..b1fb89e25a3acb 100644 --- a/src/vs/platform/native/electron-main/nativeHostMainService.ts +++ b/src/vs/platform/native/electron-main/nativeHostMainService.ts @@ -319,11 +319,7 @@ export class NativeHostMainService extends Disposable implements INativeHostMain context: OpenContext.API, contextWindowId: windowId, cli: this.environmentMainService.args, - }, { - folderUri: options?.folderUri ? URI.revive(options.folderUri) : undefined, - sessionResource: options?.sessionResource ? URI.revive(options.sessionResource) : undefined, - source: options?.source, - }); + }, options?.folderUri ? URI.revive(options.folderUri) : undefined, options?.sessionResource ? URI.revive(options.sessionResource) : undefined, options?.source); if (windows.length > 0) { windows[0].focus(); } diff --git a/src/vs/platform/window/common/window.ts b/src/vs/platform/window/common/window.ts index 2ae354dba6704e..d073d011d56c24 100644 --- a/src/vs/platform/window/common/window.ts +++ b/src/vs/platform/window/common/window.ts @@ -131,12 +131,6 @@ export function isAgentsWindowOpenSource(value: unknown): value is AgentsWindowO } } -export interface IAgentsWindowOpenRequest { - readonly folderUri?: UriComponents; - readonly sessionResource?: UriComponents; - readonly source: AgentsWindowOpenSource; -} - export type IWindowOpenable = IWorkspaceToOpen | IFolderToOpen | IFileToOpen; export interface IBaseWindowOpenable { diff --git a/src/vs/platform/windows/electron-main/windows.ts b/src/vs/platform/windows/electron-main/windows.ts index fe02370bba7ed1..7c4f02efdfa374 100644 --- a/src/vs/platform/windows/electron-main/windows.ts +++ b/src/vs/platform/windows/electron-main/windows.ts @@ -22,12 +22,6 @@ import { ICodeWindow, IWindowState, WindowMode, defaultWindowState } from '../.. export const IWindowsMainService = createDecorator('windowsMainService'); -export interface IOpenAgentsWindowMainOptions { - readonly folderUri?: URI; - readonly sessionResource?: URI; - readonly source?: AgentsWindowOpenSource; -} - export interface IWindowsMainService { readonly _serviceBrand: undefined; @@ -47,7 +41,7 @@ export interface IWindowsMainService { openExtensionDevelopmentHostWindow(extensionDevelopmentPath: string[], openConfig: IOpenConfiguration): Promise; openExistingWindow(window: ICodeWindow, openConfig: IOpenConfiguration): void; - openAgentsWindow(openConfig: IOpenConfiguration, options?: IOpenAgentsWindowMainOptions): Promise; + openAgentsWindow(openConfig: IOpenConfiguration, folderUri?: URI, sessionResource?: URI, source?: AgentsWindowOpenSource): Promise; sendToFocused(channel: string, ...args: unknown[]): void; sendToOpeningWindow(channel: string, ...args: unknown[]): void; diff --git a/src/vs/platform/windows/electron-main/windowsMainService.ts b/src/vs/platform/windows/electron-main/windowsMainService.ts index c273eb4759121e..d8428b529b26fa 100644 --- a/src/vs/platform/windows/electron-main/windowsMainService.ts +++ b/src/vs/platform/windows/electron-main/windowsMainService.ts @@ -37,9 +37,9 @@ import product from '../../product/common/product.js'; import { IProtocolMainService } from '../../protocol/electron-main/protocol.js'; import { getRemoteAuthority } from '../../remote/common/remoteHosts.js'; import { IStateService } from '../../state/node/state.js'; -import { AgentsWindowOpenSource, IAddRemoveFoldersRequest, IAgentsWindowOpenRequest, INativeOpenFileRequest, INativeWindowConfiguration, IOpenEmptyWindowOptions, IPath, IPathsToWaitFor, isFileToOpen, isFolderToOpen, isWorkspaceToOpen, IWindowOpenable, IWindowSettings } from '../../window/common/window.js'; +import { AgentsWindowOpenSource, IAddRemoveFoldersRequest, INativeOpenFileRequest, INativeWindowConfiguration, IOpenEmptyWindowOptions, IPath, IPathsToWaitFor, isFileToOpen, isFolderToOpen, isWorkspaceToOpen, IWindowOpenable, IWindowSettings } from '../../window/common/window.js'; import { CodeWindow } from './windowImpl.js'; -import { IOpenAgentsWindowMainOptions, IOpenConfiguration, IOpenEmptyConfiguration, IWindowsCountChangedEvent, IWindowsMainService, OpenContext, getLastFocused } from './windows.js'; +import { IOpenConfiguration, IOpenEmptyConfiguration, IWindowsCountChangedEvent, IWindowsMainService, OpenContext, getLastFocused } from './windows.js'; import { findWindowOnExtensionDevelopmentPath, findWindowOnFile, findWindowOnWorkspaceOrFolder } from './windowsFinder.js'; import { IWindowState, WindowsStateHandler } from './windowsStateHandler.js'; import { IRecent } from '../../workspaces/common/workspaces.js'; @@ -292,7 +292,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic this.handleChatRequest(openConfig, [window]); } - async openAgentsWindow(openConfig: IOpenConfiguration, options?: IOpenAgentsWindowMainOptions): Promise { + async openAgentsWindow(openConfig: IOpenConfiguration, folderUri?: URI, sessionResource?: URI, source?: AgentsWindowOpenSource): Promise { this.logService.trace('windowsManager#openAgentsWindow'); // Open in a new browser window with the agent sessions workspace @@ -303,13 +303,8 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic // them (folder → open session) so the session-open doesn't race the // folder-resolve. if (windows.length > 0) { - const openSource = options?.source ?? (openConfig.cli.agents ? AgentsWindowOpenSource.CommandLine : AgentsWindowOpenSource.Unknown); - const request: IAgentsWindowOpenRequest = { - folderUri: options?.folderUri?.toJSON(), - sessionResource: options?.sessionResource?.toJSON(), - source: openSource, - }; - windows[0].sendWhenReady('vscode:selectAgentsFolder', CancellationToken.None, request); + const openSource = source ?? (openConfig.cli.agents ? AgentsWindowOpenSource.CommandLine : AgentsWindowOpenSource.Unknown); + windows[0].sendWhenReady('vscode:selectAgentsFolder', CancellationToken.None, folderUri?.toJSON(), sessionResource?.toJSON(), openSource); } return windows; 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 1cb95e6dfe2a1a..4cd4e3d416126a 100644 --- a/src/vs/sessions/contrib/chat/electron-browser/chat.contribution.ts +++ b/src/vs/sessions/contrib/chat/electron-browser/chat.contribution.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { ipcRenderer } from '../../../../base/parts/sandbox/electron-browser/globals.js'; -import { URI } from '../../../../base/common/uri.js'; +import { URI, UriComponents } from '../../../../base/common/uri.js'; import { Disposable, DisposableStore, MutableDisposable } from '../../../../base/common/lifecycle.js'; import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; import { IAgentHostByokLmHandler } from '../../../../platform/agentHost/common/agentHostByokLm.js'; @@ -22,7 +22,7 @@ import { ISessionsSetUpService } from '../../../browser/sessionsSetUpService.js' import { ISessionsPartService } from '../../../services/sessions/browser/sessionsPartService.js'; import { SessionStatus } from '../../../services/sessions/common/session.js'; import { SessionsCopilotConfigSlashSubmitHandlerContribution } from '../browser/copilotConfigSlashSubmitHandler.js'; -import { AgentsWindowOpenSource, IAgentsWindowOpenRequest, isAgentsWindowOpenSource } from '../../../../platform/window/common/window.js'; +import { AgentsWindowOpenSource, isAgentsWindowOpenSource } from '../../../../platform/window/common/window.js'; import { IStorageService, StorageScope } from '../../../../platform/storage/common/storage.js'; import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; import { TOTAL_SESSIONS_KEY } from '../../sessions/browser/sessionsLifecycleTracker.js'; @@ -52,11 +52,10 @@ class SelectAgentsFolderContribution extends Disposable implements IWorkbenchCon ) { super(); const handleSelectAgentsFolder = (_: unknown, ...args: unknown[]) => { - const request = args[0] as IAgentsWindowOpenRequest | undefined; - const workspaceUri = request?.folderUri ? URI.revive(request.folderUri) : undefined; + const workspaceUri = args[0] ? URI.revive(args[0] as UriComponents) : undefined; const { folderUri, preferDevContainer } = resolveAgentsWindowFolderIntent(workspaceUri, this.configurationService); - const sessionResource = request?.sessionResource ? URI.revive(request.sessionResource) : undefined; - const source = isAgentsWindowOpenSource(request?.source) ? request.source : AgentsWindowOpenSource.Unknown; + const sessionResource = args[1] ? URI.revive(args[1] as UriComponents) : undefined; + const source = isAgentsWindowOpenSource(args[2]) ? args[2] : AgentsWindowOpenSource.Unknown; this.logService.info(`[AgentsHandoff] IPC received: folderUri=${folderUri?.toString() ?? '(none)'} sessionResource=${sessionResource?.toString() ?? '(none)'}`); this._startWindowOpenTelemetry(source); From 03087747df891d46ea22692cd1593cb004102fce Mon Sep 17 00:00:00 2001 From: Christof Marti Date: Thu, 3 Sep 2026 22:47:20 +0200 Subject: [PATCH 7/8] Consolidate workspace selection options Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/vs/sessions/browser/parts/chatGroupView.ts | 6 +++--- src/vs/sessions/browser/parts/chatGroupsView.ts | 6 +++--- src/vs/sessions/browser/parts/chatView.ts | 7 ++++++- src/vs/sessions/browser/parts/sessionView.ts | 6 +++--- src/vs/sessions/contrib/chat/browser/chatView.ts | 6 +++--- src/vs/sessions/contrib/chat/browser/newChatWidget.ts | 6 +++--- .../contrib/chat/electron-browser/chat.contribution.ts | 5 ++++- .../remoteAgentHost/browser/remoteAgentHostActions.ts | 4 ++-- .../contrib/sessions/browser/views/sessionsViewActions.ts | 2 +- 9 files changed, 28 insertions(+), 20 deletions(-) diff --git a/src/vs/sessions/browser/parts/chatGroupView.ts b/src/vs/sessions/browser/parts/chatGroupView.ts index f66acdf1b6c42a..ee805b459fda53 100644 --- a/src/vs/sessions/browser/parts/chatGroupView.ts +++ b/src/vs/sessions/browser/parts/chatGroupView.ts @@ -22,7 +22,7 @@ import { ChatCompositeBar, IChatCompositeBarDelegate } from './chatCompositeBar. import { type IRemoteHostUnavailableEmptyStateContent, RemoteHostUnavailableEmptyState } from './remoteHostUnavailableEmptyState.js'; import { SessionRemoteConnection } from './sessionRemoteConnection.js'; import { ISessionReadOnlyBannerContent, SessionReadOnlyBanner } from './sessionReadOnlyBanner.js'; -import { AbstractChatView, ChatViewKind, IChatViewOptions } from './chatView.js'; +import { AbstractChatView, ChatViewKind, IChatViewOptions, ISelectWorkspaceOptions } from './chatView.js'; /** * The data + callbacks a {@link ChatGroupView} needs from its owning @@ -333,8 +333,8 @@ export class ChatGroupView extends Disposable implements ISerializableView { return this._currentView.value?.submitInput() ?? Promise.resolve(false); } - selectWorkspace(folderUri: URI, providerId?: string, options?: { readonly preferDevContainer?: boolean }): void { - this._currentView.value?.selectWorkspace(folderUri, providerId, options); + selectWorkspace(folderUri: URI, options?: ISelectWorkspaceOptions): void { + this._currentView.value?.selectWorkspace(folderUri, options); } prefillInput(text: string): void { diff --git a/src/vs/sessions/browser/parts/chatGroupsView.ts b/src/vs/sessions/browser/parts/chatGroupsView.ts index bde821df64ee0e..5c4ae5e0940434 100644 --- a/src/vs/sessions/browser/parts/chatGroupsView.ts +++ b/src/vs/sessions/browser/parts/chatGroupsView.ts @@ -19,7 +19,7 @@ import { agentsPanelBorder } from '../../common/theme.js'; import { IChat } from '../../services/sessions/common/session.js'; import { IActiveSession } from '../../services/sessions/common/sessionsManagement.js'; import { ISessionsService } from '../../services/sessions/browser/sessionsService.js'; -import { IChatViewOptions } from './chatView.js'; +import { IChatViewOptions, ISelectWorkspaceOptions } from './chatView.js'; import { ChatGroupView, IChatGroupContext } from './chatGroupView.js'; import { ChatDropZone, ChatGroupDropTarget, IChatGroupDropTargetDelegate } from './chatGroupDropTarget.js'; import { IDraggedSessionChat, isSessionChatDrag } from '../dnd.js'; @@ -792,8 +792,8 @@ export class ChatGroupsView extends Themable { return this._activeGroup?.view.submitInput() ?? Promise.resolve(false); } - selectWorkspace(folderUri: URI, providerId?: string, options?: { readonly preferDevContainer?: boolean }): void { - this._activeGroup?.view.selectWorkspace(folderUri, providerId, options); + selectWorkspace(folderUri: URI, options?: ISelectWorkspaceOptions): void { + this._activeGroup?.view.selectWorkspace(folderUri, options); } prefillInput(text: string): void { diff --git a/src/vs/sessions/browser/parts/chatView.ts b/src/vs/sessions/browser/parts/chatView.ts index 7b78551dcf36d4..f1638f696d91c3 100644 --- a/src/vs/sessions/browser/parts/chatView.ts +++ b/src/vs/sessions/browser/parts/chatView.ts @@ -26,6 +26,11 @@ export type ChatViewKind = 'newSession' | 'newChatInSession' | 'chat'; export interface IChatViewOptions { } +export interface ISelectWorkspaceOptions { + readonly providerId?: string; + readonly preferDevContainer?: boolean; +} + /** * Base class for a view that lives inside the {@link SessionsPart} internal grid. * Each instance occupies a single grid leaf. Subclasses populate {@link element} @@ -81,7 +86,7 @@ export abstract class AbstractChatView extends Disposable implements ISerializab * implementation is a no-op; subclasses that host a workspace picker * (e.g. `NewChatView`) override this to forward the selection. */ - selectWorkspace(_folderUri: URI, _providerId?: string, _options?: { readonly preferDevContainer?: boolean }): void { + selectWorkspace(_folderUri: URI, _options?: ISelectWorkspaceOptions): void { // no-op by default } diff --git a/src/vs/sessions/browser/parts/sessionView.ts b/src/vs/sessions/browser/parts/sessionView.ts index 8645059fd75089..1e3ac00ca87ad6 100644 --- a/src/vs/sessions/browser/parts/sessionView.ts +++ b/src/vs/sessions/browser/parts/sessionView.ts @@ -14,7 +14,7 @@ import { ServiceCollection } from '../../../platform/instantiation/common/servic import { IContextKey, IContextKeyService } from '../../../platform/contextkey/common/contextkey.js'; import { IThemeService } from '../../../platform/theme/common/themeService.js'; import { IActiveSession } from '../../services/sessions/common/sessionsManagement.js'; -import { AbstractChatView, IChatViewOptions } from './chatView.js'; +import { AbstractChatView, IChatViewOptions, ISelectWorkspaceOptions } from './chatView.js'; import { ChatGroupsView } from './chatGroupsView.js'; import { SessionHeader, SessionViewFloatingToolbar } from './sessionHeader.js'; import { ISessionContext, SessionContext } from '../../services/sessions/browser/sessionContext.js'; @@ -284,9 +284,9 @@ export class SessionView extends Disposable implements ISerializableView { return this._isVisible && this._header.startTitleEditing(); } - selectWorkspace(folderUri: URI, providerId?: string, options?: { readonly preferDevContainer?: boolean }): void { + selectWorkspace(folderUri: URI, options?: ISelectWorkspaceOptions): void { const standaloneView = this._standaloneView.value; - standaloneView ? standaloneView.selectWorkspace(folderUri, providerId, options) : this._groupsView.selectWorkspace(folderUri, providerId, options); + standaloneView ? standaloneView.selectWorkspace(folderUri, options) : this._groupsView.selectWorkspace(folderUri, options); } /** Opens the given chat in a group beside the active one ("open to the side"). */ diff --git a/src/vs/sessions/contrib/chat/browser/chatView.ts b/src/vs/sessions/contrib/chat/browser/chatView.ts index 7b4c42e1f17e16..d03b8d1b368bbe 100644 --- a/src/vs/sessions/contrib/chat/browser/chatView.ts +++ b/src/vs/sessions/contrib/chat/browser/chatView.ts @@ -36,7 +36,7 @@ import { IChatModel } from '../../../../workbench/contrib/chat/common/model/chat import { ChatAgentLocation, ChatModeKind } from '../../../../workbench/contrib/chat/common/constants.js'; import { getChatSessionType } from '../../../../workbench/contrib/chat/common/model/chatUri.js'; import { IChatSessionsService, localChatSessionType } from '../../../../workbench/contrib/chat/common/chatSessionsService.js'; -import { AbstractChatView, ChatViewKind, IChatViewOptions } from '../../../browser/parts/chatView.js'; +import { AbstractChatView, ChatViewKind, IChatViewOptions, ISelectWorkspaceOptions } from '../../../browser/parts/chatView.js'; import { ChatInteractivity, getSessionStatusMessage, IChat, isActiveSessionStatus, ISession, SessionStatus } from '../../../services/sessions/common/session.js'; import { IChatViewFactory } from '../../../services/chatView/browser/chatViewFactory.js'; import { NewChatWidget } from './newChatWidget.js'; @@ -104,9 +104,9 @@ export class NewChatView extends AbstractChatView { this._widget.focusInput(); } - override selectWorkspace(folderUri: URI, providerId?: string, options?: { readonly preferDevContainer?: boolean }): void { + override selectWorkspace(folderUri: URI, options?: ISelectWorkspaceOptions): void { if (this._widget instanceof NewChatWidget) { - this._widget.selectWorkspace(folderUri, providerId, options); + this._widget.selectWorkspace(folderUri, options); } } diff --git a/src/vs/sessions/contrib/chat/browser/newChatWidget.ts b/src/vs/sessions/contrib/chat/browser/newChatWidget.ts index 456f9b524b249e..fd0adb42c3849a 100644 --- a/src/vs/sessions/contrib/chat/browser/newChatWidget.ts +++ b/src/vs/sessions/contrib/chat/browser/newChatWidget.ts @@ -37,7 +37,7 @@ import { NewChatInputWidget } from './newChatInput.js'; import { NoAgentHostEmptyState } from './noAgentHostEmptyState.js'; import { IChatRequestVariableEntry } from '../../../../workbench/contrib/chat/common/attachments/chatVariableEntries.js'; import { IAgentHostFilterService } from '../../../services/agentHostFilter/common/agentHostFilter.js'; -import { IChatViewOptions } from '../../../browser/parts/chatView.js'; +import { IChatViewOptions, ISelectWorkspaceOptions } from '../../../browser/parts/chatView.js'; import { SessionWorkspacePickerVisibleContext } from '../../../common/contextkeys.js'; import { AGENT_FEEDBACK_NEW_SESSION_RESOURCE, AgentFeedbackState, IAgentFeedback, IAgentFeedbackService } from '../../agentFeedback/browser/agentFeedbackService.js'; import { buildNewSessionPrompt } from '../../agentFeedback/browser/agentFeedbackAttachmentEntry.js'; @@ -1026,9 +1026,9 @@ export class NewChatWidget extends Disposable { this._newChatInput.attach(uris); } - selectWorkspace(folderUri: URI, providerId?: string, options?: { readonly preferDevContainer?: boolean }): void { + selectWorkspace(folderUri: URI, options?: ISelectWorkspaceOptions): void { this._preferredDevContainerFolderUri = options?.preferDevContainer ? folderUri : undefined; - this._workspacePicker.setSelectedWorkspace(folderUri, { providerId }); + this._workspacePicker.setSelectedWorkspace(folderUri, { providerId: options?.providerId }); } } 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 4cd4e3d416126a..4a4f1ab768ed9a 100644 --- a/src/vs/sessions/contrib/chat/electron-browser/chat.contribution.ts +++ b/src/vs/sessions/contrib/chat/electron-browser/chat.contribution.ts @@ -222,7 +222,10 @@ class SelectAgentsFolderContribution extends Disposable implements IWorkbenchCon } const activeSession = this.sessionsService.activeSession.get(); if (activeSession === undefined || activeSession.status.get() === SessionStatus.Untitled) { - this.sessionsPartService.getSessionView(activeSession?.sessionId)?.selectWorkspace(folderUri, resolved.providerId, { preferDevContainer }); + this.sessionsPartService.getSessionView(activeSession?.sessionId)?.selectWorkspace(folderUri, { + providerId: resolved.providerId, + preferDevContainer, + }); } return true; } diff --git a/src/vs/sessions/contrib/providers/remoteAgentHost/browser/remoteAgentHostActions.ts b/src/vs/sessions/contrib/providers/remoteAgentHost/browser/remoteAgentHostActions.ts index 3f8652cb77324d..e44b688959b30a 100644 --- a/src/vs/sessions/contrib/providers/remoteAgentHost/browser/remoteAgentHostActions.ts +++ b/src/vs/sessions/contrib/providers/remoteAgentHost/browser/remoteAgentHostActions.ts @@ -1099,7 +1099,7 @@ async function promptForTunnelFolder( } sessionsService.openNewSession(); - sessionsPartService.getSessionView(sessionsService.activeSession.get()?.sessionId)?.selectWorkspace(folderUri, provider.id); + sessionsPartService.getSessionView(sessionsService.activeSession.get()?.sessionId)?.selectWorkspace(folderUri, { providerId: provider.id }); } registerAction2(class extends Action2 { @@ -1290,7 +1290,7 @@ async function promptForWSLFolder( } sessionsService.openNewSession(); - sessionsPartService.getSessionView(sessionsService.activeSession.get()?.sessionId)?.selectWorkspace(folderUri, provider.id); + sessionsPartService.getSessionView(sessionsService.activeSession.get()?.sessionId)?.selectWorkspace(folderUri, { providerId: provider.id }); } registerAction2(class extends Action2 { diff --git a/src/vs/sessions/contrib/sessions/browser/views/sessionsViewActions.ts b/src/vs/sessions/contrib/sessions/browser/views/sessionsViewActions.ts index 4fcb2e70c0d845..88cbe99e0b0bbe 100644 --- a/src/vs/sessions/contrib/sessions/browser/views/sessionsViewActions.ts +++ b/src/vs/sessions/contrib/sessions/browser/views/sessionsViewActions.ts @@ -491,7 +491,7 @@ registerAction2(class NewSessionForWorkspaceAction extends Action2 { const newSession = sessionsService.activeSession.get(); if (folderUri) { - sessionsPartService.getSessionView(newSession?.sessionId)?.selectWorkspace(folderUri, providerId); + sessionsPartService.getSessionView(newSession?.sessionId)?.selectWorkspace(folderUri, { providerId }); } // On mobile web, the sidebar drawer covers the viewport; close it so From 9f00a447872233197a5848ea77c842abf7067a90 Mon Sep 17 00:00:00 2001 From: Christof Marti Date: Fri, 4 Sep 2026 11:09:46 +0200 Subject: [PATCH 8/8] Clarify pending Dev Container enablement Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../browser/localAgentHostSessionsProvider.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/vs/sessions/contrib/providers/agentHost/browser/localAgentHostSessionsProvider.ts b/src/vs/sessions/contrib/providers/agentHost/browser/localAgentHostSessionsProvider.ts index 5fe890615af5c5..6f746ec0c4c3bc 100644 --- a/src/vs/sessions/contrib/providers/agentHost/browser/localAgentHostSessionsProvider.ts +++ b/src/vs/sessions/contrib/providers/agentHost/browser/localAgentHostSessionsProvider.ts @@ -123,7 +123,7 @@ export class LocalAgentHostSessionsProvider extends BaseAgentHostSessionsProvide private _automationSessionResources = new ResourceSet(); private readonly _devContainerAvailableDrafts = new Set(); private readonly _devContainerDrafts = new Set(); - private readonly _preferredDevContainerDrafts = new Set(); + private readonly _pendingDevContainerEnablement = new Set(); override get order(): number { return -1; } @@ -309,16 +309,16 @@ export class LocalAgentHostSessionsProvider extends BaseAgentHostSessionsProvide try { const available = await this._devContainerAgentHostService.isAvailable(workspaceUri); if (!available || !this._getNewSession(sessionId)) { - this._preferredDevContainerDrafts.delete(sessionId); + this._pendingDevContainerEnablement.delete(sessionId); return; } this._devContainerAvailableDrafts.add(sessionId); - if (this._preferredDevContainerDrafts.delete(sessionId)) { + if (this._pendingDevContainerEnablement.delete(sessionId)) { this._devContainerDrafts.add(sessionId); } this._onDidChangeSessionConfig.fire(sessionId); } catch (error) { - this._preferredDevContainerDrafts.delete(sessionId); + this._pendingDevContainerEnablement.delete(sessionId); this._logService.warn(`[${this.id}] Failed to resolve Dev Container availability for ${workspaceUri.toString()}`, error); } } @@ -339,7 +339,7 @@ export class LocalAgentHostSessionsProvider extends BaseAgentHostSessionsProvide this._devContainerDrafts.add(sessionId); this._onDidChangeSessionConfig.fire(sessionId); } else { - this._preferredDevContainerDrafts.add(sessionId); + this._pendingDevContainerEnablement.add(sessionId); } } @@ -351,11 +351,11 @@ export class LocalAgentHostSessionsProvider extends BaseAgentHostSessionsProvide throw new Error(`Cannot enable Dev Container execution for unavailable session '${sessionId}'.`); } if (enabled) { - this._preferredDevContainerDrafts.delete(sessionId); + this._pendingDevContainerEnablement.delete(sessionId); this._devContainerDrafts.add(sessionId); } else { this._devContainerDrafts.delete(sessionId); - this._preferredDevContainerDrafts.delete(sessionId); + this._pendingDevContainerEnablement.delete(sessionId); } this._onDidChangeSessionConfig.fire(sessionId); } @@ -544,14 +544,14 @@ export class LocalAgentHostSessionsProvider extends BaseAgentHostSessionsProvide override deleteNewSession(sessionId: string): void { this._devContainerAvailableDrafts.delete(sessionId); this._devContainerDrafts.delete(sessionId); - this._preferredDevContainerDrafts.delete(sessionId); + this._pendingDevContainerEnablement.delete(sessionId); super.deleteNewSession(sessionId); } protected override _disposeAllNewSessions(): void { this._devContainerAvailableDrafts.clear(); this._devContainerDrafts.clear(); - this._preferredDevContainerDrafts.clear(); + this._pendingDevContainerEnablement.clear(); super._disposeAllNewSessions(); }