From eebcc2be1276934f90c4d61be31ec35fef635fb2 Mon Sep 17 00:00:00 2001 From: Roi Dayan Date: Sun, 20 Sep 2026 12:43:36 +0300 Subject: [PATCH 1/4] sessions: add a context key gating the classic-window actions The Agents window can be the only window a product ships, but four actions reach for a classic VS Code window with no way to gate them: - `OpenVSCodeWindowAction` is `f1: true` and bound to Cmd/Ctrl+Shift+A with no precondition and no when clause at all - `ReturnToVSCodeEditorAction` has no precondition - `OpenInVSCodeAction` and `OpenSessionInVSCodeAction` gate only on `IsAuxiliaryWindowContext` and `SessionsWelcomeVisibleContext` Adds `SessionsClassicWindowAvailableContext`, defaulting to `true` so VS Code's own behaviour is unchanged, and includes it in all four. A product that cannot open a classic window sets it to `false` once and the actions stop offering themselves, instead of each embedder finding its own way to suppress a command it cannot fulfil. Co-Authored-By: Claude Opus 5 (1M context) --- src/vs/sessions/browser/actions/vscodeActions.ts | 6 +++--- src/vs/sessions/common/contextkeys.ts | 7 +++++++ src/vs/sessions/electron-browser/actions/vscodeActions.ts | 8 +++++--- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/src/vs/sessions/browser/actions/vscodeActions.ts b/src/vs/sessions/browser/actions/vscodeActions.ts index 59d159c1035472..9e2f3645a3d739 100644 --- a/src/vs/sessions/browser/actions/vscodeActions.ts +++ b/src/vs/sessions/browser/actions/vscodeActions.ts @@ -16,7 +16,7 @@ import { IOpenerService } from '../../../platform/opener/common/opener.js'; import { IProductService } from '../../../platform/product/common/productService.js'; import { ITelemetryService } from '../../../platform/telemetry/common/telemetry.js'; import { IsAuxiliaryWindowContext } from '../../../workbench/common/contextkeys.js'; -import { IsPhoneLayoutContext, SessionsWelcomeVisibleContext } from '../../common/contextkeys.js'; +import { IsPhoneLayoutContext, SessionsClassicWindowAvailableContext, SessionsWelcomeVisibleContext } from '../../common/contextkeys.js'; import { logSessionsInteraction } from '../../common/sessionsTelemetry.js'; import { Menus } from '../../browser/menus.js'; import { ISessionsService } from '../../services/sessions/browser/sessionsService.js'; @@ -36,12 +36,12 @@ export class OpenInVSCodeAction extends Action2 { id: OpenInVSCodeAction.ID, title: localize2('openInVSCode', 'Open in Editor'), icon: Codicon.vscodeInsiders, - precondition: ContextKeyExpr.and(IsAuxiliaryWindowContext.toNegated(), SessionsWelcomeVisibleContext.toNegated()), + precondition: ContextKeyExpr.and(IsAuxiliaryWindowContext.toNegated(), SessionsWelcomeVisibleContext.toNegated(), SessionsClassicWindowAvailableContext), menu: [{ id: Menus.TitleBarCenterRight, group: 'navigation', order: 7, - when: ContextKeyExpr.and(IsAuxiliaryWindowContext.toNegated(), SessionsWelcomeVisibleContext.toNegated(), IsPhoneLayoutContext.negate()), + when: ContextKeyExpr.and(IsAuxiliaryWindowContext.toNegated(), SessionsWelcomeVisibleContext.toNegated(), SessionsClassicWindowAvailableContext, IsPhoneLayoutContext.negate()), }] }); } diff --git a/src/vs/sessions/common/contextkeys.ts b/src/vs/sessions/common/contextkeys.ts index b998a1d4d558ea..75ef1a949eecdc 100644 --- a/src/vs/sessions/common/contextkeys.ts +++ b/src/vs/sessions/common/contextkeys.ts @@ -167,6 +167,13 @@ export const SinglePaneFilesTabAvailableContext = new RawContextKey('ag //#endregion +//#region < --- Classic Window --- > + +/** `true` by default; a product that embeds the Agents window as its only window sets it to `false`. */ +export const SessionsClassicWindowAvailableContext = new RawContextKey('sessionsClassicWindowAvailable', true, localize('sessionsClassicWindowAvailable', "Whether a classic VS Code window can be opened from the Agents window")); + +//#endregion + //#region < --- Mobile Layout --- > export const IsPhoneLayoutContext = new RawContextKey('sessionsIsPhoneLayout', false, localize('sessionsIsPhoneLayout', "Whether the current layout is the phone layout")); diff --git a/src/vs/sessions/electron-browser/actions/vscodeActions.ts b/src/vs/sessions/electron-browser/actions/vscodeActions.ts index 188a45b48de521..f83b3b23d66f61 100644 --- a/src/vs/sessions/electron-browser/actions/vscodeActions.ts +++ b/src/vs/sessions/electron-browser/actions/vscodeActions.ts @@ -16,7 +16,7 @@ import { ContextKeyExpr } from '../../../platform/contextkey/common/contextkey.j import { KeybindingWeight } from '../../../platform/keybinding/common/keybindingsRegistry.js'; import { ITelemetryService } from '../../../platform/telemetry/common/telemetry.js'; import { IsAuxiliaryWindowContext } from '../../../workbench/common/contextkeys.js'; -import { IsPhoneLayoutContext, SessionsWelcomeVisibleContext } from '../../common/contextkeys.js'; +import { IsPhoneLayoutContext, SessionsClassicWindowAvailableContext, SessionsWelcomeVisibleContext } from '../../common/contextkeys.js'; import { logSessionsInteraction } from '../../common/sessionsTelemetry.js'; import { Menus } from '../../browser/menus.js'; import { ISessionsService } from '../../services/sessions/browser/sessionsService.js'; @@ -40,12 +40,12 @@ export class OpenSessionInVSCodeAction extends Action2 { id: OpenSessionInVSCodeAction.ID, title: localize2('openInVSCode', 'Open in Editor'), icon: Codicon.vscodeInsiders, - precondition: ContextKeyExpr.and(IsAuxiliaryWindowContext.toNegated(), SessionsWelcomeVisibleContext.toNegated()), + precondition: ContextKeyExpr.and(IsAuxiliaryWindowContext.toNegated(), SessionsWelcomeVisibleContext.toNegated(), SessionsClassicWindowAvailableContext), menu: [{ id: Menus.TitleBarCenterRight, group: 'navigation', order: 7, - when: ContextKeyExpr.and(IsAuxiliaryWindowContext.toNegated(), SessionsWelcomeVisibleContext.toNegated(), IsPhoneLayoutContext.negate()), + when: ContextKeyExpr.and(IsAuxiliaryWindowContext.toNegated(), SessionsWelcomeVisibleContext.toNegated(), SessionsClassicWindowAvailableContext, IsPhoneLayoutContext.negate()), }] }); } @@ -98,6 +98,7 @@ export class OpenVSCodeWindowAction extends Action2 { super({ id: OpenVSCodeWindowAction.ID, title: localize2('openVSCodeWindow', 'Open VS Code Window'), + precondition: SessionsClassicWindowAvailableContext, f1: true, keybinding: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyA, @@ -127,6 +128,7 @@ export class ReturnToVSCodeEditorAction extends Action2 { super({ id: RETURN_TO_VSCODE_EDITOR_COMMAND_ID, title: localize2('returnToVSCodeEditor', 'Return to VS Code Editor'), + precondition: SessionsClassicWindowAvailableContext, }); } From 2754bc8742c4bff5c310b89bb4b53fe598bdc28e Mon Sep 17 00:00:00 2001 From: Roi Dayan Date: Sun, 20 Sep 2026 12:53:32 +0300 Subject: [PATCH 2/4] sessions: make the key opt-out, and gate the path a precondition cannot Two fixes from review. A `RawContextKey`'s default is written only by `bindTo()`, and nothing binds this key -- so it reads as `undefined` and every positive check evaluated false, disabling the actions by default rather than leaving them alone. Consumers now test `notEqualsTo(false)`, so unset and `true` both mean available and stock VS Code is genuinely unchanged. `ReturnToVSCodeEditorAction` has no `f1` and no menu, so a precondition on it gates nothing: the sign-in dialog renders its footer button from `agents.shouldShowReturnToVSCodeEditor` and then executes the command directly, and `Action2` handlers do not enforce preconditions. The decorative precondition is removed and the availability check moves into the decision command, which is what the button is rendered from. Co-Authored-By: Claude Opus 5 (1M context) --- src/vs/sessions/browser/actions/vscodeActions.ts | 4 ++-- src/vs/sessions/common/contextkeys.ts | 8 +++++++- .../electron-browser/actions/vscodeActions.ts | 14 +++++++++----- 3 files changed, 18 insertions(+), 8 deletions(-) diff --git a/src/vs/sessions/browser/actions/vscodeActions.ts b/src/vs/sessions/browser/actions/vscodeActions.ts index 9e2f3645a3d739..e3660a61932fdb 100644 --- a/src/vs/sessions/browser/actions/vscodeActions.ts +++ b/src/vs/sessions/browser/actions/vscodeActions.ts @@ -36,12 +36,12 @@ export class OpenInVSCodeAction extends Action2 { id: OpenInVSCodeAction.ID, title: localize2('openInVSCode', 'Open in Editor'), icon: Codicon.vscodeInsiders, - precondition: ContextKeyExpr.and(IsAuxiliaryWindowContext.toNegated(), SessionsWelcomeVisibleContext.toNegated(), SessionsClassicWindowAvailableContext), + precondition: ContextKeyExpr.and(IsAuxiliaryWindowContext.toNegated(), SessionsWelcomeVisibleContext.toNegated(), SessionsClassicWindowAvailableContext.notEqualsTo(false)), menu: [{ id: Menus.TitleBarCenterRight, group: 'navigation', order: 7, - when: ContextKeyExpr.and(IsAuxiliaryWindowContext.toNegated(), SessionsWelcomeVisibleContext.toNegated(), SessionsClassicWindowAvailableContext, IsPhoneLayoutContext.negate()), + when: ContextKeyExpr.and(IsAuxiliaryWindowContext.toNegated(), SessionsWelcomeVisibleContext.toNegated(), SessionsClassicWindowAvailableContext.notEqualsTo(false), IsPhoneLayoutContext.negate()), }] }); } diff --git a/src/vs/sessions/common/contextkeys.ts b/src/vs/sessions/common/contextkeys.ts index 75ef1a949eecdc..80256b050ad6b9 100644 --- a/src/vs/sessions/common/contextkeys.ts +++ b/src/vs/sessions/common/contextkeys.ts @@ -169,7 +169,13 @@ export const SinglePaneFilesTabAvailableContext = new RawContextKey('ag //#region < --- Classic Window --- > -/** `true` by default; a product that embeds the Agents window as its only window sets it to `false`. */ +/** + * Whether a classic VS Code window can be opened from the Agents window. + * + * Nothing binds this key, so it reads as `undefined` in stock VS Code; consumers therefore test + * `notEqualsTo(false)` so that unset and `true` both mean available. A product that embeds the + * Agents window as its only window sets it to `false`. + */ export const SessionsClassicWindowAvailableContext = new RawContextKey('sessionsClassicWindowAvailable', true, localize('sessionsClassicWindowAvailable', "Whether a classic VS Code window can be opened from the Agents window")); //#endregion diff --git a/src/vs/sessions/electron-browser/actions/vscodeActions.ts b/src/vs/sessions/electron-browser/actions/vscodeActions.ts index f83b3b23d66f61..86a1a78ba4801d 100644 --- a/src/vs/sessions/electron-browser/actions/vscodeActions.ts +++ b/src/vs/sessions/electron-browser/actions/vscodeActions.ts @@ -12,7 +12,7 @@ import { localize2 } from '../../../nls.js'; import { Action2 } from '../../../platform/actions/common/actions.js'; import { IRemoteAgentHostService } from '../../../platform/agentHost/common/remoteAgentHostService.js'; import { KeyCode, KeyMod } from '../../../base/common/keyCodes.js'; -import { ContextKeyExpr } from '../../../platform/contextkey/common/contextkey.js'; +import { ContextKeyExpr, IContextKeyService } from '../../../platform/contextkey/common/contextkey.js'; import { KeybindingWeight } from '../../../platform/keybinding/common/keybindingsRegistry.js'; import { ITelemetryService } from '../../../platform/telemetry/common/telemetry.js'; import { IsAuxiliaryWindowContext } from '../../../workbench/common/contextkeys.js'; @@ -40,12 +40,12 @@ export class OpenSessionInVSCodeAction extends Action2 { id: OpenSessionInVSCodeAction.ID, title: localize2('openInVSCode', 'Open in Editor'), icon: Codicon.vscodeInsiders, - precondition: ContextKeyExpr.and(IsAuxiliaryWindowContext.toNegated(), SessionsWelcomeVisibleContext.toNegated(), SessionsClassicWindowAvailableContext), + precondition: ContextKeyExpr.and(IsAuxiliaryWindowContext.toNegated(), SessionsWelcomeVisibleContext.toNegated(), SessionsClassicWindowAvailableContext.notEqualsTo(false)), menu: [{ id: Menus.TitleBarCenterRight, group: 'navigation', order: 7, - when: ContextKeyExpr.and(IsAuxiliaryWindowContext.toNegated(), SessionsWelcomeVisibleContext.toNegated(), SessionsClassicWindowAvailableContext, IsPhoneLayoutContext.negate()), + when: ContextKeyExpr.and(IsAuxiliaryWindowContext.toNegated(), SessionsWelcomeVisibleContext.toNegated(), SessionsClassicWindowAvailableContext.notEqualsTo(false), IsPhoneLayoutContext.negate()), }] }); } @@ -98,7 +98,7 @@ export class OpenVSCodeWindowAction extends Action2 { super({ id: OpenVSCodeWindowAction.ID, title: localize2('openVSCodeWindow', 'Open VS Code Window'), - precondition: SessionsClassicWindowAvailableContext, + precondition: SessionsClassicWindowAvailableContext.notEqualsTo(false), f1: true, keybinding: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyA, @@ -128,7 +128,6 @@ export class ReturnToVSCodeEditorAction extends Action2 { super({ id: RETURN_TO_VSCODE_EDITOR_COMMAND_ID, title: localize2('returnToVSCodeEditor', 'Return to VS Code Editor'), - precondition: SessionsClassicWindowAvailableContext, }); } @@ -148,6 +147,11 @@ export class ShouldShowReturnToVSCodeEditorAction extends Action2 { } override async run(accessor: ServicesAccessor): Promise { + // The sign-in dialog renders its footer button from this answer and then invokes the command + // directly, which no precondition can gate -- so the availability check belongs here. + if (SessionsClassicWindowAvailableContext.getValue(accessor.get(IContextKeyService)) === false) { + return false; + } const nativeHostService = accessor.get(INativeHostService); const windows = await nativeHostService.getWindows({ includeAuxiliaryWindows: false }); return shouldShowReturnToVSCodeEditor(windows, getWindowId(mainWindow)); From 0772cbe64b8b6a7474c4ef0e44b96de15bf658d7 Mon Sep 17 00:00:00 2001 From: Roi Dayan Date: Sun, 20 Sep 2026 13:05:56 +0300 Subject: [PATCH 3/4] sessions: invert the key to an opt-out, with a regression test `notEqualsTo(false)` did not fix the previous round: `ContextKeyNotEqualsExpr.create` canonicalizes a boolean `false` comparison straight back to `ContextKeyDefinedExpr` (contextkey.ts:1046-1052), so the expression was identical to the one it replaced and an unbound key still evaluated false. Inverted instead. `sessionsClassicWindowUnavailable` defaults to `false` and every consumer negates it, so `ContextKeyNotExpr` evaluates `!undefined` -- unset means available, no binding and no ordering between a binder and a product that opts out. Adds a regression test over unset, `true` and `false`, against the real preconditions of `OpenVSCodeWindowAction` and `OpenSessionInVSCodeAction` -- bare and inside a conjunction, since `and()` canonicalizes too. Also condenses the guard comment in `ShouldShowReturnToVSCodeEditorAction`. Co-Authored-By: Claude Opus 5 (1M context) --- .../sessions/browser/actions/vscodeActions.ts | 6 ++--- src/vs/sessions/common/contextkeys.ts | 11 ++++---- .../electron-browser/actions/vscodeActions.ts | 13 +++++----- .../electron-browser/vscodeActions.test.ts | 26 ++++++++++++++++++- 4 files changed, 40 insertions(+), 16 deletions(-) diff --git a/src/vs/sessions/browser/actions/vscodeActions.ts b/src/vs/sessions/browser/actions/vscodeActions.ts index e3660a61932fdb..620be7f2fe144f 100644 --- a/src/vs/sessions/browser/actions/vscodeActions.ts +++ b/src/vs/sessions/browser/actions/vscodeActions.ts @@ -16,7 +16,7 @@ import { IOpenerService } from '../../../platform/opener/common/opener.js'; import { IProductService } from '../../../platform/product/common/productService.js'; import { ITelemetryService } from '../../../platform/telemetry/common/telemetry.js'; import { IsAuxiliaryWindowContext } from '../../../workbench/common/contextkeys.js'; -import { IsPhoneLayoutContext, SessionsClassicWindowAvailableContext, SessionsWelcomeVisibleContext } from '../../common/contextkeys.js'; +import { IsPhoneLayoutContext, SessionsClassicWindowUnavailableContext, SessionsWelcomeVisibleContext } from '../../common/contextkeys.js'; import { logSessionsInteraction } from '../../common/sessionsTelemetry.js'; import { Menus } from '../../browser/menus.js'; import { ISessionsService } from '../../services/sessions/browser/sessionsService.js'; @@ -36,12 +36,12 @@ export class OpenInVSCodeAction extends Action2 { id: OpenInVSCodeAction.ID, title: localize2('openInVSCode', 'Open in Editor'), icon: Codicon.vscodeInsiders, - precondition: ContextKeyExpr.and(IsAuxiliaryWindowContext.toNegated(), SessionsWelcomeVisibleContext.toNegated(), SessionsClassicWindowAvailableContext.notEqualsTo(false)), + precondition: ContextKeyExpr.and(IsAuxiliaryWindowContext.toNegated(), SessionsWelcomeVisibleContext.toNegated(), SessionsClassicWindowUnavailableContext.toNegated()), menu: [{ id: Menus.TitleBarCenterRight, group: 'navigation', order: 7, - when: ContextKeyExpr.and(IsAuxiliaryWindowContext.toNegated(), SessionsWelcomeVisibleContext.toNegated(), SessionsClassicWindowAvailableContext.notEqualsTo(false), IsPhoneLayoutContext.negate()), + when: ContextKeyExpr.and(IsAuxiliaryWindowContext.toNegated(), SessionsWelcomeVisibleContext.toNegated(), SessionsClassicWindowUnavailableContext.toNegated(), IsPhoneLayoutContext.negate()), }] }); } diff --git a/src/vs/sessions/common/contextkeys.ts b/src/vs/sessions/common/contextkeys.ts index 80256b050ad6b9..183b9fe6adedec 100644 --- a/src/vs/sessions/common/contextkeys.ts +++ b/src/vs/sessions/common/contextkeys.ts @@ -170,13 +170,14 @@ export const SinglePaneFilesTabAvailableContext = new RawContextKey('ag //#region < --- Classic Window --- > /** - * Whether a classic VS Code window can be opened from the Agents window. + * Whether opening a classic VS Code window is unavailable, so the actions that do it hide themselves. * - * Nothing binds this key, so it reads as `undefined` in stock VS Code; consumers therefore test - * `notEqualsTo(false)` so that unset and `true` both mean available. A product that embeds the - * Agents window as its only window sets it to `false`. + * Stated as an opt-out because nothing binds it: on a positive key `notEqualsTo(false)` canonicalizes + * to a defined-check (`ContextKeyNotEqualsExpr.create`), which is false when unset. Negating this one + * gives the intended default instead -- unset means available, and a product that cannot open a + * classic window sets it to `true`. */ -export const SessionsClassicWindowAvailableContext = new RawContextKey('sessionsClassicWindowAvailable', true, localize('sessionsClassicWindowAvailable', "Whether a classic VS Code window can be opened from the Agents window")); +export const SessionsClassicWindowUnavailableContext = new RawContextKey('sessionsClassicWindowUnavailable', false, localize('sessionsClassicWindowUnavailable', "Whether opening a classic VS Code window is unavailable from the Agents window")); //#endregion diff --git a/src/vs/sessions/electron-browser/actions/vscodeActions.ts b/src/vs/sessions/electron-browser/actions/vscodeActions.ts index 86a1a78ba4801d..c4cafa63fbe044 100644 --- a/src/vs/sessions/electron-browser/actions/vscodeActions.ts +++ b/src/vs/sessions/electron-browser/actions/vscodeActions.ts @@ -16,7 +16,7 @@ import { ContextKeyExpr, IContextKeyService } from '../../../platform/contextkey import { KeybindingWeight } from '../../../platform/keybinding/common/keybindingsRegistry.js'; import { ITelemetryService } from '../../../platform/telemetry/common/telemetry.js'; import { IsAuxiliaryWindowContext } from '../../../workbench/common/contextkeys.js'; -import { IsPhoneLayoutContext, SessionsClassicWindowAvailableContext, SessionsWelcomeVisibleContext } from '../../common/contextkeys.js'; +import { IsPhoneLayoutContext, SessionsClassicWindowUnavailableContext, SessionsWelcomeVisibleContext } from '../../common/contextkeys.js'; import { logSessionsInteraction } from '../../common/sessionsTelemetry.js'; import { Menus } from '../../browser/menus.js'; import { ISessionsService } from '../../services/sessions/browser/sessionsService.js'; @@ -40,12 +40,12 @@ export class OpenSessionInVSCodeAction extends Action2 { id: OpenSessionInVSCodeAction.ID, title: localize2('openInVSCode', 'Open in Editor'), icon: Codicon.vscodeInsiders, - precondition: ContextKeyExpr.and(IsAuxiliaryWindowContext.toNegated(), SessionsWelcomeVisibleContext.toNegated(), SessionsClassicWindowAvailableContext.notEqualsTo(false)), + precondition: ContextKeyExpr.and(IsAuxiliaryWindowContext.toNegated(), SessionsWelcomeVisibleContext.toNegated(), SessionsClassicWindowUnavailableContext.toNegated()), menu: [{ id: Menus.TitleBarCenterRight, group: 'navigation', order: 7, - when: ContextKeyExpr.and(IsAuxiliaryWindowContext.toNegated(), SessionsWelcomeVisibleContext.toNegated(), SessionsClassicWindowAvailableContext.notEqualsTo(false), IsPhoneLayoutContext.negate()), + when: ContextKeyExpr.and(IsAuxiliaryWindowContext.toNegated(), SessionsWelcomeVisibleContext.toNegated(), SessionsClassicWindowUnavailableContext.toNegated(), IsPhoneLayoutContext.negate()), }] }); } @@ -98,7 +98,7 @@ export class OpenVSCodeWindowAction extends Action2 { super({ id: OpenVSCodeWindowAction.ID, title: localize2('openVSCodeWindow', 'Open VS Code Window'), - precondition: SessionsClassicWindowAvailableContext.notEqualsTo(false), + precondition: SessionsClassicWindowUnavailableContext.toNegated(), f1: true, keybinding: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyA, @@ -147,9 +147,8 @@ export class ShouldShowReturnToVSCodeEditorAction extends Action2 { } override async run(accessor: ServicesAccessor): Promise { - // The sign-in dialog renders its footer button from this answer and then invokes the command - // directly, which no precondition can gate -- so the availability check belongs here. - if (SessionsClassicWindowAvailableContext.getValue(accessor.get(IContextKeyService)) === false) { + // The sign-in dialog invokes the command directly, which no precondition can gate. + if (SessionsClassicWindowUnavailableContext.getValue(accessor.get(IContextKeyService)) === true) { return false; } const nativeHostService = accessor.get(INativeHostService); diff --git a/src/vs/sessions/test/electron-browser/vscodeActions.test.ts b/src/vs/sessions/test/electron-browser/vscodeActions.test.ts index 10267fdeb43349..8ecb98fb95a2e5 100644 --- a/src/vs/sessions/test/electron-browser/vscodeActions.test.ts +++ b/src/vs/sessions/test/electron-browser/vscodeActions.test.ts @@ -10,7 +10,8 @@ import { INativeHostService } from '../../../platform/native/common/native.js'; import { IOpenedMainWindow } from '../../../platform/window/common/window.js'; import { constObservable } from '../../../base/common/observable.js'; import { URI } from '../../../base/common/uri.js'; -import { getChatSessionToOpenInEditor, returnToVSCodeEditor, shouldShowReturnToVSCodeEditor } from '../../electron-browser/actions/vscodeActions.js'; +import { getChatSessionToOpenInEditor, OpenSessionInVSCodeAction, OpenVSCodeWindowAction, returnToVSCodeEditor, shouldShowReturnToVSCodeEditor } from '../../electron-browser/actions/vscodeActions.js'; +import { IContext } from '../../../platform/contextkey/common/contextkey.js'; import { IActiveSession } from '../../services/sessions/common/sessionsManagement.js'; suite('VS Code Actions', () => { @@ -33,6 +34,29 @@ suite('VS Code Actions', () => { }); }); + test('classic-window actions stay available unless a product opts out', () => { + // Guards the default: nothing binds this key, so an expression that is false when it is unset + // would hide these actions in stock VS Code. + const context = (unavailable: boolean | undefined) => new class extends mock() { + override getValue(key: string): T | undefined { + return (key === 'sessionsClassicWindowUnavailable' ? unavailable : undefined) as T | undefined; + } + }; + // The bare precondition and one inside a conjunction, because `and()` canonicalizes too. + const bare = new OpenVSCodeWindowAction().desc.precondition; + const conjoined = new OpenSessionInVSCodeAction().desc.precondition; + + assert.deepStrictEqual({ + unset: [bare?.evaluate(context(undefined)), conjoined?.evaluate(context(undefined))], + optedOut: [bare?.evaluate(context(true)), conjoined?.evaluate(context(true))], + optedIn: [bare?.evaluate(context(false)), conjoined?.evaluate(context(false))], + }, { + unset: [true, true], + optedOut: [false, false], + optedIn: [true, true], + }); + }); + test('opens an editor window before closing the Agents window', async () => { const calls: string[] = []; const nativeHostService = new class extends mock() { From e67dc0f4b72ce1f9acda11860da2f7e128ec86e8 Mon Sep 17 00:00:00 2001 From: Roi Dayan Date: Sun, 20 Sep 2026 13:33:05 +0300 Subject: [PATCH 4/4] sessions: cover the sign-in-dialog gate, and trim the comments The regression test only evaluated action preconditions, so removing the guard in `ShouldShowReturnToVSCodeEditorAction.run()` would still have passed while the Return button stayed visible in an opted-out product. It is now covered over unset, `false` and `true`, including that opting out short-circuits before the native host is asked for windows. Trims the context key's JSDoc to the opt-out invariant -- the canonicalization history it carried is what the test protects -- and the two-line narration in the test to one line. Co-Authored-By: Claude Opus 5 (1M context) --- src/vs/sessions/common/contextkeys.ts | 9 +---- .../electron-browser/vscodeActions.test.ts | 40 +++++++++++++++++-- 2 files changed, 37 insertions(+), 12 deletions(-) diff --git a/src/vs/sessions/common/contextkeys.ts b/src/vs/sessions/common/contextkeys.ts index 183b9fe6adedec..2147f03e841985 100644 --- a/src/vs/sessions/common/contextkeys.ts +++ b/src/vs/sessions/common/contextkeys.ts @@ -169,14 +169,7 @@ export const SinglePaneFilesTabAvailableContext = new RawContextKey('ag //#region < --- Classic Window --- > -/** - * Whether opening a classic VS Code window is unavailable, so the actions that do it hide themselves. - * - * Stated as an opt-out because nothing binds it: on a positive key `notEqualsTo(false)` canonicalizes - * to a defined-check (`ContextKeyNotEqualsExpr.create`), which is false when unset. Negating this one - * gives the intended default instead -- unset means available, and a product that cannot open a - * classic window sets it to `true`. - */ +/** An opt-out, so consumers negate it rather than testing a positive key: unset means available. */ export const SessionsClassicWindowUnavailableContext = new RawContextKey('sessionsClassicWindowUnavailable', false, localize('sessionsClassicWindowUnavailable', "Whether opening a classic VS Code window is unavailable from the Agents window")); //#endregion diff --git a/src/vs/sessions/test/electron-browser/vscodeActions.test.ts b/src/vs/sessions/test/electron-browser/vscodeActions.test.ts index 8ecb98fb95a2e5..19b36ba689b892 100644 --- a/src/vs/sessions/test/electron-browser/vscodeActions.test.ts +++ b/src/vs/sessions/test/electron-browser/vscodeActions.test.ts @@ -10,8 +10,9 @@ import { INativeHostService } from '../../../platform/native/common/native.js'; import { IOpenedMainWindow } from '../../../platform/window/common/window.js'; import { constObservable } from '../../../base/common/observable.js'; import { URI } from '../../../base/common/uri.js'; -import { getChatSessionToOpenInEditor, OpenSessionInVSCodeAction, OpenVSCodeWindowAction, returnToVSCodeEditor, shouldShowReturnToVSCodeEditor } from '../../electron-browser/actions/vscodeActions.js'; -import { IContext } from '../../../platform/contextkey/common/contextkey.js'; +import { getChatSessionToOpenInEditor, OpenSessionInVSCodeAction, OpenVSCodeWindowAction, returnToVSCodeEditor, shouldShowReturnToVSCodeEditor, ShouldShowReturnToVSCodeEditorAction } from '../../electron-browser/actions/vscodeActions.js'; +import { IContext, IContextKeyService } from '../../../platform/contextkey/common/contextkey.js'; +import { ServiceIdentifier, ServicesAccessor } from '../../../platform/instantiation/common/instantiation.js'; import { IActiveSession } from '../../services/sessions/common/sessionsManagement.js'; suite('VS Code Actions', () => { @@ -35,8 +36,7 @@ suite('VS Code Actions', () => { }); test('classic-window actions stay available unless a product opts out', () => { - // Guards the default: nothing binds this key, so an expression that is false when it is unset - // would hide these actions in stock VS Code. + // Nothing binds this key, so an expression that is false when unset would hide these actions. const context = (unavailable: boolean | undefined) => new class extends mock() { override getValue(key: string): T | undefined { return (key === 'sessionsClassicWindowUnavailable' ? unavailable : undefined) as T | undefined; @@ -57,6 +57,38 @@ suite('VS Code Actions', () => { }); }); + test('the sign-in dialog is not offered a return action when a product opts out', async () => { + const run = async (unavailable: boolean | undefined) => { + let queriedWindows = false; + const accessor: ServicesAccessor = { + get: (id: ServiceIdentifier): T => (id === IContextKeyService + ? new class extends mock() { + override getContextKeyValue(key: string): V | undefined { + return (key === 'sessionsClassicWindowUnavailable' ? unavailable : undefined) as V | undefined; + } + } + : new class extends mock() { + override async getWindows(): Promise { + queriedWindows = true; + return []; + } + }) as T + }; + return { shown: await new ShouldShowReturnToVSCodeEditorAction().run(accessor), queriedWindows }; + }; + + assert.deepStrictEqual({ + unset: await run(undefined), + optedIn: await run(false), + optedOut: await run(true), + }, { + unset: { shown: true, queriedWindows: true }, + optedIn: { shown: true, queriedWindows: true }, + // Opting out short-circuits before the native host is asked. + optedOut: { shown: false, queriedWindows: false }, + }); + }); + test('opens an editor window before closing the Agents window', async () => { const calls: string[] = []; const nativeHostService = new class extends mock() {