Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/chat/browser/actions/chatSessionActions.ts
3296 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { localize } from '../../../../../nls.js';
7
import { Action2, MenuId, MenuRegistry } from '../../../../../platform/actions/common/actions.js';
8
import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js';
9
import { ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js';
10
import { KeyCode } from '../../../../../base/common/keyCodes.js';
11
import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js';
12
import { IChatService } from '../../common/chatService.js';
13
import { IChatSessionItem, IChatSessionsService } from '../../common/chatSessionsService.js';
14
import { ILogService } from '../../../../../platform/log/common/log.js';
15
import Severity from '../../../../../base/common/severity.js';
16
import { ChatContextKeys } from '../../common/chatContextKeys.js';
17
import { MarshalledId } from '../../../../../base/common/marshallingIds.js';
18
import { ChatEditorInput } from '../chatEditorInput.js';
19
import { CHAT_CATEGORY } from './chatActions.js';
20
import { AUX_WINDOW_GROUP, IEditorService, SIDE_GROUP } from '../../../../services/editor/common/editorService.js';
21
import { IChatEditorOptions } from '../chatEditor.js';
22
import { ChatSessionUri } from '../../common/chatUri.js';
23
import { ILocalChatSessionItem, VIEWLET_ID } from '../chatSessions.js';
24
import { IViewsService } from '../../../../services/views/common/viewsService.js';
25
import { ChatViewId } from '../chat.js';
26
import { ChatViewPane } from '../chatViewPane.js';
27
import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';
28
import { ChatConfiguration } from '../../common/constants.js';
29
import { Codicon } from '../../../../../base/common/codicons.js';
30
import { IDialogService } from '../../../../../platform/dialogs/common/dialogs.js';
31
32
export interface IChatSessionContext {
33
sessionId: string;
34
sessionType: 'editor' | 'widget';
35
currentTitle: string;
36
editorInput?: any;
37
editorGroup?: any;
38
widget?: any;
39
}
40
41
interface IMarshalledChatSessionContext {
42
$mid: MarshalledId.ChatSessionContext;
43
session: {
44
id: string;
45
label: string;
46
editor?: ChatEditorInput;
47
widget?: any;
48
sessionType?: 'editor' | 'widget';
49
};
50
}
51
52
function isLocalChatSessionItem(item: IChatSessionItem): item is ILocalChatSessionItem {
53
return ('editor' in item && 'group' in item) || ('widget' in item && 'sessionType' in item);
54
}
55
56
function isMarshalledChatSessionContext(obj: unknown): obj is IMarshalledChatSessionContext {
57
return !!obj &&
58
typeof obj === 'object' &&
59
'$mid' in obj &&
60
(obj as any).$mid === MarshalledId.ChatSessionContext &&
61
'session' in obj;
62
}
63
64
export class RenameChatSessionAction extends Action2 {
65
static readonly id = 'workbench.action.chat.renameSession';
66
67
constructor() {
68
super({
69
id: RenameChatSessionAction.id,
70
title: localize('renameSession', "Rename"),
71
f1: false,
72
category: CHAT_CATEGORY,
73
icon: Codicon.pencil,
74
keybinding: {
75
weight: KeybindingWeight.WorkbenchContrib,
76
primary: KeyCode.F2,
77
when: ContextKeyExpr.equals('focusedView', 'workbench.view.chat.sessions.local')
78
}
79
});
80
}
81
82
async run(accessor: ServicesAccessor, context?: IChatSessionContext | IMarshalledChatSessionContext): Promise<void> {
83
if (!context) {
84
return;
85
}
86
87
// Handle marshalled context from menu actions
88
let sessionContext: IChatSessionContext;
89
if (isMarshalledChatSessionContext(context)) {
90
const session = context.session;
91
// Extract actual session ID based on session type
92
let actualSessionId: string | undefined;
93
const currentTitle = session.label;
94
95
// For history sessions, we need to extract the actual session ID
96
if (session.id.startsWith('history-')) {
97
actualSessionId = session.id.replace('history-', '');
98
} else if (session.sessionType === 'editor' && session.editor instanceof ChatEditorInput) {
99
actualSessionId = session.editor.sessionId;
100
} else if (session.sessionType === 'widget' && session.widget) {
101
actualSessionId = session.widget.viewModel?.model.sessionId;
102
} else {
103
// Fall back to using the session ID directly
104
actualSessionId = session.id;
105
}
106
107
if (!actualSessionId) {
108
return; // Can't proceed without a session ID
109
}
110
111
sessionContext = {
112
sessionId: actualSessionId,
113
sessionType: session.sessionType || 'editor',
114
currentTitle: currentTitle,
115
editorInput: session.editor,
116
widget: session.widget
117
};
118
} else {
119
sessionContext = context;
120
}
121
122
const chatSessionsService = accessor.get(IChatSessionsService);
123
const logService = accessor.get(ILogService);
124
const chatService = accessor.get(IChatService);
125
126
try {
127
// Find the chat sessions view and trigger inline rename mode
128
// This is similar to how file renaming works in the explorer
129
await chatSessionsService.setEditableSession(sessionContext.sessionId, {
130
validationMessage: (value: string) => {
131
if (!value || value.trim().length === 0) {
132
return { content: localize('renameSession.emptyName', "Name cannot be empty"), severity: Severity.Error };
133
}
134
if (value.length > 100) {
135
return { content: localize('renameSession.nameTooLong', "Name is too long (maximum 100 characters)"), severity: Severity.Error };
136
}
137
return null;
138
},
139
placeholder: localize('renameSession.placeholder', "Enter new name for chat session"),
140
startingValue: sessionContext.currentTitle,
141
onFinish: async (value: string, success: boolean) => {
142
if (success && value && value.trim() !== sessionContext.currentTitle) {
143
try {
144
const newTitle = value.trim();
145
chatService.setChatSessionTitle(sessionContext.sessionId, newTitle);
146
// Notify the local sessions provider that items have changed
147
chatSessionsService.notifySessionItemsChanged('local');
148
} catch (error) {
149
logService.error(
150
localize('renameSession.error', "Failed to rename chat session: {0}",
151
(error instanceof Error ? error.message : String(error)))
152
);
153
}
154
}
155
await chatSessionsService.setEditableSession(sessionContext.sessionId, null);
156
}
157
});
158
} catch (error) {
159
logService.error('Failed to rename chat session', error instanceof Error ? error.message : String(error));
160
}
161
}
162
}
163
164
/**
165
* Action to delete a chat session from history
166
*/
167
export class DeleteChatSessionAction extends Action2 {
168
static readonly id = 'workbench.action.chat.deleteSession';
169
170
constructor() {
171
super({
172
id: DeleteChatSessionAction.id,
173
title: localize('deleteSession', "Delete"),
174
f1: false,
175
category: CHAT_CATEGORY,
176
icon: Codicon.x,
177
});
178
}
179
180
async run(accessor: ServicesAccessor, context?: IChatSessionContext | IMarshalledChatSessionContext): Promise<void> {
181
if (!context) {
182
return;
183
}
184
185
// Handle marshalled context from menu actions
186
let sessionContext: IChatSessionContext;
187
if (isMarshalledChatSessionContext(context)) {
188
const session = context.session;
189
// Extract actual session ID based on session type
190
let actualSessionId: string | undefined;
191
const currentTitle = session.label;
192
193
// For history sessions, we need to extract the actual session ID
194
if (session.id.startsWith('history-')) {
195
actualSessionId = session.id.replace('history-', '');
196
} else if (session.sessionType === 'editor' && session.editor instanceof ChatEditorInput) {
197
actualSessionId = session.editor.sessionId;
198
} else if (session.sessionType === 'widget' && session.widget) {
199
actualSessionId = session.widget.viewModel?.model.sessionId;
200
} else {
201
// Fall back to using the session ID directly
202
actualSessionId = session.id;
203
}
204
205
if (!actualSessionId) {
206
return; // Can't proceed without a session ID
207
}
208
209
sessionContext = {
210
sessionId: actualSessionId,
211
sessionType: session.sessionType || 'editor',
212
currentTitle: currentTitle,
213
editorInput: session.editor,
214
widget: session.widget
215
};
216
} else {
217
sessionContext = context;
218
}
219
220
const chatService = accessor.get(IChatService);
221
const dialogService = accessor.get(IDialogService);
222
const logService = accessor.get(ILogService);
223
const chatSessionsService = accessor.get(IChatSessionsService);
224
225
try {
226
// Show confirmation dialog
227
const result = await dialogService.confirm({
228
message: localize('deleteSession.confirm', "Are you sure you want to delete this chat session?"),
229
detail: localize('deleteSession.detail', "This action cannot be undone."),
230
primaryButton: localize('deleteSession.delete', "Delete"),
231
type: 'warning'
232
});
233
234
if (result.confirmed) {
235
await chatService.removeHistoryEntry(sessionContext.sessionId);
236
// Notify the local sessions provider that items have changed
237
chatSessionsService.notifySessionItemsChanged('local');
238
}
239
} catch (error) {
240
logService.error('Failed to delete chat session', error instanceof Error ? error.message : String(error));
241
}
242
}
243
}
244
245
/**
246
* Action to open a chat session in a new window
247
*/
248
export class OpenChatSessionInNewWindowAction extends Action2 {
249
static readonly id = 'workbench.action.chat.openSessionInNewWindow';
250
251
constructor() {
252
super({
253
id: OpenChatSessionInNewWindowAction.id,
254
title: localize('chat.openSessionInNewWindow.label', "Open Chat in New Window"),
255
category: CHAT_CATEGORY,
256
f1: false,
257
});
258
}
259
260
async run(accessor: ServicesAccessor, context?: IChatSessionContext | IMarshalledChatSessionContext): Promise<void> {
261
if (!context) {
262
return;
263
}
264
265
const editorService = accessor.get(IEditorService);
266
let sessionId: string;
267
let sessionItem: IChatSessionItem | undefined;
268
269
if (isMarshalledChatSessionContext(context)) {
270
const session = context.session;
271
sessionItem = session;
272
273
// For local sessions, extract the actual session ID from editor or widget
274
if (isLocalChatSessionItem(session)) {
275
if (session.sessionType === 'editor' && session.editor instanceof ChatEditorInput) {
276
sessionId = session.editor.sessionId || session.id;
277
} else if (session.sessionType === 'widget' && session.widget) {
278
sessionId = session.widget.viewModel?.model.sessionId || session.id;
279
} else {
280
sessionId = session.id;
281
}
282
} else {
283
// For external provider sessions, use the session ID directly
284
sessionId = session.id;
285
}
286
} else {
287
sessionId = context.sessionId;
288
}
289
290
if (sessionItem && (isLocalChatSessionItem(sessionItem) || sessionId.startsWith('history-'))) {
291
// For history session remove the `history` prefix
292
const sessionIdWithoutHistory = sessionId.replace('history-', '');
293
const options: IChatEditorOptions = {
294
target: { sessionId: sessionIdWithoutHistory },
295
pinned: true,
296
auxiliary: { compact: false },
297
ignoreInView: true
298
};
299
// For local sessions, create a new chat editor in the auxiliary window
300
await editorService.openEditor({
301
resource: ChatEditorInput.getNewEditorUri(),
302
options,
303
}, AUX_WINDOW_GROUP);
304
} else {
305
// For external provider sessions, open the existing session in the auxiliary window
306
const providerType = sessionItem && (sessionItem as any).provider?.chatSessionType || 'external';
307
await editorService.openEditor({
308
resource: ChatSessionUri.forSession(providerType, sessionId),
309
options: {
310
pinned: true,
311
auxiliary: { compact: false }
312
} satisfies IChatEditorOptions
313
}, AUX_WINDOW_GROUP);
314
}
315
}
316
}
317
318
/**
319
* Action to open a chat session in a new editor group to the side
320
*/
321
export class OpenChatSessionInNewEditorGroupAction extends Action2 {
322
static readonly id = 'workbench.action.chat.openSessionInNewEditorGroup';
323
324
constructor() {
325
super({
326
id: OpenChatSessionInNewEditorGroupAction.id,
327
title: localize('chat.openSessionInNewEditorGroup.label', "Open Chat to the Side"),
328
category: CHAT_CATEGORY,
329
f1: false,
330
});
331
}
332
333
async run(accessor: ServicesAccessor, context?: IChatSessionContext | IMarshalledChatSessionContext): Promise<void> {
334
if (!context) {
335
return;
336
}
337
338
const editorService = accessor.get(IEditorService);
339
let sessionId: string;
340
let sessionItem: IChatSessionItem | undefined;
341
342
if (isMarshalledChatSessionContext(context)) {
343
const session = context.session;
344
sessionItem = session;
345
346
if (isLocalChatSessionItem(session)) {
347
if (session.sessionType === 'editor' && session.editor instanceof ChatEditorInput) {
348
sessionId = session.editor.sessionId || session.id;
349
} else if (session.sessionType === 'widget' && session.widget) {
350
sessionId = session.widget.viewModel?.model.sessionId || session.id;
351
} else {
352
sessionId = session.id;
353
}
354
} else {
355
sessionId = session.id;
356
}
357
} else {
358
sessionId = context.sessionId;
359
}
360
361
// Open editor to the side using VS Code's standard pattern
362
if (sessionItem && (isLocalChatSessionItem(sessionItem) || sessionId.startsWith('history-'))) {
363
const sessionIdWithoutHistory = sessionId.replace('history-', '');
364
const options: IChatEditorOptions = {
365
target: { sessionId: sessionIdWithoutHistory },
366
pinned: true,
367
ignoreInView: true,
368
};
369
// For local sessions, create a new chat editor
370
await editorService.openEditor({
371
resource: ChatEditorInput.getNewEditorUri(),
372
options,
373
}, SIDE_GROUP);
374
} else {
375
// For external provider sessions, open the existing session
376
const providerType = sessionItem && (sessionItem as any).provider?.chatSessionType || 'external';
377
await editorService.openEditor({
378
resource: ChatSessionUri.forSession(providerType, sessionId),
379
options: { pinned: true } satisfies IChatEditorOptions
380
}, SIDE_GROUP);
381
}
382
}
383
}
384
385
/**
386
* Action to open a chat session in the sidebar (chat widget)
387
*/
388
export class OpenChatSessionInSidebarAction extends Action2 {
389
static readonly id = 'workbench.action.chat.openSessionInSidebar';
390
391
constructor() {
392
super({
393
id: OpenChatSessionInSidebarAction.id,
394
title: localize('chat.openSessionInSidebar.label', "Open Chat in Sidebar"),
395
category: CHAT_CATEGORY,
396
f1: false,
397
});
398
}
399
400
async run(accessor: ServicesAccessor, context?: IChatSessionContext | IMarshalledChatSessionContext): Promise<void> {
401
if (!context) {
402
return;
403
}
404
405
const viewsService = accessor.get(IViewsService);
406
let sessionId: string;
407
let sessionItem: IChatSessionItem | undefined;
408
409
if (isMarshalledChatSessionContext(context)) {
410
const session = context.session;
411
sessionItem = session;
412
413
if (isLocalChatSessionItem(session)) {
414
if (session.sessionType === 'editor' && session.editor instanceof ChatEditorInput) {
415
sessionId = session.editor.sessionId || session.id;
416
} else if (session.sessionType === 'widget' && session.widget) {
417
sessionId = session.widget.viewModel?.model.sessionId || session.id;
418
} else {
419
sessionId = session.id;
420
}
421
} else {
422
sessionId = session.id;
423
}
424
} else {
425
sessionId = context.sessionId;
426
}
427
428
// Open the chat view in the sidebar
429
const chatViewPane = await viewsService.openView(ChatViewId) as ChatViewPane;
430
if (chatViewPane) {
431
// Handle different session types
432
if (sessionItem && (isLocalChatSessionItem(sessionItem) || sessionId.startsWith('history-'))) {
433
// For local sessions and history sessions, remove the 'history-' prefix if present
434
const sessionIdWithoutHistory = sessionId.replace('history-', '');
435
// Load using the session ID directly
436
await chatViewPane.loadSession(sessionIdWithoutHistory);
437
} else {
438
// For external provider sessions, create a URI and load using that
439
const providerType = sessionItem && (sessionItem as any).provider?.chatSessionType || 'external';
440
const sessionUri = ChatSessionUri.forSession(providerType, sessionId);
441
await chatViewPane.loadSession(sessionUri);
442
}
443
444
// Focus the chat input
445
chatViewPane.focusInput();
446
}
447
}
448
}
449
450
/**
451
* Action to toggle the description display mode for Chat Sessions
452
*/
453
export class ToggleChatSessionsDescriptionDisplayAction extends Action2 {
454
static readonly id = 'workbench.action.chatSessions.toggleDescriptionDisplay';
455
456
constructor() {
457
super({
458
id: ToggleChatSessionsDescriptionDisplayAction.id,
459
title: localize('chatSessions.toggleDescriptionDisplay.label', "Show Rich Descriptions"),
460
category: CHAT_CATEGORY,
461
f1: false,
462
toggled: ContextKeyExpr.equals(`config.${ChatConfiguration.ShowAgentSessionsViewDescription}`, true)
463
});
464
}
465
466
async run(accessor: ServicesAccessor): Promise<void> {
467
const configurationService = accessor.get(IConfigurationService);
468
const currentValue = configurationService.getValue(ChatConfiguration.ShowAgentSessionsViewDescription);
469
470
await configurationService.updateValue(
471
ChatConfiguration.ShowAgentSessionsViewDescription,
472
!currentValue
473
);
474
}
475
}
476
477
// Register the menu item - show for all local chat sessions (including history items)
478
MenuRegistry.appendMenuItem(MenuId.ChatSessionsMenu, {
479
command: {
480
id: RenameChatSessionAction.id,
481
title: localize('renameSession', "Rename"),
482
icon: Codicon.pencil
483
},
484
group: 'inline',
485
order: 1,
486
when: ChatContextKeys.sessionType.isEqualTo('local')
487
});
488
489
// Register delete menu item - only show for non-active sessions (history items)
490
MenuRegistry.appendMenuItem(MenuId.ChatSessionsMenu, {
491
command: {
492
id: DeleteChatSessionAction.id,
493
title: localize('deleteSession', "Delete"),
494
icon: Codicon.x
495
},
496
group: 'inline',
497
order: 2,
498
when: ContextKeyExpr.and(
499
ChatContextKeys.isHistoryItem.isEqualTo(true),
500
ChatContextKeys.isActiveSession.isEqualTo(false)
501
)
502
});
503
504
MenuRegistry.appendMenuItem(MenuId.ChatSessionsMenu, {
505
command: {
506
id: OpenChatSessionInNewEditorGroupAction.id,
507
title: localize('openToSide', "Open to the Side")
508
},
509
group: 'navigation',
510
order: 2,
511
});
512
513
MenuRegistry.appendMenuItem(MenuId.ChatSessionsMenu, {
514
command: {
515
id: OpenChatSessionInSidebarAction.id,
516
title: localize('openSessionInSidebar', "Open in Sidebar")
517
},
518
group: 'navigation',
519
order: 3,
520
});
521
522
// Register the toggle command for the ViewTitle menu
523
MenuRegistry.appendMenuItem(MenuId.ViewContainerTitle, {
524
command: {
525
id: ToggleChatSessionsDescriptionDisplayAction.id,
526
title: localize('chatSessions.toggleDescriptionDisplay.label', "Show Rich Descriptions"),
527
toggled: ContextKeyExpr.equals(`config.${ChatConfiguration.ShowAgentSessionsViewDescription}`, true)
528
},
529
group: '1_config',
530
order: 1,
531
when: ContextKeyExpr.equals('viewContainer', VIEWLET_ID),
532
});
533
534
535