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
5310 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 { alert } from '../../../../../base/browser/ui/aria/aria.js';
8
import { WorkbenchActionExecutedClassification, WorkbenchActionExecutedEvent } from '../../../../../base/common/actions.js';
9
import { coalesce } from '../../../../../base/common/arrays.js';
10
import { timeout } from '../../../../../base/common/async.js';
11
import { Codicon } from '../../../../../base/common/codicons.js';
12
import { 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 { language } from '../../../../../base/common/platform.js';
17
import { basename } from '../../../../../base/common/resources.js';
18
import { ThemeIcon } from '../../../../../base/common/themables.js';
19
import { URI } from '../../../../../base/common/uri.js';
20
import { ICodeEditor } from '../../../../../editor/browser/editorBrowser.js';
21
import { EditorAction2 } from '../../../../../editor/browser/editorExtensions.js';
22
import { IRange } from '../../../../../editor/common/core/range.js';
23
import { localize, localize2 } from '../../../../../nls.js';
24
import { Action2, ICommandPaletteOptions, MenuId, MenuRegistry, registerAction2 } from '../../../../../platform/actions/common/actions.js';
25
import { ICommandService } from '../../../../../platform/commands/common/commands.js';
26
import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';
27
import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js';
28
import { IsLinuxContext, IsWindowsContext } from '../../../../../platform/contextkey/common/contextkeys.js';
29
import { IDialogService } from '../../../../../platform/dialogs/common/dialogs.js';
30
import { IFileService } from '../../../../../platform/files/common/files.js';
31
import { IInstantiationService, ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js';
32
import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js';
33
import { ILogService } from '../../../../../platform/log/common/log.js';
34
import { INotificationService } from '../../../../../platform/notification/common/notification.js';
35
import { IOpenerService } from '../../../../../platform/opener/common/opener.js';
36
import product from '../../../../../platform/product/common/product.js';
37
import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js';
38
import { ActiveEditorContext } from '../../../../common/contextkeys.js';
39
import { IViewDescriptorService, ViewContainerLocation } from '../../../../common/views.js';
40
import { ChatEntitlement, IChatEntitlementService } from '../../../../services/chat/common/chatEntitlementService.js';
41
import { ACTIVE_GROUP, AUX_WINDOW_GROUP } from '../../../../services/editor/common/editorService.js';
42
import { IHostService } from '../../../../services/host/browser/host.js';
43
import { IWorkbenchLayoutService, Parts } from '../../../../services/layout/browser/layoutService.js';
44
import { IPreferencesService } from '../../../../services/preferences/common/preferences.js';
45
import { IViewsService } from '../../../../services/views/common/viewsService.js';
46
import { EXTENSIONS_CATEGORY, IExtensionsWorkbenchService } from '../../../extensions/common/extensions.js';
47
import { SCMHistoryItemChangeRangeContentProvider, ScmHistoryItemChangeRangeUriFields } from '../../../scm/browser/scmHistoryChatContext.js';
48
import { ISCMService } from '../../../scm/common/scm.js';
49
import { IChatAgentResult, IChatAgentService } from '../../common/participants/chatAgents.js';
50
import { ChatContextKeys } from '../../common/actions/chatContextKeys.js';
51
import { ModifiedFileEntryState } from '../../common/editing/chatEditingService.js';
52
import { IChatModel, IChatResponseModel } from '../../common/model/chatModel.js';
53
import { ChatMode, IChatMode, IChatModeService } from '../../common/chatModes.js';
54
import { ElicitationState, IChatService, IChatToolInvocation } from '../../common/chatService/chatService.js';
55
import { ISCMHistoryItemChangeRangeVariableEntry, ISCMHistoryItemChangeVariableEntry } from '../../common/attachments/chatVariableEntries.js';
56
import { IChatRequestViewModel, IChatResponseViewModel, isRequestVM } from '../../common/model/chatViewModel.js';
57
import { IChatWidgetHistoryService } from '../../common/widget/chatWidgetHistoryService.js';
58
import { AgentsControlClickBehavior, ChatAgentLocation, ChatConfiguration, ChatModeKind } from '../../common/constants.js';
59
import { ILanguageModelChatSelector, ILanguageModelsService } from '../../common/languageModels.js';
60
import { CopilotUsageExtensionFeatureId } from '../../common/languageModelStats.js';
61
import { ILanguageModelToolsConfirmationService } from '../../common/tools/languageModelToolsConfirmationService.js';
62
import { ILanguageModelToolsService, IToolData, IToolSet, isToolSet } from '../../common/tools/languageModelToolsService.js';
63
import { ChatViewId, IChatWidget, IChatWidgetService } from '../chat.js';
64
import { IChatEditorOptions } from '../widgetHosts/editor/chatEditor.js';
65
import { ChatEditorInput, showClearEditingSessionConfirmation } from '../widgetHosts/editor/chatEditorInput.js';
66
import { convertBufferToScreenshotVariable } from '../attachments/chatScreenshotContext.js';
67
import { LocalChatSessionUri } from '../../common/model/chatUri.js';
68
69
export const CHAT_CATEGORY = localize2('chat.category', 'Chat');
70
71
export const ACTION_ID_NEW_CHAT = `workbench.action.chat.newChat`;
72
export const ACTION_ID_NEW_EDIT_SESSION = `workbench.action.chat.newEditSession`;
73
export const ACTION_ID_OPEN_CHAT = 'workbench.action.openChat';
74
export const CHAT_OPEN_ACTION_ID = 'workbench.action.chat.open';
75
export const CHAT_SETUP_ACTION_ID = 'workbench.action.chat.triggerSetup';
76
export const CHAT_SETUP_SUPPORT_ANONYMOUS_ACTION_ID = 'workbench.action.chat.triggerSetupSupportAnonymousAction';
77
const TOGGLE_CHAT_ACTION_ID = 'workbench.action.chat.toggle';
78
79
const defaultChat = {
80
manageSettingsUrl: product.defaultChatAgent?.manageSettingsUrl ?? '',
81
provider: product.defaultChatAgent?.provider ?? { enterprise: { id: '' } },
82
completionsAdvancedSetting: product.defaultChatAgent?.completionsAdvancedSetting ?? '',
83
completionsMenuCommand: product.defaultChatAgent?.completionsMenuCommand ?? '',
84
};
85
86
export interface IChatViewOpenOptions {
87
/**
88
* The query for chat.
89
*/
90
query: string;
91
/**
92
* Whether the query is partial and will await more input from the user.
93
*/
94
isPartialQuery?: boolean;
95
/**
96
* A list of tools IDs with `canBeReferencedInPrompt` that will be resolved and attached if they exist.
97
*/
98
toolIds?: string[];
99
/**
100
* Any previous chat requests and responses that should be shown in the chat view.
101
*/
102
previousRequests?: IChatViewOpenRequestEntry[];
103
/**
104
* Whether a screenshot of the focused window should be taken and attached
105
*/
106
attachScreenshot?: boolean;
107
/**
108
* A list of file URIs to attach to the chat as context.
109
*/
110
attachFiles?: (URI | { uri: URI; range: IRange })[];
111
/**
112
* A list of source control history item changes to attach to the chat as context.
113
*/
114
attachHistoryItemChanges?: { uri: URI; historyItemId: string }[];
115
/**
116
* A list of source control history item change ranges to attach to the chat as context.
117
*/
118
attachHistoryItemChangeRanges?: {
119
start: { uri: URI; historyItemId: string };
120
end: { uri: URI; historyItemId: string };
121
}[];
122
/**
123
* The mode ID or name to open the chat in.
124
*/
125
mode?: ChatModeKind | string;
126
127
/**
128
* The language model selector to use for the chat.
129
* An Error will be thrown if there's no match. If there are multiple
130
* matches, the first match will be used.
131
*
132
* Examples:
133
*
134
* ```
135
* {
136
* id: 'claude-sonnet-4',
137
* vendor: 'copilot'
138
* }
139
* ```
140
*
141
* Use `claude-sonnet-4` from any vendor:
142
*
143
* ```
144
* {
145
* id: 'claude-sonnet-4',
146
* }
147
* ```
148
*/
149
modelSelector?: ILanguageModelChatSelector;
150
151
/**
152
* Wait to resolve the command until the chat response reaches a terminal state (complete, error, or pending user confirmation, etc.).
153
*/
154
blockOnResponse?: boolean;
155
156
/**
157
* A list of tool identifiers to include. When specified alone, only these tools will be enabled.
158
* Identifiers can be tool IDs, tool reference names (`toolReferenceName`),
159
* toolset IDs, or toolset reference names (`referenceName`).
160
* When a toolset identifier matches, all tools in that toolset are included.
161
* Can be combined with `toolsExclude` for fine-grained control.
162
*/
163
toolsInclude?: string[];
164
165
/**
166
* A list of tool identifiers to exclude. When specified alone, all tools except these will be enabled.
167
* Identifiers can be tool IDs, tool reference names (`toolReferenceName`),
168
* toolset IDs, or toolset reference names (`referenceName`).
169
* When a toolset identifier matches, all tools in that toolset are excluded.
170
* Can be combined with `toolsInclude` - exclusions are applied after inclusions.
171
* Explicit tool references in `toolsInclude` override toolset exclusions,
172
* but explicit tool exclusions always win.
173
*/
174
toolsExclude?: string[];
175
}
176
177
export interface IChatViewOpenRequestEntry {
178
request: string;
179
response: string;
180
}
181
182
export const CHAT_CONFIG_MENU_ID = new MenuId('workbench.chat.menu.config');
183
184
const OPEN_CHAT_QUOTA_EXCEEDED_DIALOG = 'workbench.action.chat.openQuotaExceededDialog';
185
186
abstract class OpenChatGlobalAction extends Action2 {
187
constructor(overrides: Pick<ICommandPaletteOptions, 'keybinding' | 'title' | 'id' | 'menu'>, private readonly mode?: IChatMode) {
188
super({
189
...overrides,
190
icon: Codicon.chatSparkle,
191
f1: true,
192
category: CHAT_CATEGORY,
193
precondition: ContextKeyExpr.and(
194
ChatContextKeys.Setup.hidden.negate(),
195
ChatContextKeys.Setup.disabled.negate()
196
)
197
});
198
}
199
200
override async run(accessor: ServicesAccessor, opts?: string | IChatViewOpenOptions): Promise<IChatAgentResult & { type?: 'confirmation' } | undefined> {
201
opts = typeof opts === 'string' ? { query: opts } : opts;
202
203
const chatService = accessor.get(IChatService);
204
const widgetService = accessor.get(IChatWidgetService);
205
const toolsService = accessor.get(ILanguageModelToolsService);
206
const hostService = accessor.get(IHostService);
207
const chatAgentService = accessor.get(IChatAgentService);
208
const instaService = accessor.get(IInstantiationService);
209
const commandService = accessor.get(ICommandService);
210
const chatModeService = accessor.get(IChatModeService);
211
const fileService = accessor.get(IFileService);
212
const languageModelService = accessor.get(ILanguageModelsService);
213
const scmService = accessor.get(ISCMService);
214
const logService = accessor.get(ILogService);
215
const configurationService = accessor.get(IConfigurationService);
216
217
let chatWidget = widgetService.lastFocusedWidget;
218
// When this was invoked to switch to a mode via keybinding, and some chat widget is focused, use that one.
219
// Otherwise, open the view.
220
if (!this.mode || !chatWidget || !isAncestorOfActiveElement(chatWidget.domNode)) {
221
chatWidget = await widgetService.revealWidget();
222
}
223
224
if (!chatWidget) {
225
return;
226
}
227
228
const switchToMode = (opts?.mode ? chatModeService.findModeByName(opts?.mode) : undefined) ?? this.mode;
229
if (switchToMode) {
230
await this.handleSwitchToMode(switchToMode, chatWidget, instaService, commandService);
231
}
232
233
if (opts?.modelSelector) {
234
const ids = await languageModelService.selectLanguageModels(opts.modelSelector);
235
const id = ids.sort().at(0);
236
if (!id) {
237
throw new Error(`No language models found matching selector: ${JSON.stringify(opts.modelSelector)}.`);
238
}
239
240
const model = languageModelService.lookupLanguageModel(id);
241
if (!model) {
242
throw new Error(`Language model not loaded: ${id}.`);
243
}
244
245
chatWidget.input.setCurrentLanguageModel({ metadata: model, identifier: id });
246
}
247
248
if (opts?.toolsInclude || opts?.toolsExclude) {
249
const model = chatWidget.input.selectedLanguageModel.get()?.metadata;
250
const allTools = Array.from(toolsService.getTools(model));
251
const allToolSets = Array.from(toolsService.getToolSetsForModel(model));
252
253
const result = computeToolEnablementMap({
254
allTools,
255
allToolSets,
256
toolsInclude: opts.toolsInclude,
257
toolsExclude: opts.toolsExclude,
258
});
259
260
for (const identifier of result.unknownIdentifiers) {
261
logService.warn(`Tool filtering: Unknown identifier '${identifier}' - no matching tool or toolset found.`);
262
}
263
264
chatWidget.input.selectedToolsModel.set(result.enablementMap, true);
265
}
266
267
if (opts?.previousRequests?.length && chatWidget.viewModel) {
268
for (const { request, response } of opts.previousRequests) {
269
chatService.addCompleteRequest(chatWidget.viewModel.sessionResource, request, undefined, 0, { message: response });
270
}
271
}
272
if (opts?.attachScreenshot) {
273
const screenshot = await hostService.getScreenshot();
274
if (screenshot) {
275
chatWidget.attachmentModel.addContext(convertBufferToScreenshotVariable(screenshot));
276
}
277
}
278
if (opts?.attachFiles) {
279
for (const file of opts.attachFiles) {
280
const uri = file instanceof URI ? file : file.uri;
281
const range = file instanceof URI ? undefined : file.range;
282
283
if (await fileService.exists(uri)) {
284
chatWidget.attachmentModel.addFile(uri, range);
285
}
286
}
287
}
288
if (opts?.attachHistoryItemChanges) {
289
for (const historyItemChange of opts.attachHistoryItemChanges) {
290
const repository = scmService.getRepository(URI.file(historyItemChange.uri.path));
291
const historyProvider = repository?.provider.historyProvider.get();
292
if (!historyProvider) {
293
continue;
294
}
295
296
const historyItem = await historyProvider.resolveHistoryItem(historyItemChange.historyItemId);
297
if (!historyItem) {
298
continue;
299
}
300
301
chatWidget.attachmentModel.addContext({
302
id: historyItemChange.uri.toString(),
303
name: `${basename(historyItemChange.uri)}`,
304
value: historyItemChange.uri,
305
historyItem: historyItem,
306
kind: 'scmHistoryItemChange'
307
} satisfies ISCMHistoryItemChangeVariableEntry);
308
}
309
}
310
if (opts?.attachHistoryItemChangeRanges) {
311
for (const historyItemChangeRange of opts.attachHistoryItemChangeRanges) {
312
const repository = scmService.getRepository(URI.file(historyItemChangeRange.end.uri.path));
313
const historyProvider = repository?.provider.historyProvider.get();
314
if (!repository || !historyProvider) {
315
continue;
316
}
317
318
const [historyItemStart, historyItemEnd] = await Promise.all([
319
historyProvider.resolveHistoryItem(historyItemChangeRange.start.historyItemId),
320
historyProvider.resolveHistoryItem(historyItemChangeRange.end.historyItemId),
321
]);
322
if (!historyItemStart || !historyItemEnd) {
323
continue;
324
}
325
326
const uri = historyItemChangeRange.end.uri.with({
327
scheme: SCMHistoryItemChangeRangeContentProvider.scheme,
328
query: JSON.stringify({
329
repositoryId: repository.id,
330
start: historyItemStart.id,
331
end: historyItemChangeRange.end.historyItemId
332
} satisfies ScmHistoryItemChangeRangeUriFields)
333
});
334
335
chatWidget.attachmentModel.addContext({
336
id: uri.toString(),
337
name: `${basename(uri)}`,
338
value: uri,
339
historyItemChangeStart: {
340
uri: historyItemChangeRange.start.uri,
341
historyItem: historyItemStart
342
},
343
historyItemChangeEnd: {
344
uri: historyItemChangeRange.end.uri,
345
historyItem: {
346
...historyItemEnd,
347
displayId: historyItemChangeRange.end.historyItemId
348
}
349
},
350
kind: 'scmHistoryItemChangeRange'
351
} satisfies ISCMHistoryItemChangeRangeVariableEntry);
352
}
353
}
354
355
let resp: Promise<IChatResponseModel | undefined> | undefined;
356
357
if (opts?.query) {
358
359
if (opts.isPartialQuery) {
360
chatWidget.setInput(opts.query);
361
} else {
362
if (!chatWidget.viewModel) {
363
await Event.toPromise(chatWidget.onDidChangeViewModel);
364
}
365
await waitForDefaultAgent(chatAgentService, chatWidget.input.currentModeKind);
366
chatWidget.setInput(opts.query); // wait until the model is restored before setting the input, or it will be cleared when the model is restored
367
resp = chatWidget.acceptInput();
368
}
369
}
370
371
if (opts?.toolIds && opts.toolIds.length > 0) {
372
for (const toolId of opts.toolIds) {
373
const tool = toolsService.getTool(toolId);
374
if (tool) {
375
chatWidget.attachmentModel.addContext({
376
id: tool.id,
377
name: tool.displayName,
378
fullName: tool.displayName,
379
value: undefined,
380
icon: ThemeIcon.isThemeIcon(tool.icon) ? tool.icon : undefined,
381
kind: 'tool'
382
});
383
}
384
}
385
}
386
387
chatWidget.focusInput();
388
389
if (opts?.blockOnResponse) {
390
const response = await resp;
391
if (response) {
392
const autoReplyEnabled = configurationService.getValue<boolean>(ChatConfiguration.AutoReply);
393
await new Promise<void>(resolve => {
394
const d = response.onDidChange(async () => {
395
if (response.isComplete) {
396
d.dispose();
397
resolve();
398
return;
399
}
400
401
const pendingConfirmation = response.isPendingConfirmation.get();
402
if (pendingConfirmation) {
403
// Check if the pending confirmation is a question carousel that will be auto-replied.
404
// Only question carousels are auto-replied; other confirmation types (tool approvals,
405
// elicitations, etc.) should cause us to resolve immediately.
406
const hasPendingQuestionCarousel = response.response.value.some(
407
part => part.kind === 'questionCarousel' && !part.isUsed
408
);
409
if (autoReplyEnabled && hasPendingQuestionCarousel) {
410
// Auto-reply will handle this question carousel, keep waiting
411
return;
412
}
413
d.dispose();
414
resolve();
415
}
416
});
417
});
418
419
const confirmationInfo = getPendingConfirmationInfo(response);
420
if (confirmationInfo) {
421
return { ...response.result, ...confirmationInfo };
422
}
423
return { ...response.result };
424
}
425
}
426
427
return undefined;
428
}
429
430
private async handleSwitchToMode(switchToMode: IChatMode, chatWidget: IChatWidget, instaService: IInstantiationService, commandService: ICommandService): Promise<void> {
431
const currentMode = chatWidget.input.currentModeKind;
432
433
if (switchToMode) {
434
const model = chatWidget.viewModel?.model;
435
const chatModeCheck = model ? await instaService.invokeFunction(handleModeSwitch, currentMode, switchToMode.kind, model.getRequests().length, model) : { needToClearSession: false };
436
if (!chatModeCheck) {
437
return;
438
}
439
chatWidget.input.setChatMode(switchToMode.id);
440
441
if (chatModeCheck.needToClearSession) {
442
await commandService.executeCommand(ACTION_ID_NEW_CHAT);
443
}
444
}
445
}
446
}
447
448
async function waitForDefaultAgent(chatAgentService: IChatAgentService, mode: ChatModeKind): Promise<void> {
449
const defaultAgent = chatAgentService.getDefaultAgent(ChatAgentLocation.Chat, mode);
450
if (defaultAgent) {
451
return;
452
}
453
454
await Promise.race([
455
Event.toPromise(Event.filter(chatAgentService.onDidChangeAgents, () => {
456
const defaultAgent = chatAgentService.getDefaultAgent(ChatAgentLocation.Chat, mode);
457
return Boolean(defaultAgent);
458
})),
459
timeout(60_000).then(() => { throw new Error('Timed out waiting for default agent'); })
460
]);
461
}
462
463
/**
464
* Information about a pending confirmation in a chat response.
465
*/
466
export type IChatPendingConfirmationInfo =
467
| { type: 'confirmation'; kind: 'toolInvocation'; toolId: string }
468
| { type: 'confirmation'; kind: 'toolPostApproval'; toolId: string }
469
| { type: 'confirmation'; kind: 'confirmation'; title: string; data: unknown }
470
| { type: 'confirmation'; kind: 'questionCarousel'; questions: unknown[] }
471
| { type: 'confirmation'; kind: 'elicitation'; title: string };
472
473
/**
474
* Extracts detailed information about the pending confirmation from a chat response.
475
* Returns undefined if there is no pending confirmation.
476
*/
477
function getPendingConfirmationInfo(response: IChatResponseModel): IChatPendingConfirmationInfo | undefined {
478
for (const part of response.response.value) {
479
if (part.kind === 'toolInvocation') {
480
const state = part.state.get();
481
if (state.type === IChatToolInvocation.StateKind.WaitingForConfirmation) {
482
return {
483
type: 'confirmation',
484
kind: 'toolInvocation',
485
toolId: part.toolId,
486
};
487
}
488
if (state.type === IChatToolInvocation.StateKind.WaitingForPostApproval) {
489
return {
490
type: 'confirmation',
491
kind: 'toolPostApproval',
492
toolId: part.toolId,
493
};
494
}
495
}
496
if (part.kind === 'confirmation' && !part.isUsed) {
497
return {
498
type: 'confirmation',
499
kind: 'confirmation',
500
title: part.title,
501
data: part.data,
502
};
503
}
504
if (part.kind === 'questionCarousel' && !part.isUsed) {
505
return {
506
type: 'confirmation',
507
kind: 'questionCarousel',
508
questions: part.questions,
509
};
510
}
511
if (part.kind === 'elicitation2' && part.state.get() === ElicitationState.Pending) {
512
const title = part.title;
513
return {
514
type: 'confirmation',
515
kind: 'elicitation',
516
title: typeof title === 'string' ? title : title.value,
517
};
518
}
519
}
520
return undefined;
521
}
522
523
class PrimaryOpenChatGlobalAction extends OpenChatGlobalAction {
524
constructor() {
525
super({
526
id: CHAT_OPEN_ACTION_ID,
527
title: localize2('openChat', "Open Chat"),
528
keybinding: {
529
weight: KeybindingWeight.WorkbenchContrib,
530
primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KeyI,
531
mac: {
532
primary: KeyMod.CtrlCmd | KeyMod.WinCtrl | KeyCode.KeyI
533
}
534
},
535
menu: [{
536
id: MenuId.ChatTitleBarMenu,
537
group: 'a_open',
538
order: 1
539
}]
540
});
541
}
542
}
543
544
export function getOpenChatActionIdForMode(mode: IChatMode): string {
545
return `workbench.action.chat.open${mode.name.get()}`;
546
}
547
548
export abstract class ModeOpenChatGlobalAction extends OpenChatGlobalAction {
549
constructor(mode: IChatMode, keybinding?: ICommandPaletteOptions['keybinding']) {
550
super({
551
id: getOpenChatActionIdForMode(mode),
552
title: localize2('openChatMode', "Open Chat ({0})", mode.label.get()),
553
keybinding
554
}, mode);
555
}
556
}
557
558
export function registerChatActions() {
559
registerAction2(PrimaryOpenChatGlobalAction);
560
registerAction2(class extends ModeOpenChatGlobalAction {
561
constructor() { super(ChatMode.Ask); }
562
});
563
registerAction2(class extends ModeOpenChatGlobalAction {
564
constructor() {
565
super(ChatMode.Agent, {
566
when: ContextKeyExpr.has(`config.${ChatConfiguration.AgentEnabled}`),
567
weight: KeybindingWeight.WorkbenchContrib,
568
primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyI,
569
linux: {
570
primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyMod.Shift | KeyCode.KeyI
571
}
572
},);
573
}
574
});
575
registerAction2(class extends ModeOpenChatGlobalAction {
576
constructor() { super(ChatMode.Edit); }
577
});
578
579
registerAction2(class ToggleChatAction extends Action2 {
580
constructor() {
581
super({
582
id: TOGGLE_CHAT_ACTION_ID,
583
title: localize2('toggleChat', "Toggle Chat"),
584
category: CHAT_CATEGORY
585
});
586
}
587
588
async run(accessor: ServicesAccessor) {
589
const layoutService = accessor.get(IWorkbenchLayoutService);
590
const viewsService = accessor.get(IViewsService);
591
const viewDescriptorService = accessor.get(IViewDescriptorService);
592
const widgetService = accessor.get(IChatWidgetService);
593
const configurationService = accessor.get(IConfigurationService);
594
595
const chatLocation = viewDescriptorService.getViewLocationById(ChatViewId);
596
const chatVisible = viewsService.isViewVisible(ChatViewId);
597
const clickBehavior = configurationService.getValue<AgentsControlClickBehavior>(ChatConfiguration.AgentsControlClickBehavior);
598
switch (clickBehavior) {
599
case AgentsControlClickBehavior.Focus:
600
if (chatLocation === ViewContainerLocation.AuxiliaryBar) {
601
layoutService.setAuxiliaryBarMaximized(true);
602
} else {
603
this.updatePartVisibility(layoutService, chatLocation, true);
604
}
605
(await widgetService.revealWidget())?.focusInput();
606
break;
607
case AgentsControlClickBehavior.Cycle:
608
if (chatVisible) {
609
if (
610
chatLocation === ViewContainerLocation.AuxiliaryBar &&
611
!layoutService.isAuxiliaryBarMaximized()
612
) {
613
layoutService.setAuxiliaryBarMaximized(true);
614
(await widgetService.revealWidget())?.focusInput();
615
} else {
616
this.updatePartVisibility(layoutService, chatLocation, false);
617
}
618
} else {
619
this.updatePartVisibility(layoutService, chatLocation, true);
620
(await widgetService.revealWidget())?.focusInput();
621
}
622
break;
623
default:
624
if (chatVisible) {
625
this.updatePartVisibility(layoutService, chatLocation, false);
626
} else {
627
this.updatePartVisibility(layoutService, chatLocation, true);
628
(await widgetService.revealWidget())?.focusInput();
629
}
630
break;
631
}
632
}
633
634
private updatePartVisibility(layoutService: IWorkbenchLayoutService, location: ViewContainerLocation | null, visible: boolean): void {
635
let part: Parts.PANEL_PART | Parts.SIDEBAR_PART | Parts.AUXILIARYBAR_PART | undefined;
636
switch (location) {
637
case ViewContainerLocation.Panel:
638
part = Parts.PANEL_PART;
639
break;
640
case ViewContainerLocation.Sidebar:
641
part = Parts.SIDEBAR_PART;
642
break;
643
case ViewContainerLocation.AuxiliaryBar:
644
part = Parts.AUXILIARYBAR_PART;
645
break;
646
}
647
648
if (part) {
649
layoutService.setPartHidden(!visible, part);
650
}
651
}
652
});
653
654
655
registerAction2(class NewChatEditorAction extends Action2 {
656
constructor() {
657
super({
658
id: ACTION_ID_OPEN_CHAT,
659
title: localize2('interactiveSession.open', "New Chat Editor"),
660
icon: Codicon.plus,
661
f1: true,
662
category: CHAT_CATEGORY,
663
precondition: ChatContextKeys.enabled,
664
keybinding: {
665
weight: KeybindingWeight.WorkbenchContrib,
666
primary: KeyMod.CtrlCmd | KeyCode.KeyN,
667
when: ContextKeyExpr.and(ChatContextKeys.inChatSession, ChatContextKeys.inChatEditor)
668
},
669
menu: [{
670
id: MenuId.ChatTitleBarMenu,
671
group: 'b_new',
672
order: 0
673
}, {
674
id: MenuId.ChatNewMenu,
675
group: '2_new',
676
order: 2
677
}, {
678
id: MenuId.EditorTitle,
679
group: 'navigation',
680
when: ActiveEditorContext.isEqualTo(ChatEditorInput.EditorID),
681
order: 1
682
}],
683
});
684
}
685
686
async run(accessor: ServicesAccessor) {
687
const widgetService = accessor.get(IChatWidgetService);
688
await widgetService.openSession(LocalChatSessionUri.getNewSessionUri(), ACTIVE_GROUP, { pinned: true } satisfies IChatEditorOptions);
689
}
690
});
691
692
registerAction2(class NewChatWindowAction extends Action2 {
693
constructor() {
694
super({
695
id: `workbench.action.newChatWindow`,
696
title: localize2('interactiveSession.newChatWindow', "New Chat Window"),
697
f1: true,
698
category: CHAT_CATEGORY,
699
precondition: ChatContextKeys.enabled,
700
menu: [{
701
id: MenuId.ChatTitleBarMenu,
702
group: 'b_new',
703
order: 1
704
}, {
705
id: MenuId.ChatNewMenu,
706
group: '2_new',
707
order: 3
708
}]
709
});
710
}
711
712
async run(accessor: ServicesAccessor) {
713
const widgetService = accessor.get(IChatWidgetService);
714
await widgetService.openSession(LocalChatSessionUri.getNewSessionUri(), AUX_WINDOW_GROUP, { pinned: true, auxiliary: { compact: true, bounds: { width: 640, height: 640 } } } satisfies IChatEditorOptions);
715
}
716
});
717
718
registerAction2(class ClearChatInputHistoryAction extends Action2 {
719
constructor() {
720
super({
721
id: 'workbench.action.chat.clearInputHistory',
722
title: localize2('interactiveSession.clearHistory.label', "Clear Input History"),
723
precondition: ChatContextKeys.enabled,
724
category: CHAT_CATEGORY,
725
f1: true,
726
});
727
}
728
async run(accessor: ServicesAccessor, ...args: unknown[]) {
729
const historyService = accessor.get(IChatWidgetHistoryService);
730
historyService.clearHistory();
731
}
732
});
733
734
registerAction2(class FocusChatAction extends EditorAction2 {
735
constructor() {
736
super({
737
id: 'chat.action.focus',
738
title: localize2('actions.interactiveSession.focus', 'Focus Chat List'),
739
precondition: ContextKeyExpr.and(ChatContextKeys.inChatInput),
740
category: CHAT_CATEGORY,
741
keybinding: [
742
// 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
743
{
744
when: ContextKeyExpr.and(ChatContextKeys.inputCursorAtTop, ChatContextKeys.inQuickChat.negate()),
745
primary: KeyMod.CtrlCmd | KeyCode.UpArrow,
746
weight: KeybindingWeight.EditorContrib,
747
},
748
// On win/linux, ctrl+up can always focus the chat list
749
{
750
when: ContextKeyExpr.and(ContextKeyExpr.or(IsWindowsContext, IsLinuxContext), ChatContextKeys.inQuickChat.negate()),
751
primary: KeyMod.CtrlCmd | KeyCode.UpArrow,
752
weight: KeybindingWeight.EditorContrib,
753
},
754
{
755
when: ContextKeyExpr.and(ChatContextKeys.inChatSession, ChatContextKeys.inQuickChat),
756
primary: KeyMod.CtrlCmd | KeyCode.DownArrow,
757
weight: KeybindingWeight.WorkbenchContrib,
758
}
759
]
760
});
761
}
762
763
runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor): void | Promise<void> {
764
const editorUri = editor.getModel()?.uri;
765
if (editorUri) {
766
const widgetService = accessor.get(IChatWidgetService);
767
widgetService.getWidgetByInputUri(editorUri)?.focusResponseItem();
768
}
769
}
770
});
771
772
registerAction2(class FocusMostRecentlyFocusedChatAction extends EditorAction2 {
773
constructor() {
774
super({
775
id: 'workbench.chat.action.focusLastFocused',
776
title: localize2('actions.interactiveSession.focusLastFocused', 'Focus Last Focused Chat List Item'),
777
precondition: ContextKeyExpr.and(ChatContextKeys.inChatInput),
778
category: CHAT_CATEGORY,
779
keybinding: [
780
// 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
781
{
782
when: ContextKeyExpr.and(ChatContextKeys.inputCursorAtTop, ChatContextKeys.inQuickChat.negate()),
783
primary: KeyMod.CtrlCmd | KeyCode.UpArrow | KeyMod.Shift,
784
weight: KeybindingWeight.EditorContrib + 1,
785
},
786
// On win/linux, ctrl+up can always focus the chat list
787
{
788
when: ContextKeyExpr.and(ContextKeyExpr.or(IsWindowsContext, IsLinuxContext), ChatContextKeys.inQuickChat.negate()),
789
primary: KeyMod.CtrlCmd | KeyCode.UpArrow | KeyMod.Shift,
790
weight: KeybindingWeight.EditorContrib + 1,
791
},
792
{
793
when: ContextKeyExpr.and(ChatContextKeys.inChatSession, ChatContextKeys.inQuickChat),
794
primary: KeyMod.CtrlCmd | KeyCode.DownArrow | KeyMod.Shift,
795
weight: KeybindingWeight.WorkbenchContrib + 1,
796
}
797
]
798
});
799
}
800
801
runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor): void | Promise<void> {
802
const editorUri = editor.getModel()?.uri;
803
if (editorUri) {
804
const widgetService = accessor.get(IChatWidgetService);
805
widgetService.getWidgetByInputUri(editorUri)?.focusResponseItem(true);
806
}
807
}
808
});
809
810
registerAction2(class FocusChatInputAction extends Action2 {
811
constructor() {
812
super({
813
id: 'workbench.action.chat.focusInput',
814
title: localize2('interactiveSession.focusInput.label', "Focus Chat Input"),
815
f1: false,
816
keybinding: [
817
{
818
primary: KeyMod.CtrlCmd | KeyCode.DownArrow,
819
weight: KeybindingWeight.WorkbenchContrib,
820
when: ContextKeyExpr.and(ChatContextKeys.inChatSession, ChatContextKeys.inChatInput.negate(), ChatContextKeys.inQuickChat.negate()),
821
},
822
{
823
when: ContextKeyExpr.and(ChatContextKeys.inChatSession, ChatContextKeys.inChatInput.negate(), ChatContextKeys.inQuickChat),
824
primary: KeyMod.CtrlCmd | KeyCode.UpArrow,
825
weight: KeybindingWeight.WorkbenchContrib,
826
}
827
]
828
});
829
}
830
run(accessor: ServicesAccessor, ...args: unknown[]) {
831
const widgetService = accessor.get(IChatWidgetService);
832
widgetService.lastFocusedWidget?.focusInput();
833
}
834
});
835
836
registerAction2(class FocusTodosViewAction extends Action2 {
837
static readonly ID = 'workbench.action.chat.focusTodosView';
838
839
constructor() {
840
super({
841
id: FocusTodosViewAction.ID,
842
title: localize2('interactiveSession.focusTodosView.label', "Agent TODOs: Toggle Focus Between TODOs and Input"),
843
category: CHAT_CATEGORY,
844
f1: true,
845
precondition: ContextKeyExpr.and(ChatContextKeys.inChatSession, ChatContextKeys.chatModeKind.isEqualTo(ChatModeKind.Agent)),
846
keybinding: [{
847
weight: KeybindingWeight.WorkbenchContrib,
848
primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyT,
849
when: ContextKeyExpr.or(
850
ContextKeyExpr.and(ChatContextKeys.inChatSession, ChatContextKeys.chatModeKind.isEqualTo(ChatModeKind.Agent)),
851
ChatContextKeys.inChatTodoList
852
),
853
}]
854
});
855
}
856
857
run(accessor: ServicesAccessor): void {
858
const widgetService = accessor.get(IChatWidgetService);
859
const widget = widgetService.lastFocusedWidget;
860
861
if (!widget || !widget.toggleTodosViewFocus()) {
862
alert(localize('chat.todoList.focusUnavailable', "No agent todos to focus right now."));
863
}
864
}
865
});
866
867
registerAction2(class FocusQuestionCarouselAction extends Action2 {
868
static readonly ID = 'workbench.action.chat.focusQuestionCarousel';
869
870
constructor() {
871
super({
872
id: FocusQuestionCarouselAction.ID,
873
title: localize2('interactiveSession.focusQuestionCarousel.label', "Chat: Toggle Focus Between Question and Input"),
874
category: CHAT_CATEGORY,
875
f1: true,
876
precondition: ChatContextKeys.inChatSession,
877
keybinding: [{
878
weight: KeybindingWeight.WorkbenchContrib,
879
primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyH,
880
when: ChatContextKeys.inChatSession,
881
}]
882
});
883
}
884
885
run(accessor: ServicesAccessor): void {
886
const widgetService = accessor.get(IChatWidgetService);
887
const widget = widgetService.lastFocusedWidget;
888
889
if (!widget || !widget.toggleQuestionCarouselFocus()) {
890
alert(localize('chat.questionCarousel.focusUnavailable', "No chat question to focus right now."));
891
}
892
}
893
});
894
895
registerAction2(class FocusTipAction extends Action2 {
896
static readonly ID = 'workbench.action.chat.focusTip';
897
898
constructor() {
899
super({
900
id: FocusTipAction.ID,
901
title: localize2('interactiveSession.focusTip.label', "Chat: Toggle Focus Between Tip and Input"),
902
category: CHAT_CATEGORY,
903
f1: true,
904
precondition: ChatContextKeys.inChatSession,
905
keybinding: [{
906
weight: KeybindingWeight.WorkbenchContrib,
907
primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.Slash,
908
when: ContextKeyExpr.or(
909
ChatContextKeys.inChatSession,
910
ChatContextKeys.inChatTip
911
),
912
}]
913
});
914
}
915
916
run(accessor: ServicesAccessor): void {
917
const widgetService = accessor.get(IChatWidgetService);
918
const widget = widgetService.lastFocusedWidget;
919
920
if (!widget || !widget.toggleTipFocus()) {
921
alert(localize('chat.tip.focusUnavailable', "No chat tip."));
922
}
923
}
924
});
925
926
registerAction2(class ShowContextUsageAction extends Action2 {
927
constructor() {
928
super({
929
id: 'workbench.action.chat.showContextUsage',
930
title: localize2('interactiveSession.showContextUsage.label', "Show Context Window Usage"),
931
category: CHAT_CATEGORY,
932
f1: true,
933
precondition: ChatContextKeys.enabled,
934
});
935
}
936
937
async run(accessor: ServicesAccessor): Promise<void> {
938
const widgetService = accessor.get(IChatWidgetService);
939
const widget = widgetService.lastFocusedWidget ?? (await widgetService.revealWidget());
940
widget?.input.showContextUsageDetails();
941
}
942
});
943
944
const nonEnterpriseCopilotUsers = ContextKeyExpr.and(ChatContextKeys.enabled, ContextKeyExpr.notEquals(`config.${defaultChat.completionsAdvancedSetting}.authProvider`, defaultChat.provider.enterprise.id));
945
registerAction2(class extends Action2 {
946
constructor() {
947
super({
948
id: 'workbench.action.chat.manageSettings',
949
title: localize2('manageChat', "Manage Chat"),
950
category: CHAT_CATEGORY,
951
f1: true,
952
precondition: ContextKeyExpr.and(
953
ContextKeyExpr.or(
954
ChatContextKeys.Entitlement.planFree,
955
ChatContextKeys.Entitlement.planPro,
956
ChatContextKeys.Entitlement.planProPlus
957
),
958
nonEnterpriseCopilotUsers
959
),
960
menu: {
961
id: MenuId.ChatTitleBarMenu,
962
group: 'y_manage',
963
order: 1,
964
when: nonEnterpriseCopilotUsers
965
}
966
});
967
}
968
969
override async run(accessor: ServicesAccessor): Promise<void> {
970
const openerService = accessor.get(IOpenerService);
971
openerService.open(URI.parse(defaultChat.manageSettingsUrl));
972
}
973
});
974
975
registerAction2(class ShowExtensionsUsingCopilot extends Action2 {
976
977
constructor() {
978
super({
979
id: 'workbench.action.chat.showExtensionsUsingCopilot',
980
title: localize2('showCopilotUsageExtensions', "Show Extensions using Copilot"),
981
f1: true,
982
category: EXTENSIONS_CATEGORY,
983
precondition: ChatContextKeys.enabled
984
});
985
}
986
987
override async run(accessor: ServicesAccessor): Promise<void> {
988
const extensionsWorkbenchService = accessor.get(IExtensionsWorkbenchService);
989
extensionsWorkbenchService.openSearch(`@contribute:${CopilotUsageExtensionFeatureId}`);
990
}
991
});
992
993
registerAction2(class ConfigureCopilotCompletions extends Action2 {
994
995
constructor() {
996
super({
997
id: 'workbench.action.chat.configureCodeCompletions',
998
title: localize2('configureCompletions', "Configure Inline Suggestions..."),
999
precondition: ContextKeyExpr.and(
1000
ChatContextKeys.Setup.installed,
1001
ChatContextKeys.Setup.disabled.negate(),
1002
ChatContextKeys.Setup.untrusted.negate()
1003
),
1004
menu: {
1005
id: MenuId.ChatTitleBarMenu,
1006
group: 'f_completions',
1007
order: 10,
1008
}
1009
});
1010
}
1011
1012
override async run(accessor: ServicesAccessor): Promise<void> {
1013
const commandService = accessor.get(ICommandService);
1014
commandService.executeCommand(defaultChat.completionsMenuCommand);
1015
}
1016
});
1017
1018
registerAction2(class ShowQuotaExceededDialogAction extends Action2 {
1019
1020
constructor() {
1021
super({
1022
id: OPEN_CHAT_QUOTA_EXCEEDED_DIALOG,
1023
title: localize('upgradeChat', "Upgrade GitHub Copilot Plan")
1024
});
1025
}
1026
1027
override async run(accessor: ServicesAccessor) {
1028
const chatEntitlementService = accessor.get(IChatEntitlementService);
1029
const commandService = accessor.get(ICommandService);
1030
const dialogService = accessor.get(IDialogService);
1031
const telemetryService = accessor.get(ITelemetryService);
1032
1033
let message: string;
1034
const chatQuotaExceeded = chatEntitlementService.quotas.chat?.percentRemaining === 0;
1035
const completionsQuotaExceeded = chatEntitlementService.quotas.completions?.percentRemaining === 0;
1036
if (chatQuotaExceeded && !completionsQuotaExceeded) {
1037
message = localize('chatQuotaExceeded', "You've reached your monthly chat messages quota. You still have free inline suggestions available.");
1038
} else if (completionsQuotaExceeded && !chatQuotaExceeded) {
1039
message = localize('completionsQuotaExceeded', "You've reached your monthly inline suggestions quota. You still have free chat messages available.");
1040
} else {
1041
message = localize('chatAndCompletionsQuotaExceeded', "You've reached your monthly chat messages and inline suggestions quota.");
1042
}
1043
1044
if (chatEntitlementService.quotas.resetDate) {
1045
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' });
1046
const quotaResetDate = new Date(chatEntitlementService.quotas.resetDate);
1047
message = [message, localize('quotaResetDate', "The allowance will reset on {0}.", dateFormatter.value.format(quotaResetDate))].join(' ');
1048
}
1049
1050
const free = chatEntitlementService.entitlement === ChatEntitlement.Free;
1051
const upgradeToPro = free ? localize('upgradeToPro', "Upgrade to GitHub Copilot Pro (your first 30 days are free) for:\n- Unlimited inline suggestions\n- Unlimited chat messages\n- Access to premium models") : undefined;
1052
1053
await dialogService.prompt({
1054
type: 'none',
1055
message: localize('copilotQuotaReached', "GitHub Copilot Quota Reached"),
1056
cancelButton: {
1057
label: localize('dismiss', "Dismiss"),
1058
run: () => { /* noop */ }
1059
},
1060
buttons: [
1061
{
1062
label: free ? localize('upgradePro', "Upgrade to GitHub Copilot Pro") : localize('upgradePlan', "Upgrade GitHub Copilot Plan"),
1063
run: () => {
1064
const commandId = 'workbench.action.chat.upgradePlan';
1065
telemetryService.publicLog2<WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification>('workbenchActionExecuted', { id: commandId, from: 'chat-dialog' });
1066
commandService.executeCommand(commandId);
1067
}
1068
},
1069
],
1070
custom: {
1071
icon: Codicon.copilotWarningLarge,
1072
markdownDetails: coalesce([
1073
{ markdown: new MarkdownString(message, true) },
1074
upgradeToPro ? { markdown: new MarkdownString(upgradeToPro, true) } : undefined
1075
])
1076
}
1077
});
1078
}
1079
});
1080
1081
registerAction2(class ResetTrustedToolsAction extends Action2 {
1082
constructor() {
1083
super({
1084
id: 'workbench.action.chat.resetTrustedTools',
1085
title: localize2('resetTrustedTools', "Reset Tool Confirmations"),
1086
category: CHAT_CATEGORY,
1087
f1: true,
1088
precondition: ChatContextKeys.enabled
1089
});
1090
}
1091
override run(accessor: ServicesAccessor): void {
1092
accessor.get(ILanguageModelToolsConfirmationService).resetToolAutoConfirmation();
1093
accessor.get(INotificationService).info(localize('resetTrustedToolsSuccess', "Tool confirmation preferences have been reset."));
1094
}
1095
});
1096
1097
registerAction2(class UpdateInstructionsAction extends Action2 {
1098
constructor() {
1099
super({
1100
id: 'workbench.action.chat.generateInstructions',
1101
title: localize2('generateInstructions', "Generate Workspace Instructions File"),
1102
shortTitle: localize2('generateInstructions.short', "Generate Chat Instructions"),
1103
category: CHAT_CATEGORY,
1104
icon: Codicon.sparkle,
1105
f1: true,
1106
precondition: ChatContextKeys.enabled
1107
});
1108
}
1109
1110
async run(accessor: ServicesAccessor): Promise<void> {
1111
const commandService = accessor.get(ICommandService);
1112
1113
// Use chat command to open and send the query
1114
const query = `Analyze this codebase to generate or update \`.github/copilot-instructions.md\` for guiding AI coding agents.
1115
1116
Focus on discovering the essential knowledge that would help an AI agents be immediately productive in this codebase. Consider aspects like:
1117
- The "big picture" architecture that requires reading multiple files to understand - major components, service boundaries, data flows, and the "why" behind structural decisions
1118
- Critical developer workflows (builds, tests, debugging) especially commands that aren't obvious from file inspection alone
1119
- Project-specific conventions and patterns that differ from common practices
1120
- Integration points, external dependencies, and cross-component communication patterns
1121
1122
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).
1123
1124
Guidelines (read more at https://aka.ms/vscode-instructions-docs):
1125
- If \`.github/copilot-instructions.md\` exists, merge intelligently - preserve valuable content while updating outdated sections
1126
- Write concise, actionable instructions (~20-50 lines) using markdown structure
1127
- Include specific examples from the codebase when describing patterns
1128
- Avoid generic advice ("write tests", "handle errors") - focus on THIS project's specific approaches
1129
- Document only discoverable patterns, not aspirational practices
1130
- Reference key files/directories that exemplify important patterns
1131
1132
Update \`.github/copilot-instructions.md\` for the user, then ask for feedback on any unclear or incomplete sections to iterate.`;
1133
1134
await commandService.executeCommand('workbench.action.chat.open', {
1135
mode: 'agent',
1136
query: query,
1137
});
1138
}
1139
});
1140
1141
registerAction2(class OpenChatFeatureSettingsAction extends Action2 {
1142
constructor() {
1143
super({
1144
id: 'workbench.action.chat.openFeatureSettings',
1145
title: localize2('openChatFeatureSettings', "Chat Settings"),
1146
shortTitle: localize('openChatFeatureSettings.short', "Chat Settings"),
1147
category: CHAT_CATEGORY,
1148
f1: true,
1149
precondition: ChatContextKeys.enabled,
1150
menu: [{
1151
id: CHAT_CONFIG_MENU_ID,
1152
when: ContextKeyExpr.and(ChatContextKeys.enabled, ContextKeyExpr.equals('view', ChatViewId)),
1153
order: 15,
1154
group: '3_configure'
1155
},
1156
{
1157
id: MenuId.ChatWelcomeContext,
1158
group: '2_settings',
1159
order: 1
1160
}]
1161
});
1162
}
1163
1164
override async run(accessor: ServicesAccessor): Promise<void> {
1165
const preferencesService = accessor.get(IPreferencesService);
1166
preferencesService.openSettings({ query: '@feature:chat ' });
1167
}
1168
});
1169
1170
MenuRegistry.appendMenuItem(MenuId.ViewTitle, {
1171
submenu: CHAT_CONFIG_MENU_ID,
1172
title: localize2('config.label', "Configure Chat"),
1173
group: 'navigation',
1174
when: ContextKeyExpr.equals('view', ChatViewId),
1175
icon: Codicon.gear,
1176
order: 6
1177
});
1178
}
1179
1180
export function stringifyItem(item: IChatRequestViewModel | IChatResponseViewModel, includeName = true): string {
1181
if (isRequestVM(item)) {
1182
return (includeName ? `${item.username}: ` : '') + item.messageText;
1183
} else {
1184
return (includeName ? `${item.username}: ` : '') + item.response.toString();
1185
}
1186
}
1187
1188
export interface IToolFilteringOptions {
1189
allTools: IToolData[];
1190
allToolSets: IToolSet[];
1191
toolsInclude?: string[];
1192
toolsExclude?: string[];
1193
}
1194
1195
export interface IToolFilteringResult {
1196
enablementMap: Map<IToolData | IToolSet, boolean>;
1197
unknownIdentifiers: string[];
1198
}
1199
1200
/**
1201
* Computes the tool enablement map based on include/exclude filters.
1202
*
1203
* Resolution algorithm:
1204
* 1. If `toolsInclude` is specified, start with only those tools/toolsets enabled
1205
* 2. If `toolsExclude` is specified, remove those tools/toolsets
1206
* 3. Explicit tool references in `toolsInclude` override toolset exclusions
1207
* 4. Explicit tool exclusions always win
1208
* 5. Toolset enablement is calculated based on whether all member tools are enabled
1209
*
1210
* @throws Error if filtering results in zero enabled tools
1211
*/
1212
export function computeToolEnablementMap(options: IToolFilteringOptions): IToolFilteringResult {
1213
const { allTools, allToolSets, toolsInclude, toolsExclude } = options;
1214
1215
const enablementMap = new Map<IToolData | IToolSet, boolean>();
1216
const matchedIdentifiers = new Set<string>();
1217
1218
// Helper to check if a tool matches any identifier (by id or toolReferenceName)
1219
const toolMatches = (tool: IToolData, identifiers: Set<string>): boolean => {
1220
if (identifiers.has(tool.id)) {
1221
matchedIdentifiers.add(tool.id);
1222
return true;
1223
}
1224
if (tool.toolReferenceName && identifiers.has(tool.toolReferenceName)) {
1225
matchedIdentifiers.add(tool.toolReferenceName);
1226
return true;
1227
}
1228
return false;
1229
};
1230
1231
// Helper to check if a toolset matches any identifier (by id or referenceName)
1232
const toolSetMatches = (toolSet: IToolSet, identifiers: Set<string>): boolean => {
1233
if (identifiers.has(toolSet.id)) {
1234
matchedIdentifiers.add(toolSet.id);
1235
return true;
1236
}
1237
if (identifiers.has(toolSet.referenceName)) {
1238
matchedIdentifiers.add(toolSet.referenceName);
1239
return true;
1240
}
1241
return false;
1242
};
1243
1244
// Track which tools are explicitly referenced in toolsInclude
1245
const explicitlyIncludedTools = new Set<IToolData>();
1246
1247
// Step 1: Build initial set based on toolsInclude
1248
if (toolsInclude) {
1249
const includeSet = new Set(toolsInclude);
1250
1251
// First, process toolsets - if a toolset matches, enable all its tools
1252
for (const toolSet of allToolSets) {
1253
if (toolSetMatches(toolSet, includeSet)) {
1254
for (const tool of toolSet.getTools()) {
1255
enablementMap.set(tool, true);
1256
}
1257
}
1258
}
1259
1260
// Then process individual tools
1261
for (const tool of allTools) {
1262
if (toolMatches(tool, includeSet)) {
1263
enablementMap.set(tool, true);
1264
explicitlyIncludedTools.add(tool);
1265
} else if (!enablementMap.has(tool)) {
1266
enablementMap.set(tool, false);
1267
}
1268
}
1269
// Also process tools from toolsets that may not be in allTools
1270
for (const toolSet of allToolSets) {
1271
for (const tool of toolSet.getTools()) {
1272
if (toolMatches(tool, includeSet)) {
1273
enablementMap.set(tool, true);
1274
explicitlyIncludedTools.add(tool);
1275
} else if (!enablementMap.has(tool)) {
1276
enablementMap.set(tool, false);
1277
}
1278
}
1279
}
1280
} else {
1281
// No toolsInclude specified - start with all tools enabled
1282
for (const tool of allTools) {
1283
enablementMap.set(tool, true);
1284
}
1285
for (const toolSet of allToolSets) {
1286
for (const tool of toolSet.getTools()) {
1287
enablementMap.set(tool, true);
1288
}
1289
}
1290
}
1291
1292
// Step 2: Remove tools matching toolsExclude
1293
if (toolsExclude) {
1294
const excludeSet = new Set(toolsExclude);
1295
1296
// First, process toolsets - if a toolset matches, disable all its tools
1297
// (unless explicitly included as individual tools)
1298
for (const toolSet of allToolSets) {
1299
if (toolSetMatches(toolSet, excludeSet)) {
1300
for (const tool of toolSet.getTools()) {
1301
// Explicit tool reference overrides toolset exclusion
1302
if (!explicitlyIncludedTools.has(tool)) {
1303
enablementMap.set(tool, false);
1304
}
1305
}
1306
}
1307
}
1308
1309
// Then process individual tools - explicit exclusion always wins
1310
for (const tool of allTools) {
1311
if (toolMatches(tool, excludeSet)) {
1312
enablementMap.set(tool, false);
1313
}
1314
}
1315
for (const toolSet of allToolSets) {
1316
for (const tool of toolSet.getTools()) {
1317
if (toolMatches(tool, excludeSet)) {
1318
enablementMap.set(tool, false);
1319
}
1320
}
1321
}
1322
}
1323
1324
// Collect unknown identifiers
1325
const allIdentifiers = new Set([...(toolsInclude ?? []), ...(toolsExclude ?? [])]);
1326
const unknownIdentifiers: string[] = [];
1327
for (const identifier of allIdentifiers) {
1328
if (!matchedIdentifiers.has(identifier)) {
1329
unknownIdentifiers.push(identifier);
1330
}
1331
}
1332
1333
// Validate at least one tool is enabled
1334
const enabledToolCount = Array.from(enablementMap.entries()).filter(([item, enabled]) => enabled && !isToolSet(item)).length;
1335
if (enabledToolCount === 0) {
1336
throw new Error('Tool filtering resulted in zero enabled tools. At least one tool must be enabled.');
1337
}
1338
1339
// Calculate toolset enablement based on whether all member tools are enabled
1340
for (const toolSet of allToolSets) {
1341
const toolSetTools = Array.from(toolSet.getTools());
1342
const allToolsEnabled = toolSetTools.length > 0 && toolSetTools.every(t => enablementMap.get(t) === true);
1343
enablementMap.set(toolSet, allToolsEnabled);
1344
}
1345
1346
return { enablementMap, unknownIdentifiers };
1347
}
1348
1349
1350
/**
1351
* Returns whether we can continue clearing/switching chat sessions, false to cancel.
1352
*/
1353
export async function handleCurrentEditingSession(model: IChatModel, phrase: string | undefined, dialogService: IDialogService): Promise<boolean> {
1354
return showClearEditingSessionConfirmation(model, dialogService, { messageOverride: phrase });
1355
}
1356
1357
/**
1358
* Returns whether we can switch the agent, based on whether the user had to agree to clear the session, false to cancel.
1359
*/
1360
export async function handleModeSwitch(
1361
accessor: ServicesAccessor,
1362
fromMode: ChatModeKind,
1363
toMode: ChatModeKind,
1364
requestCount: number,
1365
model: IChatModel | undefined,
1366
): Promise<false | { needToClearSession: boolean }> {
1367
if (!model?.editingSession || fromMode === toMode) {
1368
return { needToClearSession: false };
1369
}
1370
1371
const configurationService = accessor.get(IConfigurationService);
1372
const dialogService = accessor.get(IDialogService);
1373
const needToClearEdits = (!configurationService.getValue(ChatConfiguration.Edits2Enabled) && (fromMode === ChatModeKind.Edit || toMode === ChatModeKind.Edit)) && requestCount > 0;
1374
if (needToClearEdits) {
1375
// If not using edits2 and switching into or out of edit mode, ask to discard the session
1376
const phrase = localize('switchMode.confirmPhrase', "Switching agents will end your current edit session.");
1377
1378
const currentEdits = model.editingSession.entries.get();
1379
const undecidedEdits = currentEdits.filter((edit) => edit.state.get() === ModifiedFileEntryState.Modified);
1380
if (undecidedEdits.length > 0) {
1381
if (!await handleCurrentEditingSession(model, phrase, dialogService)) {
1382
return false;
1383
}
1384
1385
return { needToClearSession: true };
1386
} else {
1387
const confirmation = await dialogService.confirm({
1388
title: localize('agent.newSession', "Start new session?"),
1389
message: localize('agent.newSessionMessage', "Changing the agent will end your current edit session. Would you like to change the agent?"),
1390
primaryButton: localize('agent.newSession.confirm', "Yes"),
1391
type: 'info'
1392
});
1393
if (!confirmation.confirmed) {
1394
return false;
1395
}
1396
1397
return { needToClearSession: true };
1398
}
1399
}
1400
1401
return { needToClearSession: false };
1402
}
1403
1404
export interface IClearEditingSessionConfirmationOptions {
1405
titleOverride?: string;
1406
messageOverride?: string;
1407
isArchiveAction?: boolean;
1408
}
1409
1410
1411
// --- Chat Submenus in various Components
1412
1413
MenuRegistry.appendMenuItem(MenuId.EditorContext, {
1414
submenu: MenuId.ChatTextEditorMenu,
1415
group: '1_chat',
1416
order: 5,
1417
title: localize('generateCode', "Generate Code"),
1418
when: ContextKeyExpr.and(
1419
ChatContextKeys.Setup.hidden.negate(),
1420
ChatContextKeys.Setup.disabled.negate()
1421
)
1422
});
1423
1424
// --- Chat Default Visibility
1425
1426
registerAction2(class ToggleDefaultVisibilityAction extends Action2 {
1427
constructor() {
1428
super({
1429
id: 'workbench.action.chat.toggleDefaultVisibility',
1430
title: localize2('chat.toggleDefaultVisibility.label', "Show View by Default"),
1431
toggled: ContextKeyExpr.equals('config.workbench.secondarySideBar.defaultVisibility', 'hidden').negate(),
1432
f1: false,
1433
menu: {
1434
id: MenuId.ViewTitle,
1435
when: ContextKeyExpr.and(
1436
ContextKeyExpr.equals('view', ChatViewId),
1437
ChatContextKeys.panelLocation.isEqualTo(ViewContainerLocation.AuxiliaryBar),
1438
),
1439
order: 0,
1440
group: '5_configure'
1441
},
1442
});
1443
}
1444
1445
async run(accessor: ServicesAccessor) {
1446
const configurationService = accessor.get(IConfigurationService);
1447
1448
const currentValue = configurationService.getValue<'hidden' | unknown>('workbench.secondarySideBar.defaultVisibility');
1449
configurationService.updateValue('workbench.secondarySideBar.defaultVisibility', currentValue !== 'hidden' ? 'hidden' : 'visible');
1450
}
1451
});
1452
1453
registerAction2(class EditToolApproval extends Action2 {
1454
constructor() {
1455
super({
1456
id: 'workbench.action.chat.editToolApproval',
1457
title: localize2('chat.editToolApproval.label', "Manage Tool Approval"),
1458
metadata: {
1459
description: localize2('chat.editToolApproval.description', "Edit/manage the tool approval and confirmation preferences for AI chat agents."),
1460
},
1461
precondition: ChatContextKeys.enabled,
1462
f1: true,
1463
category: CHAT_CATEGORY,
1464
});
1465
}
1466
1467
async run(accessor: ServicesAccessor, scope?: 'workspace' | 'profile' | 'session'): Promise<void> {
1468
const confirmationService = accessor.get(ILanguageModelToolsConfirmationService);
1469
const toolsService = accessor.get(ILanguageModelToolsService);
1470
confirmationService.manageConfirmationPreferences([...toolsService.getAllToolsIncludingDisabled()], scope ? { defaultScope: scope } : undefined);
1471
}
1472
});
1473
1474