Skip to content

Commit fb957a8

Browse files
connor4312Copilot
andcommitted
remote tunnels: fix agent window initialization
Prevent early command execution and false connection notifications in the Agents window. - Register the titlebar action after Remote Tunnel commands are available. - Seed the initial sharing state before enabling transition notifications. - Add tests for initial snapshots and status events during initialization. (Commit message generated by Copilot) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 40391b2 commit fb957a8

4 files changed

Lines changed: 226 additions & 85 deletions

File tree

src/vs/sessions/contrib/tunnelHost/electron-browser/tunnelHost.contribution.ts

Lines changed: 34 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -21,19 +21,7 @@ import { Menus } from '../../../browser/menus.js';
2121

2222
export const TOGGLE_SHARING_FROM_AGENTS_ID = 'sessions.tunnelHost.toggleSharingFromAgents';
2323

24-
MenuRegistry.appendMenuItem(Menus.TitleBarRightLayout, {
25-
command: {
26-
id: TOGGLE_SHARING_FROM_AGENTS_ID,
27-
title: localize('toggleSharing', "Allow Remote Connections"),
28-
icon: Codicon.radioTower,
29-
toggled: ContextKeyExpr.equals(TUNNEL_HOST_SHARING_KEY, true),
30-
},
31-
group: 'navigation',
32-
order: 90,
33-
when: ContextKeyExpr.and(ChatContextKeys.enabled, IsSessionsWindowContext, IsAuxiliaryWindowContext.toNegated())
34-
});
35-
36-
class SessionsTunnelHostTitlebarContribution extends Disposable implements IWorkbenchContribution {
24+
export class SessionsTunnelHostTitlebarContribution extends Disposable implements IWorkbenchContribution {
3725

3826
static readonly ID = 'workbench.contrib.sessionsTunnelHostTitlebar';
3927

@@ -43,30 +31,43 @@ class SessionsTunnelHostTitlebarContribution extends Disposable implements IWork
4331
) {
4432
super();
4533

34+
this._register(MenuRegistry.appendMenuItem(Menus.TitleBarRightLayout, {
35+
command: {
36+
id: TOGGLE_SHARING_FROM_AGENTS_ID,
37+
title: localize('toggleSharing', "Allow Remote Connections"),
38+
icon: Codicon.radioTower,
39+
toggled: ContextKeyExpr.equals(TUNNEL_HOST_SHARING_KEY, true),
40+
},
41+
group: 'navigation',
42+
order: 90,
43+
when: ContextKeyExpr.and(ChatContextKeys.enabled, IsSessionsWindowContext, IsAuxiliaryWindowContext.toNegated())
44+
}));
45+
46+
this._register(registerAction2(class ToggleRemoteConnectionsFromAgentsAction extends Action2 {
47+
constructor() {
48+
super({
49+
id: TOGGLE_SHARING_FROM_AGENTS_ID,
50+
title: localize('toggleSharing', "Allow Remote Connections"),
51+
icon: Codicon.radioTower,
52+
toggled: ContextKeyExpr.equals(TUNNEL_HOST_SHARING_KEY, true),
53+
});
54+
}
55+
56+
async run(accessor: ServicesAccessor): Promise<void> {
57+
await executeToggleRemoteConnections(
58+
accessor.get(IRemoteTunnelService),
59+
accessor.get(ICommandService),
60+
{ authenticationProviderId: 'github', showServiceOption: false, showSuccessNotification: false },
61+
);
62+
}
63+
}));
64+
4665
const viewItemFactory: IActionViewItemFactory = (action, _options, instantiationService) => {
4766
return instantiationService.createInstance(ToggleRemoteConnectionsActionViewItem, action);
4867
};
4968
this._register(actionViewItemService.register(Menus.TitleBarRightLayout, TOGGLE_SHARING_FROM_AGENTS_ID, viewItemFactory, remoteTunnelService.onDidChangeTunnelStatus));
5069
}
5170
}
5271

53-
registerAction2(class ToggleRemoteConnectionsFromAgentsAction extends Action2 {
54-
constructor() {
55-
super({
56-
id: TOGGLE_SHARING_FROM_AGENTS_ID,
57-
title: localize('toggleSharing', "Allow Remote Connections"),
58-
icon: Codicon.radioTower,
59-
toggled: ContextKeyExpr.equals(TUNNEL_HOST_SHARING_KEY, true),
60-
});
61-
}
62-
63-
async run(accessor: ServicesAccessor): Promise<void> {
64-
await executeToggleRemoteConnections(
65-
accessor.get(IRemoteTunnelService),
66-
accessor.get(ICommandService),
67-
{ authenticationProviderId: 'github', showServiceOption: false, showSuccessNotification: false },
68-
);
69-
}
70-
});
71-
72-
registerWorkbenchContribution2(SessionsTunnelHostTitlebarContribution.ID, SessionsTunnelHostTitlebarContribution, WorkbenchPhase.BlockRestore);
72+
// Remote Tunnel registers its delegated commands during the restored phase.
73+
registerWorkbenchContribution2(SessionsTunnelHostTitlebarContribution.ID, SessionsTunnelHostTitlebarContribution, WorkbenchPhase.Eventually);

src/vs/sessions/contrib/tunnelHost/test/electron-browser/tunnelHost.contribution.test.ts

Lines changed: 63 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { Codicon } from '../../../../../base/common/codicons.js';
88
import { ThemeIcon } from '../../../../../base/common/themables.js';
99
import { mock } from '../../../../../base/test/common/mock.js';
1010
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js';
11+
import { NullActionViewItemService } from '../../../../../platform/actions/browser/actionViewItemService.js';
1112
import { isIMenuItem, MenuId, MenuRegistry } from '../../../../../platform/actions/common/actions.js';
1213
import { CommandsRegistry, ICommandService } from '../../../../../platform/commands/common/commands.js';
1314
import type { ContextKeyExpression, ContextKeyValue } from '../../../../../platform/contextkey/common/contextkey.js';
@@ -18,7 +19,7 @@ import { IsAuxiliaryWindowContext, IsSessionsWindowContext, RemoteNameContext }
1819
import { Menus } from '../../../../browser/menus.js';
1920
import { RemoteTunnelCommandIds } from '../../../../../workbench/contrib/remoteTunnel/electron-browser/remoteTunnel.contribution.js';
2021
import { TOGGLE_SHARING_ID } from '../../../../../workbench/contrib/chat/electron-browser/tunnelHost.contribution.js';
21-
import { TOGGLE_SHARING_FROM_AGENTS_ID } from '../../electron-browser/tunnelHost.contribution.js';
22+
import { SessionsTunnelHostTitlebarContribution, TOGGLE_SHARING_FROM_AGENTS_ID } from '../../electron-browser/tunnelHost.contribution.js';
2223

2324
class TestRemoteTunnelService extends mock<IRemoteTunnelService>() {
2425
override getMode(): Promise<TunnelMode> {
@@ -43,11 +44,16 @@ suite('Sessions - Tunnel Host Contribution', () => {
4344

4445
ensureNoDisposablesAreLeakedInTestSuite();
4546

46-
test('remote connections toggle is in Agents titlebar and non-Agents chat input', () => {
47+
test('registers the remote connections toggle with the titlebar contribution', () => {
4748
const findToggle = (menu: MenuId, id: string) => MenuRegistry.getMenuItems(menu)
4849
.filter(isIMenuItem)
4950
.find(item => item.command.id === id);
5051

52+
assert.strictEqual(findToggle(Menus.TitleBarRightLayout, TOGGLE_SHARING_FROM_AGENTS_ID), undefined);
53+
assert.strictEqual(CommandsRegistry.getCommand(TOGGLE_SHARING_FROM_AGENTS_ID), undefined);
54+
55+
const contribution = new SessionsTunnelHostTitlebarContribution(new TestRemoteTunnelService(), new NullActionViewItemService());
56+
5157
const summarize = (menu: MenuId, id: string) => {
5258
const item = findToggle(menu, id);
5359
return item && {
@@ -57,58 +63,71 @@ suite('Sessions - Tunnel Host Contribution', () => {
5763
};
5864
};
5965

60-
assert.deepStrictEqual({
61-
titlebar: summarize(Menus.TitleBarRightLayout, TOGGLE_SHARING_FROM_AGENTS_ID),
62-
chatInput: summarize(MenuId.ChatInputSecondary, TOGGLE_SHARING_ID),
63-
}, {
64-
titlebar: { group: 'navigation', order: 90, icon: Codicon.radioTower.id },
65-
chatInput: { group: 'navigation', order: 10, icon: Codicon.radioTower.id },
66-
});
67-
68-
const titlebarToggle = findToggle(Menus.TitleBarRightLayout, TOGGLE_SHARING_FROM_AGENTS_ID);
69-
const chatInputToggle = findToggle(MenuId.ChatInputSecondary, TOGGLE_SHARING_ID);
70-
if (!titlebarToggle?.when || !chatInputToggle?.when) {
71-
assert.fail('remote connections menu items should have when clauses');
72-
}
66+
try {
67+
assert.deepStrictEqual({
68+
titlebar: summarize(Menus.TitleBarRightLayout, TOGGLE_SHARING_FROM_AGENTS_ID),
69+
chatInput: summarize(MenuId.ChatInputSecondary, TOGGLE_SHARING_ID),
70+
}, {
71+
titlebar: { group: 'navigation', order: 90, icon: Codicon.radioTower.id },
72+
chatInput: { group: 'navigation', order: 10, icon: Codicon.radioTower.id },
73+
});
74+
75+
const titlebarToggle = findToggle(Menus.TitleBarRightLayout, TOGGLE_SHARING_FROM_AGENTS_ID);
76+
const chatInputToggle = findToggle(MenuId.ChatInputSecondary, TOGGLE_SHARING_ID);
77+
if (!titlebarToggle?.when || !chatInputToggle?.when) {
78+
assert.fail('remote connections menu items should have when clauses');
79+
}
80+
81+
const evalWhen = (when: ContextKeyExpression, values: Record<string, ContextKeyValue>) => {
82+
return when.evaluate({ getValue: <T extends ContextKeyValue = ContextKeyValue>(key: string) => values[key] as T });
83+
};
84+
const agentHostChat = {
85+
[ChatContextKeys.enabled.key]: true,
86+
[ChatContextKeys.chatIsAgentHostSession.key]: true,
87+
[IsAuxiliaryWindowContext.key]: false,
88+
[RemoteNameContext.key]: '',
89+
};
7390

74-
const evalWhen = (when: ContextKeyExpression, values: Record<string, ContextKeyValue>) => {
75-
return when.evaluate({ getValue: <T extends ContextKeyValue = ContextKeyValue>(key: string) => values[key] as T });
76-
};
77-
const agentHostChat = {
78-
[ChatContextKeys.enabled.key]: true,
79-
[ChatContextKeys.chatIsAgentHostSession.key]: true,
80-
[IsAuxiliaryWindowContext.key]: false,
81-
[RemoteNameContext.key]: '',
82-
};
91+
assert.deepStrictEqual({
92+
agentsTitlebar: evalWhen(titlebarToggle.when, { ...agentHostChat, [IsSessionsWindowContext.key]: true }),
93+
editorTitlebar: evalWhen(titlebarToggle.when, { ...agentHostChat, [IsSessionsWindowContext.key]: false }),
94+
agentsChatInput: evalWhen(chatInputToggle.when, { ...agentHostChat, [IsSessionsWindowContext.key]: true }),
95+
editorChatInput: evalWhen(chatInputToggle.when, { ...agentHostChat, [IsSessionsWindowContext.key]: false }),
96+
remoteEditorChatInput: evalWhen(chatInputToggle.when, { ...agentHostChat, [IsSessionsWindowContext.key]: false, [RemoteNameContext.key]: 'ssh-remote' }),
97+
}, {
98+
agentsTitlebar: true,
99+
editorTitlebar: false,
100+
agentsChatInput: false,
101+
editorChatInput: true,
102+
remoteEditorChatInput: false,
103+
});
104+
} finally {
105+
contribution.dispose();
106+
}
83107

84-
assert.deepStrictEqual({
85-
agentsTitlebar: evalWhen(titlebarToggle.when, { ...agentHostChat, [IsSessionsWindowContext.key]: true }),
86-
editorTitlebar: evalWhen(titlebarToggle.when, { ...agentHostChat, [IsSessionsWindowContext.key]: false }),
87-
agentsChatInput: evalWhen(chatInputToggle.when, { ...agentHostChat, [IsSessionsWindowContext.key]: true }),
88-
editorChatInput: evalWhen(chatInputToggle.when, { ...agentHostChat, [IsSessionsWindowContext.key]: false }),
89-
remoteEditorChatInput: evalWhen(chatInputToggle.when, { ...agentHostChat, [IsSessionsWindowContext.key]: false, [RemoteNameContext.key]: 'ssh-remote' }),
90-
}, {
91-
agentsTitlebar: true,
92-
editorTitlebar: false,
93-
agentsChatInput: false,
94-
editorChatInput: true,
95-
remoteEditorChatInput: false,
96-
});
108+
assert.strictEqual(findToggle(Menus.TitleBarRightLayout, TOGGLE_SHARING_FROM_AGENTS_ID), undefined);
109+
assert.strictEqual(CommandsRegistry.getCommand(TOGGLE_SHARING_FROM_AGENTS_ID), undefined);
97110
});
98111

99112
test('Agents turn-on forces GitHub without offering service installation', async () => {
100113
const instantiationService = new TestInstantiationService();
101114
const commandService = new TestCommandService();
102115
instantiationService.stub(IRemoteTunnelService, new TestRemoteTunnelService());
103116
instantiationService.stub(ICommandService, commandService);
104-
const command = CommandsRegistry.getCommand(TOGGLE_SHARING_FROM_AGENTS_ID);
105-
assert.ok(command);
117+
const contribution = new SessionsTunnelHostTitlebarContribution(new TestRemoteTunnelService(), new NullActionViewItemService());
106118

107-
await instantiationService.invokeFunction(command.handler);
119+
try {
120+
const command = CommandsRegistry.getCommand(TOGGLE_SHARING_FROM_AGENTS_ID);
121+
assert.ok(command);
108122

109-
assert.deepStrictEqual(commandService.calls, [{
110-
id: RemoteTunnelCommandIds.turnOn,
111-
args: [{ authenticationProviderId: 'github', showServiceOption: false, showSuccessNotification: false }],
112-
}]);
123+
await instantiationService.invokeFunction(command.handler);
124+
125+
assert.deepStrictEqual(commandService.calls, [{
126+
id: RemoteTunnelCommandIds.turnOn,
127+
args: [{ authenticationProviderId: 'github', showServiceOption: false, showSuccessNotification: false }],
128+
}]);
129+
} finally {
130+
contribution.dispose();
131+
}
113132
});
114133
});

src/vs/workbench/contrib/chat/electron-browser/toggleRemoteConnectionsActionViewItem.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ export class ToggleRemoteConnectionsActionViewItem extends BaseActionViewItem {
4545
private _status: TunnelStatus = { type: 'uninitialized' };
4646
private _hasReceivedMode = false;
4747
private _hasReceivedStatus = false;
48+
private _hasInitializedState = false;
4849

4950
constructor(
5051
action: IAction,
@@ -104,10 +105,12 @@ export class ToggleRemoteConnectionsActionViewItem extends BaseActionViewItem {
104105
this.element.setAttribute('aria-label', this._getAriaLabel());
105106
this.element.setAttribute('aria-pressed', String(state.isSharing));
106107

107-
if (state.isSharing && !this._wasSharing && !state.isConnecting) {
108-
this._showToast();
109-
} else if (!state.isSharing && this._wasSharing) {
110-
this._hideToast();
108+
if (this._hasInitializedState) {
109+
if (state.isSharing && !this._wasSharing && !state.isConnecting) {
110+
this._showToast();
111+
} else if (!state.isSharing && this._wasSharing) {
112+
this._hideToast();
113+
}
111114
}
112115

113116
this._wasSharing = state.isSharing;
@@ -125,6 +128,7 @@ export class ToggleRemoteConnectionsActionViewItem extends BaseActionViewItem {
125128
this._status = status;
126129
}
127130
this._updateState();
131+
this._hasInitializedState = true;
128132
}
129133

130134
private _showToast(): void {

0 commit comments

Comments
 (0)