diff --git a/src/vs/sessions/browser/actions/vscodeActions.ts b/src/vs/sessions/browser/actions/vscodeActions.ts index 59d159c103547..620be7f2fe144 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, 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()), + precondition: ContextKeyExpr.and(IsAuxiliaryWindowContext.toNegated(), SessionsWelcomeVisibleContext.toNegated(), SessionsClassicWindowUnavailableContext.toNegated()), menu: [{ id: Menus.TitleBarCenterRight, group: 'navigation', order: 7, - when: ContextKeyExpr.and(IsAuxiliaryWindowContext.toNegated(), SessionsWelcomeVisibleContext.toNegated(), 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 b998a1d4d558e..2147f03e84198 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 --- > + +/** 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 + //#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 188a45b48de52..c4cafa63fbe04 100644 --- a/src/vs/sessions/electron-browser/actions/vscodeActions.ts +++ b/src/vs/sessions/electron-browser/actions/vscodeActions.ts @@ -12,11 +12,11 @@ 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'; -import { IsPhoneLayoutContext, 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()), + precondition: ContextKeyExpr.and(IsAuxiliaryWindowContext.toNegated(), SessionsWelcomeVisibleContext.toNegated(), SessionsClassicWindowUnavailableContext.toNegated()), menu: [{ id: Menus.TitleBarCenterRight, group: 'navigation', order: 7, - when: ContextKeyExpr.and(IsAuxiliaryWindowContext.toNegated(), SessionsWelcomeVisibleContext.toNegated(), IsPhoneLayoutContext.negate()), + when: ContextKeyExpr.and(IsAuxiliaryWindowContext.toNegated(), SessionsWelcomeVisibleContext.toNegated(), SessionsClassicWindowUnavailableContext.toNegated(), IsPhoneLayoutContext.negate()), }] }); } @@ -98,6 +98,7 @@ export class OpenVSCodeWindowAction extends Action2 { super({ id: OpenVSCodeWindowAction.ID, title: localize2('openVSCodeWindow', 'Open VS Code Window'), + precondition: SessionsClassicWindowUnavailableContext.toNegated(), f1: true, keybinding: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyA, @@ -146,6 +147,10 @@ export class ShouldShowReturnToVSCodeEditorAction extends Action2 { } override async run(accessor: ServicesAccessor): Promise { + // 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); const windows = await nativeHostService.getWindows({ includeAuxiliaryWindows: false }); return shouldShowReturnToVSCodeEditor(windows, getWindowId(mainWindow)); diff --git a/src/vs/sessions/test/electron-browser/vscodeActions.test.ts b/src/vs/sessions/test/electron-browser/vscodeActions.test.ts index 10267fdeb4334..19b36ba689b89 100644 --- a/src/vs/sessions/test/electron-browser/vscodeActions.test.ts +++ b/src/vs/sessions/test/electron-browser/vscodeActions.test.ts @@ -10,7 +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, returnToVSCodeEditor, shouldShowReturnToVSCodeEditor } from '../../electron-browser/actions/vscodeActions.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', () => { @@ -33,6 +35,60 @@ suite('VS Code Actions', () => { }); }); + test('classic-window actions stay available unless a product opts out', () => { + // 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; + } + }; + // 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('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() {