Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/chat/browser/actions/chatActions.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 { isAncestorOfActiveElement } from '../../../../../base/browser/dom.js';
7
import { toAction, WorkbenchActionExecutedClassification, WorkbenchActionExecutedEvent } from '../../../../../base/common/actions.js';
8
import { coalesce } from '../../../../../base/common/arrays.js';
9
import { timeout } from '../../../../../base/common/async.js';
10
import { CancellationTokenSource } from '../../../../../base/common/cancellation.js';
11
import { Codicon } from '../../../../../base/common/codicons.js';
12
import { fromNowByDay, safeIntl } from '../../../../../base/common/date.js';
13
import { Event } from '../../../../../base/common/event.js';
14
import { MarkdownString } from '../../../../../base/common/htmlContent.js';
15
import { KeyCode, KeyMod } from '../../../../../base/common/keyCodes.js';
16
import { Disposable, DisposableStore, markAsSingleton } from '../../../../../base/common/lifecycle.js';
17
import { MarshalledId } from '../../../../../base/common/marshallingIds.js';
18
import { language } from '../../../../../base/common/platform.js';
19
import { ThemeIcon } from '../../../../../base/common/themables.js';
20
import { URI } from '../../../../../base/common/uri.js';
21
import { ICodeEditor } from '../../../../../editor/browser/editorBrowser.js';
22
import { EditorAction2 } from '../../../../../editor/browser/editorExtensions.js';
23
import { Position } from '../../../../../editor/common/core/position.js';
24
import { SuggestController } from '../../../../../editor/contrib/suggest/browser/suggestController.js';
25
import { localize, localize2 } from '../../../../../nls.js';
26
import { IActionViewItemService } from '../../../../../platform/actions/browser/actionViewItemService.js';
27
import { DropdownWithPrimaryActionViewItem } from '../../../../../platform/actions/browser/dropdownWithPrimaryActionViewItem.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';
30
import { CommandsRegistry, ICommandService } from '../../../../../platform/commands/common/commands.js';
31
import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';
32
import { ContextKeyExpr, IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js';
33
import { IsLinuxContext, IsWindowsContext } from '../../../../../platform/contextkey/common/contextkeys.js';
34
import { IDialogService } from '../../../../../platform/dialogs/common/dialogs.js';
35
import { IFileService } from '../../../../../platform/files/common/files.js';
36
import { IInstantiationService, ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js';
37
import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js';
38
import { INotificationService } from '../../../../../platform/notification/common/notification.js';
39
import { IOpenerService } from '../../../../../platform/opener/common/opener.js';
40
import product from '../../../../../platform/product/common/product.js';
41
import { IQuickInputButton, IQuickInputService, IQuickPickItem, IQuickPickSeparator } from '../../../../../platform/quickinput/common/quickInput.js';
42
import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js';
43
import { ToggleTitleBarConfigAction } from '../../../../browser/parts/titlebar/titlebarActions.js';
44
import { ActiveEditorContext, IsCompactTitleBarContext } from '../../../../common/contextkeys.js';
45
import { IWorkbenchContribution } from '../../../../common/contributions.js';
46
import { IViewDescriptorService, ViewContainerLocation } from '../../../../common/views.js';
47
import { GroupDirection, IEditorGroupsService } from '../../../../services/editor/common/editorGroupsService.js';
48
import { ACTIVE_GROUP, AUX_WINDOW_GROUP, IEditorService } from '../../../../services/editor/common/editorService.js';
49
import { IHostService } from '../../../../services/host/browser/host.js';
50
import { IWorkbenchLayoutService, Parts } from '../../../../services/layout/browser/layoutService.js';
51
import { IViewsService } from '../../../../services/views/common/viewsService.js';
52
import { IPreferencesService } from '../../../../services/preferences/common/preferences.js';
53
import { EXTENSIONS_CATEGORY, IExtensionsWorkbenchService } from '../../../extensions/common/extensions.js';
54
import { IChatAgentResult, IChatAgentService } from '../../common/chatAgents.js';
55
import { ChatContextKeys } from '../../common/chatContextKeys.js';
56
import { IChatEditingSession, ModifiedFileEntryState } from '../../common/chatEditingService.js';
57
import { ChatEntitlement, IChatEntitlementService } from '../../common/chatEntitlementService.js';
58
import { ChatMode, IChatMode, IChatModeService } from '../../common/chatModes.js';
59
import { extractAgentAndCommand } from '../../common/chatParserTypes.js';
60
import { IChatDetail, IChatService } from '../../common/chatService.js';
61
import { IChatSessionItem, IChatSessionsService } from '../../common/chatSessionsService.js';
62
import { ChatSessionUri } from '../../common/chatUri.js';
63
import { IChatRequestViewModel, IChatResponseViewModel, isRequestVM } from '../../common/chatViewModel.js';
64
import { IChatWidgetHistoryService } from '../../common/chatWidgetHistoryService.js';
65
import { ChatAgentLocation, ChatConfiguration, ChatModeKind } from '../../common/constants.js';
66
import { CopilotUsageExtensionFeatureId } from '../../common/languageModelStats.js';
67
import { ILanguageModelToolsService } from '../../common/languageModelToolsService.js';
68
import { ChatViewId, IChatWidget, IChatWidgetService, showChatView, showCopilotView } from '../chat.js';
69
import { IChatEditorOptions } from '../chatEditor.js';
70
import { ChatEditorInput, shouldShowClearEditingSessionConfirmation, showClearEditingSessionConfirmation } from '../chatEditorInput.js';
71
import { VIEWLET_ID } from '../chatSessions.js';
72
import { ChatViewPane } from '../chatViewPane.js';
73
import { convertBufferToScreenshotVariable } from '../contrib/screenshot.js';
74
import { clearChatEditor } from './chatClear.js';
75
import { ILanguageModelChatSelector, ILanguageModelsService } from '../../common/languageModels.js';
76
import { IChatResponseModel } from '../../common/chatModel.js';
77
import { EditorContextKeys } from '../../../../../editor/common/editorContextKeys.js';
78
import { mainWindow } from '../../../../../base/browser/window.js';
79
80
export const CHAT_CATEGORY = localize2('chat.category', 'Chat');
81
82
export const ACTION_ID_NEW_CHAT = `workbench.action.chat.newChat`;
83
export const ACTION_ID_NEW_EDIT_SESSION = `workbench.action.chat.newEditSession`;
84
export const CHAT_OPEN_ACTION_ID = 'workbench.action.chat.open';
85
export const CHAT_SETUP_ACTION_ID = 'workbench.action.chat.triggerSetup';
86
const TOGGLE_CHAT_ACTION_ID = 'workbench.action.chat.toggle';
87
const CHAT_CLEAR_HISTORY_ACTION_ID = 'workbench.action.chat.clearHistory';
88
89
export interface IChatViewOpenOptions {
90
/**
91
* The query for chat.
92
*/
93
query: string;
94
/**
95
* Whether the query is partial and will await more input from the user.
96
*/
97
isPartialQuery?: boolean;
98
/**
99
* A list of tools IDs with `canBeReferencedInPrompt` that will be resolved and attached if they exist.
100
*/
101
toolIds?: string[];
102
/**
103
* Any previous chat requests and responses that should be shown in the chat view.
104
*/
105
previousRequests?: IChatViewOpenRequestEntry[];
106
/**
107
* Whether a screenshot of the focused window should be taken and attached
108
*/
109
attachScreenshot?: boolean;
110
/**
111
* A list of file URIs to attach to the chat as context.
112
*/
113
attachFiles?: URI[];
114
/**
115
* The mode ID or name to open the chat in.
116
*/
117
mode?: ChatModeKind | string;
118
119
/**
120
* The language model selector to use for the chat.
121
* An Error will be thrown if there's no match. If there are multiple
122
* matches, the first match will be used.
123
*
124
* Examples:
125
*
126
* ```
127
* {
128
* id: 'claude-sonnet-4',
129
* vendor: 'copilot'
130
* }
131
* ```
132
*
133
* Use `claude-sonnet-4` from any vendor:
134
*
135
* ```
136
* {
137
* id: 'claude-sonnet-4',
138
* }
139
* ```
140
*/
141
modelSelector?: ILanguageModelChatSelector;
142
143
/**
144
* Wait to resolve the command until the chat response reaches a terminal state (complete, error, or pending user confirmation, etc.).
145
*/
146
blockOnResponse?: boolean;
147
}
148
149
export interface IChatViewOpenRequestEntry {
150
request: string;
151
response: string;
152
}
153
154
export const CHAT_CONFIG_MENU_ID = new MenuId('workbench.chat.menu.config');
155
156
const OPEN_CHAT_QUOTA_EXCEEDED_DIALOG = 'workbench.action.chat.openQuotaExceededDialog';
157
158
abstract class OpenChatGlobalAction extends Action2 {
159
constructor(overrides: Pick<ICommandPaletteOptions, 'keybinding' | 'title' | 'id' | 'menu'>, private readonly mode?: IChatMode) {
160
super({
161
...overrides,
162
icon: Codicon.chatSparkle,
163
f1: true,
164
category: CHAT_CATEGORY,
165
precondition: ContextKeyExpr.and(
166
ChatContextKeys.Setup.hidden.negate(),
167
ChatContextKeys.Setup.disabled.negate()
168
)
169
});
170
}
171
172
override async run(accessor: ServicesAccessor, opts?: string | IChatViewOpenOptions): Promise<IChatAgentResult & { type?: 'confirmation' } | undefined> {
173
opts = typeof opts === 'string' ? { query: opts } : opts;
174
175
const chatService = accessor.get(IChatService);
176
const widgetService = accessor.get(IChatWidgetService);
177
const toolsService = accessor.get(ILanguageModelToolsService);
178
const viewsService = accessor.get(IViewsService);
179
const hostService = accessor.get(IHostService);
180
const chatAgentService = accessor.get(IChatAgentService);
181
const instaService = accessor.get(IInstantiationService);
182
const commandService = accessor.get(ICommandService);
183
const chatModeService = accessor.get(IChatModeService);
184
const fileService = accessor.get(IFileService);
185
const languageModelService = accessor.get(ILanguageModelsService);
186
187
let chatWidget = widgetService.lastFocusedWidget;
188
// When this was invoked to switch to a mode via keybinding, and some chat widget is focused, use that one.
189
// Otherwise, open the view.
190
if (!this.mode || !chatWidget || !isAncestorOfActiveElement(chatWidget.domNode)) {
191
chatWidget = await showChatView(viewsService);
192
}
193
194
if (!chatWidget) {
195
return;
196
}
197
198
const switchToMode = (opts?.mode ? chatModeService.findModeByName(opts?.mode) : undefined) ?? this.mode;
199
if (switchToMode) {
200
await this.handleSwitchToMode(switchToMode, chatWidget, instaService, commandService);
201
}
202
203
if (opts?.modelSelector) {
204
const ids = await languageModelService.selectLanguageModels(opts.modelSelector, false);
205
const id = ids.sort().at(0);
206
if (!id) {
207
throw new Error(`No language models found matching selector: ${JSON.stringify(opts.modelSelector)}.`);
208
}
209
210
const model = languageModelService.lookupLanguageModel(id);
211
if (!model) {
212
throw new Error(`Language model not loaded: ${id}.`);
213
}
214
215
chatWidget.input.setCurrentLanguageModel({ metadata: model, identifier: id });
216
}
217
218
if (opts?.previousRequests?.length && chatWidget.viewModel) {
219
for (const { request, response } of opts.previousRequests) {
220
chatService.addCompleteRequest(chatWidget.viewModel.sessionId, request, undefined, 0, { message: response });
221
}
222
}
223
if (opts?.attachScreenshot) {
224
const screenshot = await hostService.getScreenshot();
225
if (screenshot) {
226
chatWidget.attachmentModel.addContext(convertBufferToScreenshotVariable(screenshot));
227
}
228
}
229
if (opts?.attachFiles) {
230
for (const file of opts.attachFiles) {
231
if (await fileService.exists(file)) {
232
chatWidget.attachmentModel.addFile(file);
233
}
234
}
235
}
236
237
let resp: Promise<IChatResponseModel | undefined> | undefined;
238
239
if (opts?.query) {
240
chatWidget.setInput(opts.query);
241
242
if (!opts.isPartialQuery) {
243
await chatWidget.waitForReady();
244
await waitForDefaultAgent(chatAgentService, chatWidget.input.currentModeKind);
245
resp = chatWidget.acceptInput();
246
}
247
}
248
249
if (opts?.toolIds && opts.toolIds.length > 0) {
250
for (const toolId of opts.toolIds) {
251
const tool = toolsService.getTool(toolId);
252
if (tool) {
253
chatWidget.attachmentModel.addContext({
254
id: tool.id,
255
name: tool.displayName,
256
fullName: tool.displayName,
257
value: undefined,
258
icon: ThemeIcon.isThemeIcon(tool.icon) ? tool.icon : undefined,
259
kind: 'tool'
260
});
261
}
262
}
263
}
264
265
chatWidget.focusInput();
266
267
if (opts?.blockOnResponse) {
268
const response = await resp;
269
if (response) {
270
await new Promise<void>(resolve => {
271
const d = response.onDidChange(async () => {
272
if (response.isComplete || response.isPendingConfirmation.get()) {
273
d.dispose();
274
resolve();
275
}
276
});
277
});
278
279
return { ...response.result, type: response.isPendingConfirmation.get() ? 'confirmation' : undefined };
280
}
281
}
282
283
return undefined;
284
}
285
286
private async handleSwitchToMode(switchToMode: IChatMode, chatWidget: IChatWidget, instaService: IInstantiationService, commandService: ICommandService): Promise<void> {
287
const currentMode = chatWidget.input.currentModeKind;
288
289
if (switchToMode) {
290
const editingSession = chatWidget.viewModel?.model.editingSession;
291
const requestCount = chatWidget.viewModel?.model.getRequests().length ?? 0;
292
const chatModeCheck = await instaService.invokeFunction(handleModeSwitch, currentMode, switchToMode.kind, requestCount, editingSession);
293
if (!chatModeCheck) {
294
return;
295
}
296
chatWidget.input.setChatMode(switchToMode.id);
297
298
if (chatModeCheck.needToClearSession) {
299
await commandService.executeCommand(ACTION_ID_NEW_CHAT);
300
}
301
}
302
}
303
}
304
305
async function waitForDefaultAgent(chatAgentService: IChatAgentService, mode: ChatModeKind): Promise<void> {
306
const defaultAgent = chatAgentService.getDefaultAgent(ChatAgentLocation.Panel, mode);
307
if (defaultAgent) {
308
return;
309
}
310
311
await Promise.race([
312
Event.toPromise(Event.filter(chatAgentService.onDidChangeAgents, () => {
313
const defaultAgent = chatAgentService.getDefaultAgent(ChatAgentLocation.Panel, mode);
314
return Boolean(defaultAgent);
315
})),
316
timeout(60_000).then(() => { throw new Error('Timed out waiting for default agent'); })
317
]);
318
}
319
320
class PrimaryOpenChatGlobalAction extends OpenChatGlobalAction {
321
constructor() {
322
super({
323
id: CHAT_OPEN_ACTION_ID,
324
title: localize2('openChat', "Open Chat"),
325
keybinding: {
326
weight: KeybindingWeight.WorkbenchContrib,
327
primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KeyI,
328
mac: {
329
primary: KeyMod.CtrlCmd | KeyMod.WinCtrl | KeyCode.KeyI
330
}
331
},
332
menu: [{
333
id: MenuId.ChatTitleBarMenu,
334
group: 'a_open',
335
order: 1
336
}]
337
});
338
}
339
}
340
341
export function getOpenChatActionIdForMode(mode: IChatMode): string {
342
return `workbench.action.chat.open${mode.name}`;
343
}
344
345
abstract class ModeOpenChatGlobalAction extends OpenChatGlobalAction {
346
constructor(mode: IChatMode, keybinding?: ICommandPaletteOptions['keybinding']) {
347
super({
348
id: getOpenChatActionIdForMode(mode),
349
title: localize2('openChatMode', "Open Chat ({0})", mode.label),
350
keybinding
351
}, mode);
352
}
353
}
354
355
export function registerChatActions() {
356
registerAction2(PrimaryOpenChatGlobalAction);
357
registerAction2(class extends ModeOpenChatGlobalAction {
358
constructor() { super(ChatMode.Ask); }
359
});
360
registerAction2(class extends ModeOpenChatGlobalAction {
361
constructor() {
362
super(ChatMode.Agent, {
363
when: ContextKeyExpr.has(`config.${ChatConfiguration.AgentEnabled}`),
364
weight: KeybindingWeight.WorkbenchContrib,
365
primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyI,
366
linux: {
367
primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyMod.Shift | KeyCode.KeyI
368
}
369
},);
370
}
371
});
372
registerAction2(class extends ModeOpenChatGlobalAction {
373
constructor() { super(ChatMode.Edit); }
374
});
375
376
registerAction2(class ToggleChatAction extends Action2 {
377
constructor() {
378
super({
379
id: TOGGLE_CHAT_ACTION_ID,
380
title: localize2('toggleChat', "Toggle Chat"),
381
category: CHAT_CATEGORY
382
});
383
}
384
385
async run(accessor: ServicesAccessor) {
386
const layoutService = accessor.get(IWorkbenchLayoutService);
387
const viewsService = accessor.get(IViewsService);
388
const viewDescriptorService = accessor.get(IViewDescriptorService);
389
390
const chatLocation = viewDescriptorService.getViewLocationById(ChatViewId);
391
392
if (viewsService.isViewVisible(ChatViewId)) {
393
this.updatePartVisibility(layoutService, chatLocation, false);
394
} else {
395
this.updatePartVisibility(layoutService, chatLocation, true);
396
(await showCopilotView(viewsService, layoutService))?.focusInput();
397
}
398
}
399
400
private updatePartVisibility(layoutService: IWorkbenchLayoutService, location: ViewContainerLocation | null, visible: boolean): void {
401
let part: Parts.PANEL_PART | Parts.SIDEBAR_PART | Parts.AUXILIARYBAR_PART | undefined;
402
switch (location) {
403
case ViewContainerLocation.Panel:
404
part = Parts.PANEL_PART;
405
break;
406
case ViewContainerLocation.Sidebar:
407
part = Parts.SIDEBAR_PART;
408
break;
409
case ViewContainerLocation.AuxiliaryBar:
410
part = Parts.AUXILIARYBAR_PART;
411
break;
412
}
413
414
if (part) {
415
layoutService.setPartHidden(!visible, part);
416
}
417
}
418
});
419
420
registerAction2(class ChatHistoryAction extends Action2 {
421
constructor() {
422
super({
423
id: `workbench.action.chat.history`,
424
title: localize2('chat.history.label', "Show Chats..."),
425
menu: [
426
{
427
id: MenuId.ViewTitle,
428
when: ContextKeyExpr.and(
429
ContextKeyExpr.equals('view', ChatViewId),
430
ChatContextKeys.inEmptyStateWithHistoryEnabled.negate()
431
),
432
group: 'navigation',
433
order: 2
434
},
435
{
436
id: MenuId.EditorTitle,
437
when: ActiveEditorContext.isEqualTo(ChatEditorInput.EditorID),
438
},
439
],
440
category: CHAT_CATEGORY,
441
icon: Codicon.history,
442
f1: true,
443
precondition: ChatContextKeys.enabled
444
});
445
}
446
447
private showLegacyPicker = async (
448
chatService: IChatService,
449
quickInputService: IQuickInputService,
450
commandService: ICommandService,
451
editorService: IEditorService,
452
view: ChatViewPane
453
) => {
454
const clearChatHistoryButton: IQuickInputButton = {
455
iconClass: ThemeIcon.asClassName(Codicon.clearAll),
456
tooltip: localize('interactiveSession.history.clear', "Clear All Workspace Chats"),
457
};
458
459
const openInEditorButton: IQuickInputButton = {
460
iconClass: ThemeIcon.asClassName(Codicon.file),
461
tooltip: localize('interactiveSession.history.editor', "Open in Editor"),
462
};
463
const deleteButton: IQuickInputButton = {
464
iconClass: ThemeIcon.asClassName(Codicon.x),
465
tooltip: localize('interactiveSession.history.delete', "Delete"),
466
};
467
const renameButton: IQuickInputButton = {
468
iconClass: ThemeIcon.asClassName(Codicon.pencil),
469
tooltip: localize('chat.history.rename', "Rename"),
470
};
471
472
interface IChatPickerItem extends IQuickPickItem {
473
chat: IChatDetail;
474
}
475
476
const getPicks = async () => {
477
const items = await chatService.getHistory();
478
items.sort((a, b) => (b.lastMessageDate ?? 0) - (a.lastMessageDate ?? 0));
479
480
let lastDate: string | undefined = undefined;
481
const picks = items.flatMap((i): [IQuickPickSeparator | undefined, IChatPickerItem] => {
482
const timeAgoStr = fromNowByDay(i.lastMessageDate, true, true);
483
const separator: IQuickPickSeparator | undefined = timeAgoStr !== lastDate ? {
484
type: 'separator', label: timeAgoStr,
485
} : undefined;
486
lastDate = timeAgoStr;
487
return [
488
separator,
489
{
490
label: i.title,
491
description: i.isActive ? `(${localize('currentChatLabel', 'current')})` : '',
492
chat: i,
493
buttons: i.isActive ? [renameButton] : [
494
renameButton,
495
openInEditorButton,
496
deleteButton,
497
]
498
}
499
];
500
});
501
502
return coalesce(picks);
503
};
504
505
const store = new (DisposableStore as { new(): DisposableStore })();
506
const picker = store.add(quickInputService.createQuickPick<IChatPickerItem>({ useSeparators: true }));
507
picker.title = localize('interactiveSession.history.title', "Workspace Chat History");
508
picker.placeholder = localize('interactiveSession.history.pick', "Switch to chat");
509
picker.buttons = [clearChatHistoryButton];
510
const picks = await getPicks();
511
picker.items = picks;
512
store.add(picker.onDidTriggerButton(async button => {
513
if (button === clearChatHistoryButton) {
514
await commandService.executeCommand(CHAT_CLEAR_HISTORY_ACTION_ID);
515
}
516
}));
517
store.add(picker.onDidTriggerItemButton(async context => {
518
if (context.button === openInEditorButton) {
519
const options: IChatEditorOptions = { target: { sessionId: context.item.chat.sessionId }, pinned: true };
520
editorService.openEditor({ resource: ChatEditorInput.getNewEditorUri(), options }, ACTIVE_GROUP);
521
picker.hide();
522
} else if (context.button === deleteButton) {
523
chatService.removeHistoryEntry(context.item.chat.sessionId);
524
picker.items = await getPicks();
525
} else if (context.button === renameButton) {
526
const title = await quickInputService.input({ title: localize('newChatTitle', "New chat title"), value: context.item.chat.title });
527
if (title) {
528
chatService.setChatSessionTitle(context.item.chat.sessionId, title);
529
}
530
531
// The quick input hides the picker, it gets disposed, so we kick it off from scratch
532
await this.showLegacyPicker(chatService, quickInputService, commandService, editorService, view);
533
}
534
}));
535
store.add(picker.onDidAccept(async () => {
536
try {
537
const item = picker.selectedItems[0];
538
const sessionId = item.chat.sessionId;
539
await view.loadSession(sessionId);
540
} finally {
541
picker.hide();
542
}
543
}));
544
store.add(picker.onDidHide(() => store.dispose()));
545
546
picker.show();
547
};
548
549
private showIntegratedPicker = async (
550
chatService: IChatService,
551
quickInputService: IQuickInputService,
552
commandService: ICommandService,
553
editorService: IEditorService,
554
chatWidgetService: IChatWidgetService,
555
view: ChatViewPane,
556
chatSessionsService: IChatSessionsService,
557
contextKeyService: IContextKeyService,
558
menuService: IMenuService,
559
showAllChats: boolean = false,
560
showAllAgents: boolean = false
561
) => {
562
const clearChatHistoryButton: IQuickInputButton = {
563
iconClass: ThemeIcon.asClassName(Codicon.clearAll),
564
tooltip: localize('interactiveSession.history.clear', "Clear All Workspace Chats"),
565
};
566
567
const openInEditorButton: IQuickInputButton = {
568
iconClass: ThemeIcon.asClassName(Codicon.file),
569
tooltip: localize('interactiveSession.history.editor', "Open in Editor"),
570
};
571
const deleteButton: IQuickInputButton = {
572
iconClass: ThemeIcon.asClassName(Codicon.x),
573
tooltip: localize('interactiveSession.history.delete', "Delete"),
574
};
575
const renameButton: IQuickInputButton = {
576
iconClass: ThemeIcon.asClassName(Codicon.pencil),
577
tooltip: localize('chat.history.rename', "Rename"),
578
};
579
580
interface IChatPickerItem extends IQuickPickItem {
581
chat: IChatDetail;
582
}
583
584
interface ICodingAgentPickerItem extends IChatPickerItem {
585
id?: string;
586
session?: { providerType: string; session: IChatSessionItem };
587
uri?: URI;
588
}
589
590
const getPicks = async (showAllChats: boolean = false, showAllAgents: boolean = false) => {
591
// Fast picks: Get cached/immediate items first
592
const cachedItems = await chatService.getHistory();
593
cachedItems.sort((a, b) => (b.lastMessageDate ?? 0) - (a.lastMessageDate ?? 0));
594
595
const allFastPickItems: IChatPickerItem[] = cachedItems.map((i) => {
596
const timeAgoStr = fromNowByDay(i.lastMessageDate, true, true);
597
const currentLabel = i.isActive ? localize('currentChatLabel', 'current') : '';
598
const description = currentLabel ? `${timeAgoStr} • ${currentLabel}` : timeAgoStr;
599
600
return {
601
label: i.title,
602
description: description,
603
chat: i,
604
buttons: i.isActive ? [renameButton] : [
605
renameButton,
606
openInEditorButton,
607
deleteButton,
608
]
609
};
610
});
611
612
const fastPickItems = showAllChats ? allFastPickItems : allFastPickItems.slice(0, 5);
613
const fastPicks: (IQuickPickSeparator | IChatPickerItem)[] = [];
614
if (fastPickItems.length > 0) {
615
fastPicks.push({
616
type: 'separator',
617
label: localize('chat.history.recent', 'Recent Chats'),
618
});
619
fastPicks.push(...fastPickItems);
620
621
// Add "Show more..." if there are more items and we're not showing all chats
622
if (!showAllChats && allFastPickItems.length > 5) {
623
fastPicks.push({
624
label: localize('chat.history.showMore', 'Show more...'),
625
description: '',
626
chat: {
627
sessionId: 'show-more-chats',
628
title: 'Show more...',
629
isActive: false,
630
lastMessageDate: 0,
631
},
632
buttons: []
633
});
634
}
635
}
636
637
// Slow picks: Get coding agents asynchronously via AsyncIterable
638
const slowPicks = (async function* (): AsyncGenerator<(IQuickPickSeparator | ICodingAgentPickerItem)[]> {
639
try {
640
const agentPicks: ICodingAgentPickerItem[] = [];
641
642
// Use the new Promise-based API to get chat sessions
643
const cancellationToken = new CancellationTokenSource();
644
try {
645
const providers = chatSessionsService.getAllChatSessionContributions();
646
const providerNSessions: { providerType: string; session: IChatSessionItem }[] = [];
647
648
for (const provider of providers) {
649
const sessions = await chatSessionsService.provideChatSessionItems(provider.type, cancellationToken.token);
650
providerNSessions.push(...sessions.map(session => ({ providerType: provider.type, session })));
651
}
652
653
for (const session of providerNSessions) {
654
const sessionContent = session.session;
655
656
const ckey = contextKeyService.createKey('chatSessionType', session.providerType);
657
const actions = menuService.getMenuActions(MenuId.ChatSessionsMenu, contextKeyService);
658
const { primary } = getContextMenuActions(actions, 'inline');
659
ckey.reset();
660
661
// Use primary actions if available, otherwise fall back to secondary actions
662
const buttons = primary.map(action => ({
663
id: action.id,
664
tooltip: action.tooltip,
665
iconClass: action.class || ThemeIcon.asClassName(Codicon.symbolClass),
666
}));
667
// Create agent pick from the session content
668
const agentPick: ICodingAgentPickerItem = {
669
label: sessionContent.label,
670
description: '',
671
session: { providerType: session.providerType, session: sessionContent },
672
chat: {
673
sessionId: sessionContent.id,
674
title: sessionContent.label,
675
isActive: false,
676
lastMessageDate: 0,
677
},
678
buttons,
679
id: sessionContent.id
680
};
681
682
// Check if this agent already exists (update existing or add new)
683
const existingIndex = agentPicks.findIndex(pick => pick.chat.sessionId === sessionContent.id);
684
if (existingIndex >= 0) {
685
agentPicks[existingIndex] = agentPick;
686
} else {
687
// Respect show limits
688
const maxToShow = showAllAgents ? Number.MAX_SAFE_INTEGER : 5;
689
if (agentPicks.length < maxToShow) {
690
agentPicks.push(agentPick);
691
}
692
}
693
}
694
695
// Create current picks with separator if we have agents
696
const currentPicks: (IQuickPickSeparator | ICodingAgentPickerItem)[] = [];
697
698
if (agentPicks.length > 0) {
699
// Always add separator for coding agents section
700
currentPicks.push({
701
type: 'separator',
702
label: 'Chat Sessions',
703
});
704
currentPicks.push(...agentPicks);
705
706
// Add "Show more..." if needed and not showing all agents
707
if (!showAllAgents && providerNSessions.length > 5) {
708
currentPicks.push({
709
label: localize('chat.history.showMoreAgents', 'Show more...'),
710
description: '',
711
chat: {
712
sessionId: 'show-more-agents',
713
title: 'Show more...',
714
isActive: false,
715
lastMessageDate: 0,
716
},
717
buttons: [],
718
uri: undefined,
719
});
720
}
721
}
722
723
// Yield the current state
724
yield currentPicks;
725
726
} finally {
727
cancellationToken.dispose();
728
}
729
730
} catch (error) {
731
// Gracefully handle errors in async contributions
732
return;
733
}
734
})();
735
736
// Return fast picks immediately, add slow picks as async generator
737
return {
738
fast: coalesce(fastPicks),
739
slow: slowPicks
740
};
741
};
742
743
const store = new (DisposableStore as { new(): DisposableStore })();
744
const picker = store.add(quickInputService.createQuickPick<IChatPickerItem | ICodingAgentPickerItem>({ useSeparators: true }));
745
picker.title = (showAllChats || showAllAgents) ?
746
localize('interactiveSession.history.titleAll', "All Workspace Chat History") :
747
localize('interactiveSession.history.title', "Workspace Chat History");
748
picker.placeholder = localize('interactiveSession.history.pick', "Switch to chat");
749
picker.buttons = [clearChatHistoryButton];
750
751
// Get fast and slow picks
752
const { fast, slow } = await getPicks(showAllChats, showAllAgents);
753
754
// Set fast picks immediately
755
picker.items = fast;
756
picker.busy = true;
757
758
// Consume slow picks progressively
759
(async () => {
760
try {
761
for await (const slowPicks of slow) {
762
if (!store.isDisposed) {
763
picker.items = coalesce([...fast, ...slowPicks]);
764
}
765
}
766
} catch (error) {
767
// Handle errors gracefully
768
} finally {
769
if (!store.isDisposed) {
770
picker.busy = false;
771
}
772
}
773
})();
774
store.add(picker.onDidTriggerButton(async button => {
775
if (button === clearChatHistoryButton) {
776
await commandService.executeCommand(CHAT_CLEAR_HISTORY_ACTION_ID);
777
}
778
}));
779
store.add(picker.onDidTriggerItemButton(async context => {
780
if (context.button === openInEditorButton) {
781
const options: IChatEditorOptions = { target: { sessionId: context.item.chat.sessionId }, pinned: true };
782
editorService.openEditor({ resource: ChatEditorInput.getNewEditorUri(), options }, ACTIVE_GROUP);
783
picker.hide();
784
} else if (context.button === deleteButton) {
785
chatService.removeHistoryEntry(context.item.chat.sessionId);
786
// Refresh picker items after deletion
787
const { fast, slow } = await getPicks(showAllChats, showAllAgents);
788
picker.items = fast;
789
picker.busy = true;
790
791
// Consume slow picks progressively after deletion
792
(async () => {
793
try {
794
for await (const slowPicks of slow) {
795
if (!store.isDisposed) {
796
picker.items = coalesce([...fast, ...slowPicks]);
797
}
798
}
799
} catch (error) {
800
// Handle errors gracefully
801
} finally {
802
if (!store.isDisposed) {
803
picker.busy = false;
804
}
805
}
806
})();
807
} else if (context.button === renameButton) {
808
const title = await quickInputService.input({ title: localize('newChatTitle', "New chat title"), value: context.item.chat.title });
809
if (title) {
810
chatService.setChatSessionTitle(context.item.chat.sessionId, title);
811
}
812
813
// The quick input hides the picker, it gets disposed, so we kick it off from scratch
814
await this.showIntegratedPicker(
815
chatService,
816
quickInputService,
817
commandService,
818
editorService,
819
chatWidgetService,
820
view,
821
chatSessionsService,
822
contextKeyService,
823
menuService,
824
showAllChats,
825
showAllAgents
826
);
827
} else {
828
const buttonItem = context.button as ICodingAgentPickerItem;
829
if (buttonItem.id) {
830
const contextItem = context.item as ICodingAgentPickerItem;
831
commandService.executeCommand(buttonItem.id, {
832
uri: contextItem.uri,
833
session: contextItem.session?.session,
834
$mid: MarshalledId.ChatSessionContext
835
});
836
837
// dismiss quick picker
838
picker.hide();
839
}
840
}
841
}));
842
store.add(picker.onDidAccept(async () => {
843
try {
844
const item = picker.selectedItems[0];
845
const sessionId = item.chat.sessionId;
846
847
// Handle "Show more..." options
848
if (sessionId === 'show-more-chats') {
849
picker.hide();
850
// Create a new picker with all chat items expanded
851
await this.showIntegratedPicker(
852
chatService,
853
quickInputService,
854
commandService,
855
editorService,
856
chatWidgetService,
857
view,
858
chatSessionsService,
859
contextKeyService,
860
menuService,
861
true,
862
showAllAgents
863
);
864
return;
865
} else if (sessionId === 'show-more-agents') {
866
picker.hide();
867
// Create a new picker with all agent items expanded
868
await this.showIntegratedPicker(
869
chatService,
870
quickInputService,
871
commandService,
872
editorService,
873
chatWidgetService,
874
view,
875
chatSessionsService,
876
contextKeyService,
877
menuService,
878
showAllChats,
879
true
880
);
881
return;
882
} else if ((item as ICodingAgentPickerItem).id !== undefined) {
883
// TODO: This is a temporary change that will be replaced by opening a new chat instance
884
const codingAgentItem = item as ICodingAgentPickerItem;
885
if (codingAgentItem.session) {
886
await this.showChatSessionInEditor(codingAgentItem.session.providerType, codingAgentItem.session.session, editorService);
887
}
888
}
889
890
await view.loadSession(sessionId);
891
} finally {
892
picker.hide();
893
}
894
}));
895
store.add(picker.onDidHide(() => store.dispose()));
896
897
picker.show();
898
};
899
900
async run(accessor: ServicesAccessor) {
901
const chatService = accessor.get(IChatService);
902
const quickInputService = accessor.get(IQuickInputService);
903
const viewsService = accessor.get(IViewsService);
904
const editorService = accessor.get(IEditorService);
905
const chatWidgetService = accessor.get(IChatWidgetService);
906
const dialogService = accessor.get(IDialogService);
907
const commandService = accessor.get(ICommandService);
908
const chatSessionsService = accessor.get(IChatSessionsService);
909
const contextKeyService = accessor.get(IContextKeyService);
910
const menuService = accessor.get(IMenuService);
911
912
const view = await viewsService.openView<ChatViewPane>(ChatViewId);
913
if (!view) {
914
return;
915
}
916
917
const chatSessionId = view.widget.viewModel?.model.sessionId;
918
if (!chatSessionId) {
919
return;
920
}
921
922
const editingSession = view.widget.viewModel?.model.editingSession;
923
if (editingSession) {
924
const phrase = localize('switchChat.confirmPhrase', "Switching chats will end your current edit session.");
925
if (!await handleCurrentEditingSession(editingSession, phrase, dialogService)) {
926
return;
927
}
928
}
929
930
// Check if there are any non-local chat session item providers registered
931
const allProviders = chatSessionsService.getAllChatSessionItemProviders();
932
const hasNonLocalProviders = allProviders.some(provider => provider.chatSessionType !== 'local');
933
934
if (hasNonLocalProviders) {
935
await this.showIntegratedPicker(
936
chatService,
937
quickInputService,
938
commandService,
939
editorService,
940
chatWidgetService,
941
view,
942
chatSessionsService,
943
contextKeyService,
944
menuService
945
);
946
} else {
947
await this.showLegacyPicker(chatService, quickInputService, commandService, editorService, view);
948
}
949
}
950
951
private async showChatSessionInEditor(providerType: string, session: IChatSessionItem, editorService: IEditorService) {
952
// Open the chat editor
953
await editorService.openEditor({
954
resource: ChatSessionUri.forSession(providerType, session.id),
955
options: {} satisfies IChatEditorOptions
956
});
957
}
958
});
959
960
registerAction2(class NewChatEditorAction extends Action2 {
961
constructor() {
962
super({
963
id: `workbench.action.openChat`,
964
title: localize2('interactiveSession.open', "New Chat Editor"),
965
f1: true,
966
category: CHAT_CATEGORY,
967
precondition: ChatContextKeys.enabled,
968
keybinding: {
969
weight: KeybindingWeight.WorkbenchContrib + 1,
970
primary: KeyMod.CtrlCmd | KeyCode.KeyN,
971
when: ContextKeyExpr.and(ChatContextKeys.inChatSession, ChatContextKeys.inChatEditor)
972
},
973
menu: {
974
id: MenuId.ChatTitleBarMenu,
975
group: 'b_new',
976
order: 0
977
}
978
});
979
}
980
981
async run(accessor: ServicesAccessor) {
982
const editorService = accessor.get(IEditorService);
983
await editorService.openEditor({ resource: ChatEditorInput.getNewEditorUri(), options: { pinned: true } satisfies IChatEditorOptions });
984
}
985
});
986
987
registerAction2(class NewChatWindowAction extends Action2 {
988
constructor() {
989
super({
990
id: `workbench.action.newChatWindow`,
991
title: localize2('interactiveSession.newChatWindow', "New Chat Window"),
992
f1: true,
993
category: CHAT_CATEGORY,
994
precondition: ChatContextKeys.enabled,
995
menu: {
996
id: MenuId.ChatTitleBarMenu,
997
group: 'b_new',
998
order: 1
999
}
1000
});
1001
}
1002
1003
async run(accessor: ServicesAccessor) {
1004
const editorService = accessor.get(IEditorService);
1005
await editorService.openEditor({ resource: ChatEditorInput.getNewEditorUri(), options: { pinned: true, auxiliary: { compact: true, bounds: { width: 640, height: 640 } } } satisfies IChatEditorOptions }, AUX_WINDOW_GROUP);
1006
}
1007
});
1008
1009
registerAction2(class OpenChatEditorInNewWindowAction extends Action2 {
1010
constructor() {
1011
super({
1012
id: `workbench.action.chat.newChatInNewWindow`,
1013
title: localize2('chatSessions.openNewChatInNewWindow', 'Open New Chat in New Window'),
1014
f1: false,
1015
category: CHAT_CATEGORY,
1016
precondition: ChatContextKeys.enabled,
1017
menu: {
1018
id: MenuId.ViewTitle,
1019
group: 'submenu',
1020
order: 1,
1021
when: ContextKeyExpr.equals('view', `${VIEWLET_ID}.local`),
1022
}
1023
});
1024
}
1025
1026
async run(accessor: ServicesAccessor) {
1027
const editorService = accessor.get(IEditorService);
1028
await editorService.openEditor({
1029
resource: ChatEditorInput.getNewEditorUri(),
1030
options: {
1031
pinned: true,
1032
auxiliary: { compact: false }
1033
} satisfies IChatEditorOptions
1034
}, AUX_WINDOW_GROUP);
1035
}
1036
});
1037
1038
registerAction2(class NewChatInSideBarAction extends Action2 {
1039
constructor() {
1040
super({
1041
id: `workbench.action.chat.newChatInSideBar`,
1042
title: localize2('chatSessions.newChatInSideBar', 'Open New Chat in Side Bar'),
1043
f1: false,
1044
category: CHAT_CATEGORY,
1045
precondition: ChatContextKeys.enabled,
1046
menu: {
1047
id: MenuId.ViewTitle,
1048
group: 'submenu',
1049
order: 1,
1050
when: ContextKeyExpr.equals('view', `${VIEWLET_ID}.local`),
1051
}
1052
});
1053
}
1054
1055
async run(accessor: ServicesAccessor) {
1056
const viewsService = accessor.get(IViewsService);
1057
1058
// Open the chat view in the sidebar and get the widget
1059
const chatWidget = await showChatView(viewsService);
1060
1061
if (chatWidget) {
1062
// Clear the current chat to start a new one
1063
chatWidget.clear();
1064
await chatWidget.waitForReady();
1065
chatWidget.attachmentModel.clear(true);
1066
chatWidget.input.relatedFiles?.clear();
1067
1068
// Focus the input area
1069
chatWidget.focusInput();
1070
}
1071
}
1072
});
1073
1074
registerAction2(class OpenChatInNewEditorGroupAction extends Action2 {
1075
constructor() {
1076
super({
1077
id: 'workbench.action.chat.openNewChatToTheSide',
1078
title: localize2('chat.openNewChatToTheSide.label', "Open New Chat Editor to the Side"),
1079
category: CHAT_CATEGORY,
1080
precondition: ChatContextKeys.enabled,
1081
f1: false,
1082
menu: {
1083
id: MenuId.ViewTitle,
1084
group: 'submenu',
1085
order: 1,
1086
when: ContextKeyExpr.equals('view', `${VIEWLET_ID}.local`),
1087
}
1088
});
1089
}
1090
1091
async run(accessor: ServicesAccessor, ...args: any[]) {
1092
const editorService = accessor.get(IEditorService);
1093
const editorGroupService = accessor.get(IEditorGroupsService);
1094
1095
// Create a new editor group to the right
1096
const newGroup = editorGroupService.addGroup(editorGroupService.activeGroup, GroupDirection.RIGHT);
1097
editorGroupService.activateGroup(newGroup);
1098
1099
// Open a new chat editor in the new group
1100
await editorService.openEditor(
1101
{ resource: ChatEditorInput.getNewEditorUri(), options: { pinned: true } },
1102
newGroup.id
1103
);
1104
}
1105
});
1106
1107
registerAction2(class ChatAddAction extends Action2 {
1108
constructor() {
1109
super({
1110
id: 'workbench.action.chat.addParticipant',
1111
title: localize2('chatWith', "Chat with Extension"),
1112
icon: Codicon.mention,
1113
f1: false,
1114
category: CHAT_CATEGORY,
1115
menu: [{
1116
id: MenuId.ChatExecute,
1117
when: ContextKeyExpr.and(
1118
ChatContextKeys.chatModeKind.isEqualTo(ChatModeKind.Ask),
1119
ContextKeyExpr.not('config.chat.emptyChatState.enabled'),
1120
ChatContextKeys.lockedToCodingAgent.negate()
1121
),
1122
group: 'navigation',
1123
order: 1
1124
}]
1125
});
1126
}
1127
1128
override async run(accessor: ServicesAccessor, ...args: any[]): Promise<void> {
1129
const widgetService = accessor.get(IChatWidgetService);
1130
const context: { widget?: IChatWidget } | undefined = args[0];
1131
const widget = context?.widget ?? widgetService.lastFocusedWidget;
1132
if (!widget) {
1133
return;
1134
}
1135
1136
const hasAgentOrCommand = extractAgentAndCommand(widget.parsedInput);
1137
if (hasAgentOrCommand?.agentPart || hasAgentOrCommand?.commandPart) {
1138
return;
1139
}
1140
1141
const suggestCtrl = SuggestController.get(widget.inputEditor);
1142
if (suggestCtrl) {
1143
const curText = widget.inputEditor.getValue();
1144
const newValue = curText ? `@ ${curText}` : '@';
1145
if (!curText.startsWith('@')) {
1146
widget.inputEditor.setValue(newValue);
1147
}
1148
1149
widget.inputEditor.setPosition(new Position(1, 2));
1150
suggestCtrl.triggerSuggest(undefined, true);
1151
}
1152
}
1153
});
1154
1155
registerAction2(class ClearChatInputHistoryAction extends Action2 {
1156
constructor() {
1157
super({
1158
id: 'workbench.action.chat.clearInputHistory',
1159
title: localize2('interactiveSession.clearHistory.label', "Clear Input History"),
1160
precondition: ChatContextKeys.enabled,
1161
category: CHAT_CATEGORY,
1162
f1: true,
1163
});
1164
}
1165
async run(accessor: ServicesAccessor, ...args: any[]) {
1166
const historyService = accessor.get(IChatWidgetHistoryService);
1167
historyService.clearHistory();
1168
}
1169
});
1170
1171
registerAction2(class ClearChatHistoryAction extends Action2 {
1172
constructor() {
1173
super({
1174
id: CHAT_CLEAR_HISTORY_ACTION_ID,
1175
title: localize2('chat.clear.label', "Clear All Workspace Chats"),
1176
precondition: ChatContextKeys.enabled,
1177
category: CHAT_CATEGORY,
1178
f1: true,
1179
});
1180
}
1181
async run(accessor: ServicesAccessor, ...args: any[]) {
1182
const editorGroupsService = accessor.get(IEditorGroupsService);
1183
const chatService = accessor.get(IChatService);
1184
const instantiationService = accessor.get(IInstantiationService);
1185
const widgetService = accessor.get(IChatWidgetService);
1186
1187
await chatService.clearAllHistoryEntries();
1188
1189
widgetService.getAllWidgets().forEach(widget => {
1190
widget.clear();
1191
});
1192
1193
// Clear all chat editors. Have to go this route because the chat editor may be in the background and
1194
// not have a ChatEditorInput.
1195
editorGroupsService.groups.forEach(group => {
1196
group.editors.forEach(editor => {
1197
if (editor instanceof ChatEditorInput) {
1198
instantiationService.invokeFunction(clearChatEditor, editor);
1199
}
1200
});
1201
});
1202
}
1203
});
1204
1205
registerAction2(class FocusChatAction extends EditorAction2 {
1206
constructor() {
1207
super({
1208
id: 'chat.action.focus',
1209
title: localize2('actions.interactiveSession.focus', 'Focus Chat List'),
1210
precondition: ContextKeyExpr.and(ChatContextKeys.inChatInput),
1211
category: CHAT_CATEGORY,
1212
keybinding: [
1213
// On mac, require that the cursor is at the top of the input, to avoid stealing cmd+up to move the cursor to the top
1214
{
1215
when: ContextKeyExpr.and(ChatContextKeys.inputCursorAtTop, ChatContextKeys.inQuickChat.negate()),
1216
primary: KeyMod.CtrlCmd | KeyCode.UpArrow,
1217
weight: KeybindingWeight.EditorContrib,
1218
},
1219
// On win/linux, ctrl+up can always focus the chat list
1220
{
1221
when: ContextKeyExpr.and(ContextKeyExpr.or(IsWindowsContext, IsLinuxContext), ChatContextKeys.inQuickChat.negate()),
1222
primary: KeyMod.CtrlCmd | KeyCode.UpArrow,
1223
weight: KeybindingWeight.EditorContrib,
1224
},
1225
{
1226
when: ContextKeyExpr.and(ChatContextKeys.inChatSession, ChatContextKeys.inQuickChat),
1227
primary: KeyMod.CtrlCmd | KeyCode.DownArrow,
1228
weight: KeybindingWeight.WorkbenchContrib,
1229
}
1230
]
1231
});
1232
}
1233
1234
runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor): void | Promise<void> {
1235
const editorUri = editor.getModel()?.uri;
1236
if (editorUri) {
1237
const widgetService = accessor.get(IChatWidgetService);
1238
widgetService.getWidgetByInputUri(editorUri)?.focusLastMessage();
1239
}
1240
}
1241
});
1242
1243
registerAction2(class FocusChatInputAction extends Action2 {
1244
constructor() {
1245
super({
1246
id: 'workbench.action.chat.focusInput',
1247
title: localize2('interactiveSession.focusInput.label', "Focus Chat Input"),
1248
f1: false,
1249
keybinding: [
1250
{
1251
primary: KeyMod.CtrlCmd | KeyCode.DownArrow,
1252
weight: KeybindingWeight.WorkbenchContrib,
1253
when: ContextKeyExpr.and(ChatContextKeys.inChatSession, ChatContextKeys.inChatInput.negate(), ChatContextKeys.inQuickChat.negate()),
1254
},
1255
{
1256
when: ContextKeyExpr.and(ChatContextKeys.inChatSession, ChatContextKeys.inChatInput.negate(), ChatContextKeys.inQuickChat),
1257
primary: KeyMod.CtrlCmd | KeyCode.UpArrow,
1258
weight: KeybindingWeight.WorkbenchContrib,
1259
}
1260
]
1261
});
1262
}
1263
run(accessor: ServicesAccessor, ...args: any[]) {
1264
const widgetService = accessor.get(IChatWidgetService);
1265
widgetService.lastFocusedWidget?.focusInput();
1266
}
1267
});
1268
1269
const nonEnterpriseCopilotUsers = ContextKeyExpr.and(ChatContextKeys.enabled, ContextKeyExpr.notEquals(`config.${defaultChat.completionsAdvancedSetting}.authProvider`, defaultChat.provider.enterprise.id));
1270
registerAction2(class extends Action2 {
1271
constructor() {
1272
super({
1273
id: 'workbench.action.chat.manageSettings',
1274
title: localize2('manageChat', "Manage Chat"),
1275
category: CHAT_CATEGORY,
1276
f1: true,
1277
precondition: ContextKeyExpr.and(
1278
ContextKeyExpr.or(
1279
ChatContextKeys.Entitlement.planFree,
1280
ChatContextKeys.Entitlement.planPro,
1281
ChatContextKeys.Entitlement.planProPlus
1282
),
1283
nonEnterpriseCopilotUsers
1284
),
1285
menu: {
1286
id: MenuId.ChatTitleBarMenu,
1287
group: 'y_manage',
1288
order: 1,
1289
when: nonEnterpriseCopilotUsers
1290
}
1291
});
1292
}
1293
1294
override async run(accessor: ServicesAccessor): Promise<void> {
1295
const openerService = accessor.get(IOpenerService);
1296
openerService.open(URI.parse(defaultChat.manageSettingsUrl));
1297
}
1298
});
1299
1300
registerAction2(class ShowExtensionsUsingCopilot extends Action2 {
1301
1302
constructor() {
1303
super({
1304
id: 'workbench.action.chat.showExtensionsUsingCopilot',
1305
title: localize2('showCopilotUsageExtensions', "Show Extensions using Copilot"),
1306
f1: true,
1307
category: EXTENSIONS_CATEGORY,
1308
precondition: ChatContextKeys.enabled
1309
});
1310
}
1311
1312
override async run(accessor: ServicesAccessor): Promise<void> {
1313
const extensionsWorkbenchService = accessor.get(IExtensionsWorkbenchService);
1314
extensionsWorkbenchService.openSearch(`@feature:${CopilotUsageExtensionFeatureId}`);
1315
}
1316
});
1317
1318
registerAction2(class ConfigureCopilotCompletions extends Action2 {
1319
1320
constructor() {
1321
super({
1322
id: 'workbench.action.chat.configureCodeCompletions',
1323
title: localize2('configureCompletions', "Configure Code Completions..."),
1324
precondition: ContextKeyExpr.and(
1325
ChatContextKeys.Setup.installed,
1326
ChatContextKeys.Setup.disabled.negate(),
1327
ChatContextKeys.Setup.untrusted.negate()
1328
),
1329
menu: {
1330
id: MenuId.ChatTitleBarMenu,
1331
group: 'f_completions',
1332
order: 10,
1333
}
1334
});
1335
}
1336
1337
override async run(accessor: ServicesAccessor): Promise<void> {
1338
const commandService = accessor.get(ICommandService);
1339
commandService.executeCommand(defaultChat.completionsMenuCommand);
1340
}
1341
});
1342
1343
registerAction2(class ShowQuotaExceededDialogAction extends Action2 {
1344
1345
constructor() {
1346
super({
1347
id: OPEN_CHAT_QUOTA_EXCEEDED_DIALOG,
1348
title: localize('upgradeChat', "Upgrade GitHub Copilot Plan")
1349
});
1350
}
1351
1352
override async run(accessor: ServicesAccessor) {
1353
const chatEntitlementService = accessor.get(IChatEntitlementService);
1354
const commandService = accessor.get(ICommandService);
1355
const dialogService = accessor.get(IDialogService);
1356
const telemetryService = accessor.get(ITelemetryService);
1357
1358
let message: string;
1359
const chatQuotaExceeded = chatEntitlementService.quotas.chat?.percentRemaining === 0;
1360
const completionsQuotaExceeded = chatEntitlementService.quotas.completions?.percentRemaining === 0;
1361
if (chatQuotaExceeded && !completionsQuotaExceeded) {
1362
message = localize('chatQuotaExceeded', "You've reached your monthly chat messages quota. You still have free code completions available.");
1363
} else if (completionsQuotaExceeded && !chatQuotaExceeded) {
1364
message = localize('completionsQuotaExceeded', "You've reached your monthly code completions quota. You still have free chat messages available.");
1365
} else {
1366
message = localize('chatAndCompletionsQuotaExceeded', "You've reached your monthly chat messages and code completions quota.");
1367
}
1368
1369
if (chatEntitlementService.quotas.resetDate) {
1370
const dateFormatter = chatEntitlementService.quotas.resetDateHasTime ? safeIntl.DateTimeFormat(language, { year: 'numeric', month: 'long', day: 'numeric', hour: 'numeric', minute: 'numeric' }) : safeIntl.DateTimeFormat(language, { year: 'numeric', month: 'long', day: 'numeric' });
1371
const quotaResetDate = new Date(chatEntitlementService.quotas.resetDate);
1372
message = [message, localize('quotaResetDate', "The allowance will reset on {0}.", dateFormatter.value.format(quotaResetDate))].join(' ');
1373
}
1374
1375
const free = chatEntitlementService.entitlement === ChatEntitlement.Free;
1376
const upgradeToPro = free ? localize('upgradeToPro', "Upgrade to GitHub Copilot Pro (your first 30 days are free) for:\n- Unlimited code completions\n- Unlimited chat messages\n- Access to premium models") : undefined;
1377
1378
await dialogService.prompt({
1379
type: 'none',
1380
message: localize('copilotQuotaReached', "GitHub Copilot Quota Reached"),
1381
cancelButton: {
1382
label: localize('dismiss', "Dismiss"),
1383
run: () => { /* noop */ }
1384
},
1385
buttons: [
1386
{
1387
label: free ? localize('upgradePro', "Upgrade to GitHub Copilot Pro") : localize('upgradePlan', "Upgrade GitHub Copilot Plan"),
1388
run: () => {
1389
const commandId = 'workbench.action.chat.upgradePlan';
1390
telemetryService.publicLog2<WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification>('workbenchActionExecuted', { id: commandId, from: 'chat-dialog' });
1391
commandService.executeCommand(commandId);
1392
}
1393
},
1394
],
1395
custom: {
1396
icon: Codicon.copilotWarningLarge,
1397
markdownDetails: coalesce([
1398
{ markdown: new MarkdownString(message, true) },
1399
upgradeToPro ? { markdown: new MarkdownString(upgradeToPro, true) } : undefined
1400
])
1401
}
1402
});
1403
}
1404
});
1405
1406
registerAction2(class ResetTrustedToolsAction extends Action2 {
1407
constructor() {
1408
super({
1409
id: 'workbench.action.chat.resetTrustedTools',
1410
title: localize2('resetTrustedTools', "Reset Tool Confirmations"),
1411
category: CHAT_CATEGORY,
1412
f1: true,
1413
precondition: ChatContextKeys.enabled
1414
});
1415
}
1416
override run(accessor: ServicesAccessor): void {
1417
accessor.get(ILanguageModelToolsService).resetToolAutoConfirmation();
1418
accessor.get(INotificationService).info(localize('resetTrustedToolsSuccess', "Tool confirmation preferences have been reset."));
1419
}
1420
});
1421
1422
registerAction2(class UpdateInstructionsAction extends Action2 {
1423
constructor() {
1424
super({
1425
id: 'workbench.action.chat.generateInstructions',
1426
title: localize2('generateInstructions', "Generate Workspace Instructions File"),
1427
shortTitle: localize2('generateInstructions.short', "Generate Instructions"),
1428
category: CHAT_CATEGORY,
1429
icon: Codicon.sparkle,
1430
f1: true,
1431
precondition: ChatContextKeys.enabled,
1432
menu: {
1433
id: CHAT_CONFIG_MENU_ID,
1434
when: ContextKeyExpr.and(ChatContextKeys.enabled, ContextKeyExpr.equals('view', ChatViewId)),
1435
order: 13,
1436
group: '1_level'
1437
}
1438
});
1439
}
1440
1441
async run(accessor: ServicesAccessor): Promise<void> {
1442
const commandService = accessor.get(ICommandService);
1443
1444
// Use chat command to open and send the query
1445
const query = `Analyze this codebase to generate or update \`.github/copilot-instructions.md\` for guiding AI coding agents.
1446
1447
Focus on discovering the essential knowledge that would help an AI agents be immediately productive in this codebase. Consider aspects like:
1448
- The "big picture" architecture that requires reading multiple files to understand - major components, service boundaries, data flows, and the "why" behind structural decisions
1449
- Critical developer workflows (builds, tests, debugging) especially commands that aren't obvious from file inspection alone
1450
- Project-specific conventions and patterns that differ from common practices
1451
- Integration points, external dependencies, and cross-component communication patterns
1452
1453
Source existing AI conventions from \`**/{.github/copilot-instructions.md,AGENT.md,AGENTS.md,CLAUDE.md,.cursorrules,.windsurfrules,.clinerules,.cursor/rules/**,.windsurf/rules/**,.clinerules/**,README.md}\` (do one glob search).
1454
1455
Guidelines (read more at https://aka.ms/vscode-instructions-docs):
1456
- If \`.github/copilot-instructions.md\` exists, merge intelligently - preserve valuable content while updating outdated sections
1457
- Write concise, actionable instructions (~20-50 lines) using markdown structure
1458
- Include specific examples from the codebase when describing patterns
1459
- Avoid generic advice ("write tests", "handle errors") - focus on THIS project's specific approaches
1460
- Document only discoverable patterns, not aspirational practices
1461
- Reference key files/directories that exemplify important patterns
1462
1463
Update \`.github/copilot-instructions.md\` for the user, then ask for feedback on any unclear or incomplete sections to iterate.`;
1464
1465
await commandService.executeCommand('workbench.action.chat.open', {
1466
mode: 'agent',
1467
query: query,
1468
});
1469
}
1470
});
1471
1472
registerAction2(class OpenChatFeatureSettingsAction extends Action2 {
1473
constructor() {
1474
super({
1475
id: 'workbench.action.chat.openFeatureSettings',
1476
title: localize2('openChatFeatureSettings', "Chat Settings"),
1477
shortTitle: localize('openChatFeatureSettings.short', "Chat Settings"),
1478
category: CHAT_CATEGORY,
1479
f1: true,
1480
precondition: ChatContextKeys.enabled,
1481
menu: {
1482
id: CHAT_CONFIG_MENU_ID,
1483
when: ContextKeyExpr.and(ChatContextKeys.enabled, ContextKeyExpr.equals('view', ChatViewId)),
1484
order: 15,
1485
group: '2_configure'
1486
}
1487
});
1488
}
1489
1490
override async run(accessor: ServicesAccessor): Promise<void> {
1491
const preferencesService = accessor.get(IPreferencesService);
1492
preferencesService.openSettings({ query: '@feature:chat' });
1493
}
1494
});
1495
1496
MenuRegistry.appendMenuItem(MenuId.ViewTitle, {
1497
submenu: CHAT_CONFIG_MENU_ID,
1498
title: localize2('config.label', "Configure Chat..."),
1499
group: 'navigation',
1500
when: ContextKeyExpr.equals('view', ChatViewId),
1501
icon: Codicon.settingsGear,
1502
order: 6
1503
});
1504
}
1505
1506
export function stringifyItem(item: IChatRequestViewModel | IChatResponseViewModel, includeName = true): string {
1507
if (isRequestVM(item)) {
1508
return (includeName ? `${item.username}: ` : '') + item.messageText;
1509
} else {
1510
return (includeName ? `${item.username}: ` : '') + item.response.toString();
1511
}
1512
}
1513
1514
1515
// --- Title Bar Chat Controls
1516
1517
const defaultChat = {
1518
documentationUrl: product.defaultChatAgent?.documentationUrl ?? '',
1519
manageSettingsUrl: product.defaultChatAgent?.manageSettingsUrl ?? '',
1520
managePlanUrl: product.defaultChatAgent?.managePlanUrl ?? '',
1521
provider: product.defaultChatAgent?.provider ?? { enterprise: { id: '' } },
1522
completionsAdvancedSetting: product.defaultChatAgent?.completionsAdvancedSetting ?? '',
1523
completionsMenuCommand: product.defaultChatAgent?.completionsMenuCommand ?? '',
1524
};
1525
1526
// Add next to the command center if command center is disabled
1527
MenuRegistry.appendMenuItem(MenuId.CommandCenter, {
1528
submenu: MenuId.ChatTitleBarMenu,
1529
title: localize('title4', "Chat"),
1530
icon: Codicon.chatSparkle,
1531
when: ContextKeyExpr.and(
1532
ChatContextKeys.supported,
1533
ContextKeyExpr.and(
1534
ChatContextKeys.Setup.hidden.negate(),
1535
ChatContextKeys.Setup.disabled.negate()
1536
),
1537
ContextKeyExpr.has('config.chat.commandCenter.enabled')
1538
),
1539
order: 10001 // to the right of command center
1540
});
1541
1542
// Add to the global title bar if command center is disabled
1543
MenuRegistry.appendMenuItem(MenuId.TitleBar, {
1544
submenu: MenuId.ChatTitleBarMenu,
1545
title: localize('title4', "Chat"),
1546
group: 'navigation',
1547
icon: Codicon.chatSparkle,
1548
when: ContextKeyExpr.and(
1549
ChatContextKeys.supported,
1550
ContextKeyExpr.and(
1551
ChatContextKeys.Setup.hidden.negate(),
1552
ChatContextKeys.Setup.disabled.negate()
1553
),
1554
ContextKeyExpr.has('config.chat.commandCenter.enabled'),
1555
ContextKeyExpr.has('config.window.commandCenter').negate(),
1556
),
1557
order: 1
1558
});
1559
1560
registerAction2(class ToggleCopilotControl extends ToggleTitleBarConfigAction {
1561
constructor() {
1562
super(
1563
'chat.commandCenter.enabled',
1564
localize('toggle.chatControl', 'Chat Controls'),
1565
localize('toggle.chatControlsDescription', "Toggle visibility of the Chat Controls in title bar"), 5,
1566
ContextKeyExpr.and(
1567
ContextKeyExpr.and(
1568
ChatContextKeys.Setup.hidden.negate(),
1569
ChatContextKeys.Setup.disabled.negate()
1570
),
1571
IsCompactTitleBarContext.negate(),
1572
ChatContextKeys.supported
1573
)
1574
);
1575
}
1576
});
1577
1578
export class CopilotTitleBarMenuRendering extends Disposable implements IWorkbenchContribution {
1579
1580
static readonly ID = 'workbench.contrib.copilotTitleBarMenuRendering';
1581
1582
constructor(
1583
@IActionViewItemService actionViewItemService: IActionViewItemService,
1584
@IChatEntitlementService chatEntitlementService: IChatEntitlementService,
1585
) {
1586
super();
1587
1588
const disposable = actionViewItemService.register(MenuId.CommandCenter, MenuId.ChatTitleBarMenu, (action, options, instantiationService, windowId) => {
1589
if (!(action instanceof SubmenuItemAction)) {
1590
return undefined;
1591
}
1592
1593
const dropdownAction = toAction({
1594
id: 'copilot.titleBarMenuRendering.more',
1595
label: localize('more', "More..."),
1596
run() { }
1597
});
1598
1599
const chatSentiment = chatEntitlementService.sentiment;
1600
const chatQuotaExceeded = chatEntitlementService.quotas.chat?.percentRemaining === 0;
1601
const signedOut = chatEntitlementService.entitlement === ChatEntitlement.Unknown;
1602
const free = chatEntitlementService.entitlement === ChatEntitlement.Free;
1603
1604
const isAuxiliaryWindow = windowId !== mainWindow.vscodeWindowId;
1605
let primaryActionId = isAuxiliaryWindow ? CHAT_OPEN_ACTION_ID : TOGGLE_CHAT_ACTION_ID;
1606
let primaryActionTitle = isAuxiliaryWindow ? localize('openChat', "Open Chat") : localize('toggleChat', "Toggle Chat");
1607
let primaryActionIcon = Codicon.chatSparkle;
1608
if (chatSentiment.installed && !chatSentiment.disabled) {
1609
if (signedOut) {
1610
primaryActionId = CHAT_SETUP_ACTION_ID;
1611
primaryActionTitle = localize('signInToChatSetup', "Sign in to use AI features...");
1612
primaryActionIcon = Codicon.chatSparkleError;
1613
} else if (chatQuotaExceeded && free) {
1614
primaryActionId = OPEN_CHAT_QUOTA_EXCEEDED_DIALOG;
1615
primaryActionTitle = localize('chatQuotaExceededButton', "GitHub Copilot Free plan chat messages quota reached. Click for details.");
1616
primaryActionIcon = Codicon.chatSparkleWarning;
1617
}
1618
}
1619
return instantiationService.createInstance(DropdownWithPrimaryActionViewItem, instantiationService.createInstance(MenuItemAction, {
1620
id: primaryActionId,
1621
title: primaryActionTitle,
1622
icon: primaryActionIcon,
1623
}, undefined, undefined, undefined, undefined), dropdownAction, action.actions, '', { ...options, skipTelemetry: true });
1624
}, Event.any(
1625
chatEntitlementService.onDidChangeSentiment,
1626
chatEntitlementService.onDidChangeQuotaExceeded,
1627
chatEntitlementService.onDidChangeEntitlement
1628
));
1629
1630
// Reduces flicker a bit on reload/restart
1631
markAsSingleton(disposable);
1632
}
1633
}
1634
1635
/**
1636
* Returns whether we can continue clearing/switching chat sessions, false to cancel.
1637
*/
1638
export async function handleCurrentEditingSession(currentEditingSession: IChatEditingSession, phrase: string | undefined, dialogService: IDialogService): Promise<boolean> {
1639
if (shouldShowClearEditingSessionConfirmation(currentEditingSession)) {
1640
return showClearEditingSessionConfirmation(currentEditingSession, dialogService, { messageOverride: phrase });
1641
}
1642
1643
return true;
1644
}
1645
1646
/**
1647
* Returns whether we can switch the chat mode, based on whether the user had to agree to clear the session, false to cancel.
1648
*/
1649
export async function handleModeSwitch(
1650
accessor: ServicesAccessor,
1651
fromMode: ChatModeKind,
1652
toMode: ChatModeKind,
1653
requestCount: number,
1654
editingSession: IChatEditingSession | undefined,
1655
): Promise<false | { needToClearSession: boolean }> {
1656
if (!editingSession || fromMode === toMode) {
1657
return { needToClearSession: false };
1658
}
1659
1660
const configurationService = accessor.get(IConfigurationService);
1661
const dialogService = accessor.get(IDialogService);
1662
const needToClearEdits = (!configurationService.getValue(ChatConfiguration.Edits2Enabled) && (fromMode === ChatModeKind.Edit || toMode === ChatModeKind.Edit)) && requestCount > 0;
1663
if (needToClearEdits) {
1664
// If not using edits2 and switching into or out of edit mode, ask to discard the session
1665
const phrase = localize('switchMode.confirmPhrase', "Switching chat modes will end your current edit session.");
1666
1667
const currentEdits = editingSession.entries.get();
1668
const undecidedEdits = currentEdits.filter((edit) => edit.state.get() === ModifiedFileEntryState.Modified);
1669
if (undecidedEdits.length > 0) {
1670
if (!await handleCurrentEditingSession(editingSession, phrase, dialogService)) {
1671
return false;
1672
}
1673
1674
return { needToClearSession: true };
1675
} else {
1676
const confirmation = await dialogService.confirm({
1677
title: localize('agent.newSession', "Start new session?"),
1678
message: localize('agent.newSessionMessage', "Changing the chat mode will end your current edit session. Would you like to change the chat mode?"),
1679
primaryButton: localize('agent.newSession.confirm', "Yes"),
1680
type: 'info'
1681
});
1682
if (!confirmation.confirmed) {
1683
return false;
1684
}
1685
1686
return { needToClearSession: true };
1687
}
1688
}
1689
1690
return { needToClearSession: false };
1691
}
1692
1693
export interface IClearEditingSessionConfirmationOptions {
1694
titleOverride?: string;
1695
messageOverride?: string;
1696
}
1697
1698
1699
// --- Chat Submenus in various Components
1700
1701
MenuRegistry.appendMenuItem(MenuId.EditorContext, {
1702
submenu: MenuId.ChatTextEditorMenu,
1703
group: '1_chat',
1704
order: 5,
1705
title: localize('generateCode', "Generate Code"),
1706
when: ContextKeyExpr.and(
1707
ChatContextKeys.Setup.hidden.negate(),
1708
ChatContextKeys.Setup.disabled.negate()
1709
)
1710
});
1711
1712
// TODO@bpasero remove these when Chat extension is built-in
1713
{
1714
function registerGenerateCodeCommand(coreCommand: string, actualCommand: string): void {
1715
CommandsRegistry.registerCommand(coreCommand, async accessor => {
1716
const commandService = accessor.get(ICommandService);
1717
const telemetryService = accessor.get(ITelemetryService);
1718
const editorGroupService = accessor.get(IEditorGroupsService);
1719
1720
telemetryService.publicLog2<WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification>('workbenchActionExecuted', { id: CHAT_SETUP_ACTION_ID, from: 'editor' });
1721
1722
if (editorGroupService.activeGroup.activeEditor) {
1723
// Pinning the editor helps when the Chat extension welcome kicks in after install to keep context
1724
editorGroupService.activeGroup.pinEditor(editorGroupService.activeGroup.activeEditor);
1725
}
1726
1727
const result = await commandService.executeCommand(CHAT_SETUP_ACTION_ID);
1728
if (!result) {
1729
return;
1730
}
1731
1732
await commandService.executeCommand(actualCommand);
1733
});
1734
}
1735
registerGenerateCodeCommand('chat.internal.explain', 'github.copilot.chat.explain');
1736
registerGenerateCodeCommand('chat.internal.fix', 'github.copilot.chat.fix');
1737
registerGenerateCodeCommand('chat.internal.review', 'github.copilot.chat.review');
1738
registerGenerateCodeCommand('chat.internal.generateDocs', 'github.copilot.chat.generateDocs');
1739
registerGenerateCodeCommand('chat.internal.generateTests', 'github.copilot.chat.generateTests');
1740
1741
const internalGenerateCodeContext = ContextKeyExpr.and(
1742
ChatContextKeys.Setup.hidden.negate(),
1743
ChatContextKeys.Setup.disabled.negate(),
1744
ChatContextKeys.Setup.installed.negate(),
1745
);
1746
1747
MenuRegistry.appendMenuItem(MenuId.EditorContext, {
1748
command: {
1749
id: 'chat.internal.explain',
1750
title: localize('explain', "Explain"),
1751
},
1752
group: '1_chat',
1753
order: 4,
1754
when: internalGenerateCodeContext
1755
});
1756
1757
MenuRegistry.appendMenuItem(MenuId.ChatTextEditorMenu, {
1758
command: {
1759
id: 'chat.internal.fix',
1760
title: localize('fix', "Fix"),
1761
},
1762
group: '1_action',
1763
order: 1,
1764
when: ContextKeyExpr.and(
1765
internalGenerateCodeContext,
1766
EditorContextKeys.readOnly.negate()
1767
)
1768
});
1769
1770
MenuRegistry.appendMenuItem(MenuId.ChatTextEditorMenu, {
1771
command: {
1772
id: 'chat.internal.review',
1773
title: localize('review', "Code Review"),
1774
},
1775
group: '1_action',
1776
order: 2,
1777
when: internalGenerateCodeContext
1778
});
1779
1780
MenuRegistry.appendMenuItem(MenuId.ChatTextEditorMenu, {
1781
command: {
1782
id: 'chat.internal.generateDocs',
1783
title: localize('generateDocs', "Generate Docs"),
1784
},
1785
group: '2_generate',
1786
order: 1,
1787
when: ContextKeyExpr.and(
1788
internalGenerateCodeContext,
1789
EditorContextKeys.readOnly.negate()
1790
)
1791
});
1792
1793
MenuRegistry.appendMenuItem(MenuId.ChatTextEditorMenu, {
1794
command: {
1795
id: 'chat.internal.generateTests',
1796
title: localize('generateTests', "Generate Tests"),
1797
},
1798
group: '2_generate',
1799
order: 2,
1800
when: ContextKeyExpr.and(
1801
internalGenerateCodeContext,
1802
EditorContextKeys.readOnly.negate()
1803
)
1804
});
1805
}
1806
1807
1808
// --- Chat Default Visibility
1809
1810
registerAction2(class ToggleDefaultVisibilityAction extends Action2 {
1811
constructor() {
1812
super({
1813
id: 'workbench.action.chat.toggleDefaultVisibility',
1814
title: localize2('chat.toggleDefaultVisibility.label', "Show View by Default"),
1815
toggled: ContextKeyExpr.equals('config.workbench.secondarySideBar.defaultVisibility', 'hidden').negate(),
1816
f1: false,
1817
menu: {
1818
id: MenuId.ViewTitle,
1819
when: ContextKeyExpr.and(
1820
ContextKeyExpr.equals('view', ChatViewId),
1821
ChatContextKeys.panelLocation.isEqualTo(ViewContainerLocation.AuxiliaryBar),
1822
),
1823
order: 0,
1824
group: '5_configure'
1825
},
1826
});
1827
}
1828
1829
async run(accessor: ServicesAccessor) {
1830
const configurationService = accessor.get(IConfigurationService);
1831
1832
const currentValue = configurationService.getValue<'hidden' | unknown>('workbench.secondarySideBar.defaultVisibility');
1833
configurationService.updateValue('workbench.secondarySideBar.defaultVisibility', currentValue !== 'hidden' ? 'hidden' : 'visible');
1834
}
1835
});
1836
1837
registerAction2(class EditToolApproval extends Action2 {
1838
constructor() {
1839
super({
1840
id: 'workbench.action.chat.editToolApproval',
1841
title: localize2('chat.editToolApproval.label', "Edit Tool Approval"),
1842
f1: false
1843
});
1844
}
1845
1846
async run(accessor: ServicesAccessor, toolId: string): Promise<void> {
1847
if (!toolId) {
1848
return;
1849
}
1850
1851
const quickInputService = accessor.get(IQuickInputService);
1852
const toolsService = accessor.get(ILanguageModelToolsService);
1853
const tool = toolsService.getTool(toolId);
1854
if (!tool) {
1855
return;
1856
}
1857
1858
const currentState = toolsService.getToolAutoConfirmation(toolId);
1859
1860
interface TItem extends IQuickPickItem {
1861
id: 'session' | 'workspace' | 'profile' | 'never';
1862
}
1863
1864
const items: TItem[] = [
1865
{ id: 'never', label: localize('chat.toolApproval.manual', "Always require manual approval") },
1866
{ id: 'session', label: localize('chat.toolApproval.session', "Auto-approve for this session") },
1867
{ id: 'workspace', label: localize('chat.toolApproval.workspace', "Auto-approve for this workspace") },
1868
{ id: 'profile', label: localize('chat.toolApproval.profile', "Auto-approve globally") }
1869
];
1870
1871
const quickPick = quickInputService.createQuickPick<TItem>();
1872
quickPick.placeholder = localize('chat.editToolApproval.title', "Approval setting for {0}", tool.displayName ?? tool.id);
1873
quickPick.items = items;
1874
quickPick.canSelectMany = false;
1875
quickPick.activeItems = items.filter(item => item.id === currentState);
1876
1877
const selection = await new Promise<TItem | undefined>((resolve) => {
1878
quickPick.onDidAccept(() => {
1879
const selected = quickPick.selectedItems[0];
1880
resolve(selected);
1881
});
1882
quickPick.onDidHide(() => {
1883
resolve(undefined);
1884
1885
});
1886
quickPick.show();
1887
});
1888
1889
quickPick.dispose();
1890
1891
if (selection) {
1892
toolsService.setToolAutoConfirmation(toolId, selection.id);
1893
}
1894
}
1895
});
1896
1897