Skip to content

Commit 714d1f5

Browse files
committed
Chat menu e2e
1 parent d1b8f7f commit 714d1f5

File tree

10 files changed

+112
-22
lines changed

10 files changed

+112
-22
lines changed

src/vs/base/common/marshallingIds.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,5 @@ export const enum MarshalledId {
2727
LanguageModelTextPart,
2828
LanguageModelPromptTsxPart,
2929
LanguageModelDataPart,
30+
ChatSessionContext,
3031
}

src/vs/platform/actions/common/actions.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,7 @@ export class MenuId {
256256
static readonly ChatTextEditorMenu = new MenuId('ChatTextEditorMenu');
257257
static readonly ChatTerminalMenu = new MenuId('ChatTerminalMenu');
258258
static readonly ChatToolOutputResourceContext = new MenuId('ChatToolOutputResourceContext');
259+
static readonly ChatSessionsMenu = new MenuId('ChatSessionsMenu');
259260
static readonly AccessibleView = new MenuId('AccessibleView');
260261
static readonly MultiDiffEditorFileToolbar = new MenuId('MultiDiffEditorFileToolbar');
261262
static readonly DiffEditorHunkToolbar = new MenuId('DiffEditorHunkToolbar');

src/vs/workbench/api/browser/mainThreadChatSessions.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,10 @@ export class MainThreadChatSessions extends Disposable implements MainThreadChat
2323
super();
2424
}
2525

26-
$registerChatSessionsProvider(handle: number): void {
26+
$registerChatSessionsProvider(handle: number, chatSessionType: string): void {
2727
// Register the provider handle - this tracks that a provider exists
2828
const provider: IChatSessionsProvider = {
29+
chatSessionType,
2930
provideChatSessions: (token) => this._provideChatSessionsInformation(handle, token)
3031
};
3132
this._registrations.set(handle, this._chatSessionsService.registerChatSessionsProvider(handle, provider));

src/vs/workbench/api/common/extHost.api.impl.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ import { ExtHostWebviewViews } from './extHostWebviewView.js';
112112
import { IExtHostWindow } from './extHostWindow.js';
113113
import { IExtHostWorkspace } from './extHostWorkspace.js';
114114
import { ExtHostAiSettingsSearch } from './extHostAiSettingsSearch.js';
115-
import { IExtHostChatSessions } from './extHostChatSessions.js';
115+
import { ExtHostChatSessions } from './extHostChatSessions.js';
116116

117117
export interface IExtensionRegistries {
118118
mine: ExtensionDescriptionRegistry;
@@ -154,7 +154,6 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
154154
const extHostLanguageModels = accessor.get(IExtHostLanguageModels);
155155
const extHostMcp = accessor.get(IExtHostMpcService);
156156
const extHostDataChannels = accessor.get(IExtHostDataChannels);
157-
const extHostChatSessions = accessor.get(IExtHostChatSessions);
158157

159158
// register addressable instances
160159
rpcProtocol.set(ExtHostContext.ExtHostFileSystemInfo, extHostFileSystemInfo);
@@ -174,7 +173,6 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
174173
rpcProtocol.set(ExtHostContext.ExtHostAuthentication, extHostAuthentication);
175174
rpcProtocol.set(ExtHostContext.ExtHostChatProvider, extHostLanguageModels);
176175
rpcProtocol.set(ExtHostContext.ExtHostDataChannels, extHostDataChannels);
177-
rpcProtocol.set(ExtHostContext.ExtHostChatSessions, extHostChatSessions);
178176

179177
// automatically create and register addressable instances
180178
const extHostDecorations = rpcProtocol.set(ExtHostContext.ExtHostDecorations, accessor.get(IExtHostDecorations));
@@ -231,6 +229,8 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
231229
const extHostStatusBar = rpcProtocol.set(ExtHostContext.ExtHostStatusBar, new ExtHostStatusBar(rpcProtocol, extHostCommands.converter));
232230
const extHostSpeech = rpcProtocol.set(ExtHostContext.ExtHostSpeech, new ExtHostSpeech(rpcProtocol));
233231
const extHostEmbeddings = rpcProtocol.set(ExtHostContext.ExtHostEmbeddings, new ExtHostEmbeddings(rpcProtocol));
232+
const extHostChatSessions = rpcProtocol.set(ExtHostContext.ExtHostChatSessions, new ExtHostChatSessions(extHostCommands, rpcProtocol, extHostLogService));
233+
234234
rpcProtocol.set(ExtHostContext.ExtHostMcp, accessor.get(IExtHostMpcService));
235235

236236
// Check that no named customers are missing

src/vs/workbench/api/common/extHost.common.services.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ import { ExtHostMcpService, IExtHostMpcService } from './extHostMcp.js';
3434
import { ExtHostUrls, IExtHostUrlsService } from './extHostUrls.js';
3535
import { ExtHostProgress, IExtHostProgress } from './extHostProgress.js';
3636
import { ExtHostDataChannels, IExtHostDataChannels } from './extHostDataChannels.js';
37-
import { ExtHostChatSessions, IExtHostChatSessions } from './extHostChatSessions.js';
3837

3938
registerSingleton(IExtHostLocalizationService, ExtHostLocalizationService, InstantiationType.Delayed);
4039
registerSingleton(ILoggerService, ExtHostLoggerService, InstantiationType.Delayed);
@@ -65,4 +64,3 @@ registerSingleton(IExtHostEditorTabs, ExtHostEditorTabs, InstantiationType.Eager
6564
registerSingleton(IExtHostVariableResolverProvider, ExtHostVariableResolverProviderService, InstantiationType.Eager);
6665
registerSingleton(IExtHostMpcService, ExtHostMcpService, InstantiationType.Eager);
6766
registerSingleton(IExtHostDataChannels, ExtHostDataChannels, InstantiationType.Eager);
68-
registerSingleton(IExtHostChatSessions, ExtHostChatSessions, InstantiationType.Eager);

src/vs/workbench/api/common/extHost.protocol.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3102,7 +3102,7 @@ export interface MainThreadChatStatusShape {
31023102
}
31033103

31043104
export interface MainThreadChatSessionsShape extends IDisposable {
3105-
$registerChatSessionsProvider(handle: number): void;
3105+
$registerChatSessionsProvider(handle: number, chatSessionType: string): void;
31063106
$unregisterChatSessionsProvider(handle: number): void;
31073107
}
31083108

src/vs/workbench/api/common/extHostChatSessions.ts

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ import { ExtHostChatSessionsShape, MainContext, MainThreadChatSessionsShape } fr
1010
import type * as vscode from 'vscode';
1111
import { ILogService } from '../../../platform/log/common/log.js';
1212
import { Proxied } from '../../services/extensions/common/proxyIdentifier.js';
13+
import { ExtHostCommands } from './extHostCommands.js';
14+
import { MarshalledId } from '../../../base/common/marshallingIds.js';
15+
import { URI } from '../../../base/common/uri.js';
1316

1417
export interface IExtHostChatSessions extends ExtHostChatSessionsShape {
1518
registerChatSessionsProvider(provider: vscode.ChatSessionsProvider): vscode.Disposable;
@@ -23,21 +26,40 @@ export class ExtHostChatSessions extends Disposable implements IExtHostChatSessi
2326
private readonly _proxy: Proxied<MainThreadChatSessionsShape>;
2427
private readonly _statusProviders = new Map<number, { provider: vscode.ChatSessionsProvider; disposable: DisposableStore }>();
2528
private _nextHandle = 0;
29+
private _sessionMap: Map<string, vscode.ChatSessionContent & Record<string, unknown>> = new Map();
2630

2731
constructor(
32+
commands: ExtHostCommands,
2833
@IExtHostRpcService private readonly _extHostRpc: IExtHostRpcService,
2934
@ILogService private readonly _logService: ILogService,
3035
) {
3136
super();
3237
this._proxy = this._extHostRpc.getProxy(MainContext.MainThreadChatSessions);
38+
39+
commands.registerArgumentProcessor({
40+
processArgument: (arg) => {
41+
if (arg && arg.$mid === MarshalledId.ChatSessionContext) {
42+
const id = this.uriToId(arg.uri);
43+
const sessionContent = this._sessionMap.get(id);
44+
if (sessionContent) {
45+
return sessionContent;
46+
} else {
47+
this._logService.warn(`No chat session found for URI: ${id}`);
48+
return arg;
49+
}
50+
}
51+
52+
return arg;
53+
}
54+
});
3355
}
3456

3557
registerChatSessionsProvider(provider: vscode.ChatSessionsProvider): vscode.Disposable {
3658
const handle = this._nextHandle++;
3759
const disposables = new DisposableStore();
3860

3961
this._statusProviders.set(handle, { provider, disposable: disposables });
40-
this._proxy.$registerChatSessionsProvider(handle);
62+
this._proxy.$registerChatSessionsProvider(handle, provider.chatSessionType);
4163

4264
return {
4365
dispose: () => {
@@ -56,6 +78,20 @@ export class ExtHostChatSessions extends Disposable implements IExtHostChatSessi
5678
return [];
5779
}
5880

59-
return await entry.provider.provideChatSessions(token);
81+
const session = await entry.provider.provideChatSessions(token);
82+
for (const sessionContent of session) {
83+
if (sessionContent.uri) {
84+
this._sessionMap.set(
85+
this.uriToId(sessionContent.uri),
86+
sessionContent as vscode.ChatSessionContent & Record<string, unknown>
87+
);
88+
}
89+
}
90+
91+
return session;
92+
}
93+
94+
private uriToId(uri: URI): string {
95+
return `${uri.scheme}+${uri.authority}+${uri.path}`;
6096
}
6197
}

src/vs/workbench/contrib/chat/browser/actions/chatActions.ts

Lines changed: 53 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { Event } from '../../../../../base/common/event.js';
1414
import { MarkdownString } from '../../../../../base/common/htmlContent.js';
1515
import { KeyCode, KeyMod } from '../../../../../base/common/keyCodes.js';
1616
import { Disposable, DisposableStore, markAsSingleton } from '../../../../../base/common/lifecycle.js';
17+
import { MarshalledId } from '../../../../../base/common/marshallingIds.js';
1718
import { language } from '../../../../../base/common/platform.js';
1819
import { ThemeIcon } from '../../../../../base/common/themables.js';
1920
import { URI } from '../../../../../base/common/uri.js';
@@ -24,10 +25,11 @@ import { SuggestController } from '../../../../../editor/contrib/suggest/browser
2425
import { localize, localize2 } from '../../../../../nls.js';
2526
import { IActionViewItemService } from '../../../../../platform/actions/browser/actionViewItemService.js';
2627
import { DropdownWithPrimaryActionViewItem } from '../../../../../platform/actions/browser/dropdownWithPrimaryActionViewItem.js';
27-
import { Action2, ICommandPaletteOptions, MenuId, MenuItemAction, MenuRegistry, registerAction2, SubmenuItemAction } from '../../../../../platform/actions/common/actions.js';
28+
import { getContextMenuActions } from '../../../../../platform/actions/browser/menuEntryActionViewItem.js';
29+
import { Action2, ICommandPaletteOptions, IMenuService, MenuId, MenuItemAction, MenuRegistry, registerAction2, SubmenuItemAction } from '../../../../../platform/actions/common/actions.js';
2830
import { ICommandService } from '../../../../../platform/commands/common/commands.js';
2931
import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';
30-
import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js';
32+
import { ContextKeyExpr, IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js';
3133
import { IsLinuxContext, IsWindowsContext } from '../../../../../platform/contextkey/common/contextkeys.js';
3234
import { IDialogService } from '../../../../../platform/dialogs/common/dialogs.js';
3335
import { IFileService } from '../../../../../platform/files/common/files.js';
@@ -475,6 +477,8 @@ export function registerChatActions() {
475477
editorService: IEditorService,
476478
view: ChatViewPane,
477479
chatSessionsService: IChatSessionsService,
480+
contextKeyService: IContextKeyService,
481+
menuService: IMenuService,
478482
showAllChats: boolean = false,
479483
showAllAgents: boolean = false
480484
) => {
@@ -501,6 +505,7 @@ export function registerChatActions() {
501505
}
502506

503507
interface ICodingAgentPickerItem extends IChatPickerItem {
508+
id?: string;
504509
uri?: URI;
505510
}
506511

@@ -560,9 +565,22 @@ export function registerChatActions() {
560565
const cancellationToken = new CancellationTokenSource();
561566

562567
try {
563-
const chatSessions = await chatSessionsService.provideChatSessions(cancellationToken.token);
564-
565-
for (const sessionContent of chatSessions) {
568+
const sessions = await chatSessionsService.provideChatSessions(cancellationToken.token);
569+
570+
for (const session of sessions) {
571+
const sessionContent = session.session;
572+
const provider = session.provider;
573+
574+
const ckey = contextKeyService.createKey('chatSessionType', provider.chatSessionType);
575+
const actions = menuService.getMenuActions(MenuId.ChatSessionsMenu, contextKeyService);
576+
const menuActions = getContextMenuActions(actions, 'navigation');
577+
ckey.reset();
578+
579+
const buttons = menuActions.secondary.map(action => ({
580+
id: action.id,
581+
tooltip: action.tooltip,
582+
iconClass: action.class || ThemeIcon.asClassName(Codicon.symbolClass),
583+
}));
566584
// Create agent pick from the session content
567585
const agentPick: ICodingAgentPickerItem = {
568586
label: sessionContent.label,
@@ -573,7 +591,7 @@ export function registerChatActions() {
573591
isActive: false,
574592
lastMessageDate: 0,
575593
},
576-
buttons: [],
594+
buttons,
577595
uri: sessionContent.uri
578596
};
579597

@@ -602,7 +620,7 @@ export function registerChatActions() {
602620
currentPicks.push(...agentPicks);
603621

604622
// Add "Show more..." if needed and not showing all agents
605-
if (!showAllAgents && chatSessions.length > 5) {
623+
if (!showAllAgents && sessions.length > 5) {
606624
currentPicks.push({
607625
label: localize('chat.history.showMoreAgents', 'Show more...'),
608626
description: '',
@@ -716,9 +734,20 @@ export function registerChatActions() {
716734
editorService,
717735
view,
718736
chatSessionsService,
737+
contextKeyService,
738+
menuService,
719739
showAllChats,
720740
showAllAgents
721741
);
742+
} else {
743+
const buttonItem = context.button as ICodingAgentPickerItem;
744+
if (buttonItem.id) {
745+
const contextItem = context.item as ICodingAgentPickerItem;
746+
commandService.executeCommand(buttonItem.id, {
747+
uri: contextItem.uri,
748+
$mid: MarshalledId.ChatSessionContext
749+
});
750+
}
722751
}
723752
}));
724753
store.add(picker.onDidAccept(async () => {
@@ -737,6 +766,8 @@ export function registerChatActions() {
737766
editorService,
738767
view,
739768
chatSessionsService,
769+
contextKeyService,
770+
menuService,
740771
true,
741772
showAllAgents
742773
);
@@ -751,10 +782,13 @@ export function registerChatActions() {
751782
editorService,
752783
view,
753784
chatSessionsService,
785+
contextKeyService,
786+
menuService,
754787
showAllChats,
755788
true
756789
);
757790
return;
791+
} else if ((item as ICodingAgentPickerItem).uri !== undefined) {
758792
} else if ((item as ICodingAgentPickerItem).uri !== undefined) {
759793
// TODO: handle click
760794
return;
@@ -779,6 +813,8 @@ export function registerChatActions() {
779813
const commandService = accessor.get(ICommandService);
780814
const chatSessionsService = accessor.get(IChatSessionsService);
781815
const configurationService = accessor.get(IConfigurationService);
816+
const contextKeyService = accessor.get(IContextKeyService);
817+
const menuService = accessor.get(IMenuService);
782818

783819
const view = await viewsService.openView<ChatViewPane>(ChatViewId);
784820
if (!view) {
@@ -800,7 +836,16 @@ export function registerChatActions() {
800836

801837
const showAgentSessionsMenuConfig = configurationService.getValue<string>(ChatConfiguration.AgentSessionsViewLocation);
802838
if (showAgentSessionsMenuConfig === 'showChatsMenu' && chatSessionsService.hasChatSessionsProviders) {
803-
await this.showIntegratedPicker(chatService, quickInputService, commandService, editorService, view, chatSessionsService);
839+
await this.showIntegratedPicker(
840+
chatService,
841+
quickInputService,
842+
commandService,
843+
editorService,
844+
view,
845+
chatSessionsService,
846+
contextKeyService,
847+
menuService
848+
);
804849
} else {
805850
await this.showLegacyPicker(chatService, quickInputService, commandService, editorService, view);
806851
}

src/vs/workbench/contrib/chat/common/chatSessionsService.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,15 @@ export interface IChatSessionContent {
2121
}
2222

2323
export interface IChatSessionsProvider {
24+
readonly chatSessionType: string;
2425
provideChatSessions(token: CancellationToken): Promise<IChatSessionContent[]>;
2526
}
2627

2728
export interface IChatSessionsService {
2829
readonly _serviceBrand: undefined;
2930
registerChatSessionsProvider(handle: number, provider: IChatSessionsProvider): IDisposable;
3031
hasChatSessionsProviders: boolean;
31-
provideChatSessions(token: CancellationToken): Promise<IChatSessionContent[]>;
32+
provideChatSessions(token: CancellationToken): Promise<{ provider: IChatSessionsProvider; session: IChatSessionContent }[]>;
3233
}
3334

3435
export const IChatSessionsService = createDecorator<IChatSessionsService>('chatSessionsService');
@@ -43,19 +44,19 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ
4344
super();
4445
}
4546

46-
public async provideChatSessions(token: CancellationToken): Promise<IChatSessionContent[]> {
47-
const results: IChatSessionContent[] = [];
47+
public async provideChatSessions(token: CancellationToken): Promise<{ provider: IChatSessionsProvider; session: IChatSessionContent }[]> {
48+
const results: { provider: IChatSessionsProvider; session: IChatSessionContent }[] = [];
4849

4950
// Iterate through all registered providers and collect their results
5051
for (const [handle, provider] of this._providers) {
5152
try {
5253
if (provider.provideChatSessions) {
53-
results.push(...await provider.provideChatSessions(token));
54+
const sessions = await provider.provideChatSessions(token);
55+
results.push(...sessions.map(session => ({ provider, session })));
5456
}
5557
} catch (error) {
5658
this._logService.error(`Error getting chat sessions from provider ${handle}:`, error);
5759
}
58-
5960
if (token.isCancellationRequested) {
6061
break;
6162
}

src/vs/workbench/services/actions/common/menusExtensionPoint.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -451,6 +451,13 @@ const apiMenus: IAPIMenu[] = [
451451
supportsSubmenus: false,
452452
proposed: 'chatParticipantPrivate'
453453
},
454+
{
455+
key: 'chat/chatSessions',
456+
id: MenuId.ChatSessionsMenu,
457+
description: localize('menus.chatSessions', "The Chat Sessions menu."),
458+
supportsSubmenus: false,
459+
proposed: 'chatSessionsProvider'
460+
},
454461
{
455462
key: 'editor/context/chat',
456463
id: MenuId.ChatTextEditorMenu,

0 commit comments

Comments
 (0)