Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions src/vs/sessions/browser/actions/vscodeActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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()),
}]
});
}
Expand Down
7 changes: 7 additions & 0 deletions src/vs/sessions/common/contextkeys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,13 @@ export const SinglePaneFilesTabAvailableContext = new RawContextKey<boolean>('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<boolean>('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<boolean>('sessionsIsPhoneLayout', false, localize('sessionsIsPhoneLayout', "Whether the current layout is the phone layout"));
Expand Down
13 changes: 9 additions & 4 deletions src/vs/sessions/electron-browser/actions/vscodeActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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()),
}]
});
}
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -146,6 +147,10 @@ export class ShouldShowReturnToVSCodeEditorAction extends Action2 {
}

override async run(accessor: ServicesAccessor): Promise<boolean> {
// The sign-in dialog invokes the command directly, which no precondition can gate.
if (SessionsClassicWindowUnavailableContext.getValue(accessor.get(IContextKeyService)) === true) {

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, and it is the sharpest of the three: the test would have passed with that guard deleted. Covered in e67dc0f over unset, false and true, against ShouldShowReturnToVSCodeEditorAction.run() itself rather than a reconstruction — and the true case asserts queriedWindows === false, so the short-circuit before INativeHostService is pinned as you asked rather than implied by the return value.

return false;
}
const nativeHostService = accessor.get(INativeHostService);
const windows = await nativeHostService.getWindows({ includeAuxiliaryWindows: false });
return shouldShowReturnToVSCodeEditor(windows, getWindowId(mainWindow));
Expand Down
58 changes: 57 additions & 1 deletion src/vs/sessions/test/electron-browser/vscodeActions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand All @@ -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<IContext>() {
override getValue<T>(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: <T>(id: ServiceIdentifier<T>): T => (id === IContextKeyService
? new class extends mock<IContextKeyService>() {
override getContextKeyValue<V>(key: string): V | undefined {
return (key === 'sessionsClassicWindowUnavailable' ? unavailable : undefined) as V | undefined;
}
}
: new class extends mock<INativeHostService>() {
override async getWindows(): Promise<IOpenedMainWindow[]> {
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<INativeHostService>() {
Expand Down