Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/chat/browser/chatSlashCommands.ts
13401 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 { timeout } from '../../../../base/common/async.js';
7
import { MarkdownString, isMarkdownString } from '../../../../base/common/htmlContent.js';
8
import { Disposable, DisposableMap, DisposableStore } from '../../../../base/common/lifecycle.js';
9
import { URI } from '../../../../base/common/uri.js';
10
import * as nls from '../../../../nls.js';
11
import { ICommandService } from '../../../../platform/commands/common/commands.js';
12
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
13
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
14
import { ILogService } from '../../../../platform/log/common/log.js';
15
import { IChatAgentService } from '../common/participants/chatAgents.js';
16
import { ChatContextKeys } from '../common/actions/chatContextKeys.js';
17
import { IChatSlashCommandService } from '../common/participants/chatSlashCommands.js';
18
import { IChatService } from '../common/chatService/chatService.js';
19
import { IChatSessionsService, IChatSessionProviderOptionGroup, IChatSessionProviderOptionItem, SessionType } from '../common/chatSessionsService.js';
20
import { ChatAgentLocation, ChatConfiguration, ChatModeKind, ChatPermissionLevel } from '../common/constants.js';
21
import { ACTION_ID_NEW_CHAT } from './actions/chatActions.js';
22
import { ChatSubmitAction, OpenModePickerAction, OpenModelPickerAction } from './actions/chatExecuteActions.js';
23
import { ManagePluginsAction } from './actions/chatPluginActions.js';
24
import { ConfigureToolsAction } from './actions/chatToolActions.js';
25
import { IAgentSessionsService } from './agentSessions/agentSessionsService.js';
26
import { CONFIGURE_INSTRUCTIONS_ACTION_ID } from './promptSyntax/attachInstructionsAction.js';
27
import { showConfigureHooksQuickPick } from './promptSyntax/hookActions.js';
28
import { CONFIGURE_PROMPTS_ACTION_ID } from './promptSyntax/runPromptAction.js';
29
import { CONFIGURE_SKILLS_ACTION_ID } from './promptSyntax/skillActions.js';
30
import { IChatWidgetService } from './chat.js';
31
import { agentSlashCommandToMarkdown, agentToMarkdown } from './widget/chatContentParts/chatMarkdownDecorationsRenderer.js';
32
import { IWorkbenchEnvironmentService } from '../../../services/environment/common/environmentService.js';
33
import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js';
34
35
export class ChatSlashCommandsContribution extends Disposable {
36
37
static readonly ID = 'workbench.contrib.chatSlashCommands';
38
39
constructor(
40
@IChatSlashCommandService slashCommandService: IChatSlashCommandService,
41
@ICommandService commandService: ICommandService,
42
@IChatAgentService chatAgentService: IChatAgentService,
43
@IInstantiationService instantiationService: IInstantiationService,
44
@IAgentSessionsService agentSessionsService: IAgentSessionsService,
45
@IChatService chatService: IChatService,
46
@IConfigurationService configurationService: IConfigurationService,
47
@IChatWidgetService chatWidgetService: IChatWidgetService,
48
@IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService,
49
) {
50
super();
51
52
this._store.add(slashCommandService.registerSlashCommand({
53
command: 'clear',
54
detail: nls.localize('clear', "Start a new chat and archive the current one"),
55
sortText: 'z2_clear',
56
executeImmediately: true,
57
locations: [ChatAgentLocation.Chat]
58
}, async (_prompt, _progress, _history, _location, sessionResource) => {
59
agentSessionsService.getSession(sessionResource)?.setArchived(true);
60
commandService.executeCommand(ACTION_ID_NEW_CHAT);
61
}));
62
this._store.add(slashCommandService.registerSlashCommand({
63
command: 'hooks',
64
detail: nls.localize('hooks', "Configure hooks"),
65
sortText: 'z3_hooks',
66
executeImmediately: true,
67
silent: true,
68
locations: [ChatAgentLocation.Chat],
69
sessionTypes: [SessionType.Local],
70
}, async () => {
71
await instantiationService.invokeFunction(showConfigureHooksQuickPick);
72
}));
73
this._store.add(slashCommandService.registerSlashCommand({
74
command: 'models',
75
detail: nls.localize('models', "Open the model picker"),
76
sortText: 'z3_models',
77
executeImmediately: true,
78
silent: true,
79
locations: [ChatAgentLocation.Chat],
80
}, async () => {
81
await commandService.executeCommand(OpenModelPickerAction.ID);
82
}));
83
this._store.add(slashCommandService.registerSlashCommand({
84
command: 'tools',
85
detail: nls.localize('tools', "Configure tools"),
86
sortText: 'z3_tools',
87
executeImmediately: true,
88
silent: true,
89
locations: [ChatAgentLocation.Chat],
90
sessionTypes: [SessionType.Local],
91
}, async () => {
92
await commandService.executeCommand(ConfigureToolsAction.ID);
93
}));
94
this._store.add(slashCommandService.registerSlashCommand({
95
command: 'plugins',
96
detail: nls.localize('plugins', "Manage plugins"),
97
sortText: 'z3_plugins',
98
executeImmediately: true,
99
silent: true,
100
locations: [ChatAgentLocation.Chat],
101
sessionTypes: [SessionType.Local],
102
}, async () => {
103
await commandService.executeCommand(ManagePluginsAction.ID);
104
}));
105
if (!this.environmentService.isSessionsWindow) {
106
this._store.add(slashCommandService.registerSlashCommand({
107
command: 'debug',
108
detail: nls.localize('debug', "Show Chat Debug View"),
109
sortText: 'z3_debug',
110
executeImmediately: true,
111
silent: true,
112
locations: [ChatAgentLocation.Chat],
113
}, async () => {
114
await commandService.executeCommand('github.copilot.debug.showChatLogView');
115
}));
116
}
117
this._store.add(slashCommandService.registerSlashCommand({
118
command: 'agents',
119
detail: nls.localize('agents', "Configure custom agents"),
120
sortText: 'z3_agents',
121
executeImmediately: true,
122
silent: true,
123
locations: [ChatAgentLocation.Chat],
124
sessionTypes: [SessionType.Local],
125
}, async () => {
126
await commandService.executeCommand(OpenModePickerAction.ID);
127
}));
128
this._store.add(slashCommandService.registerSlashCommand({
129
command: 'skills',
130
detail: nls.localize('skills', "Configure skills"),
131
sortText: 'z3_skills',
132
executeImmediately: true,
133
silent: true,
134
locations: [ChatAgentLocation.Chat],
135
sessionTypes: [SessionType.Local],
136
}, async () => {
137
await commandService.executeCommand(CONFIGURE_SKILLS_ACTION_ID);
138
}));
139
this._store.add(slashCommandService.registerSlashCommand({
140
command: 'instructions',
141
detail: nls.localize('instructions', "Configure instructions"),
142
sortText: 'z3_instructions',
143
executeImmediately: true,
144
silent: true,
145
locations: [ChatAgentLocation.Chat],
146
sessionTypes: [SessionType.Local],
147
}, async () => {
148
await commandService.executeCommand(CONFIGURE_INSTRUCTIONS_ACTION_ID);
149
}));
150
this._store.add(slashCommandService.registerSlashCommand({
151
command: 'prompts',
152
detail: nls.localize('prompts', "Configure prompt files"),
153
sortText: 'z3_prompts',
154
executeImmediately: true,
155
silent: true,
156
locations: [ChatAgentLocation.Chat],
157
sessionTypes: [SessionType.Local],
158
}, async () => {
159
await commandService.executeCommand(CONFIGURE_PROMPTS_ACTION_ID);
160
}));
161
this._store.add(slashCommandService.registerSlashCommand({
162
command: 'fork',
163
detail: nls.localize('fork', "Fork conversation into a new chat session"),
164
sortText: 'z2_fork',
165
executeImmediately: true,
166
silent: true,
167
locations: [ChatAgentLocation.Chat],
168
when: ContextKeyExpr.or(
169
ChatContextKeys.lockedToCodingAgent.negate(),
170
ChatContextKeys.chatSessionSupportsFork
171
),
172
}, async (_prompt, _progress, _history, _location, sessionResource) => {
173
await commandService.executeCommand('workbench.action.chat.forkConversation', sessionResource);
174
}));
175
this._store.add(slashCommandService.registerSlashCommand({
176
command: 'rename',
177
detail: nls.localize('rename', "Rename this chat"),
178
sortText: 'z2_rename',
179
executeImmediately: false,
180
silent: true,
181
locations: [ChatAgentLocation.Chat],
182
sessionTypes: [SessionType.Local],
183
}, async (prompt, _progress, _history, _location, sessionResource) => {
184
const title = prompt.trim();
185
if (title) {
186
chatService.setChatSessionTitle(sessionResource, title);
187
}
188
}));
189
const setPermissionLevelForSession = (sessionResource: URI, level: ChatPermissionLevel) => {
190
const widget = chatWidgetService.getWidgetBySessionResource(sessionResource) ?? chatWidgetService.lastFocusedWidget;
191
if (widget) {
192
widget.input.setPermissionLevel(level);
193
}
194
};
195
const autoApprovePolicyValue = configurationService.inspect<boolean>(ChatConfiguration.GlobalAutoApprove).policyValue;
196
if (autoApprovePolicyValue !== false) {
197
this._store.add(slashCommandService.registerSlashCommand({
198
command: 'autoApprove',
199
detail: nls.localize('autoApprove', "Set permissions to bypass approvals"),
200
sortText: 'z1_autoApprove',
201
executeImmediately: true,
202
silent: true,
203
locations: [ChatAgentLocation.Chat],
204
sessionTypes: [SessionType.Local, SessionType.CopilotCLI],
205
}, async (_prompt, _progress, _history, _location, sessionResource) => {
206
setPermissionLevelForSession(sessionResource, ChatPermissionLevel.AutoApprove);
207
}));
208
this._store.add(slashCommandService.registerSlashCommand({
209
command: 'disableAutoApprove',
210
detail: nls.localize('disableAutoApprove', "Set permissions back to default"),
211
sortText: 'z1_disableAutoApprove',
212
executeImmediately: true,
213
silent: true,
214
locations: [ChatAgentLocation.Chat],
215
sessionTypes: [SessionType.Local, SessionType.CopilotCLI],
216
}, async (_prompt, _progress, _history, _location, sessionResource) => {
217
setPermissionLevelForSession(sessionResource, ChatPermissionLevel.Default);
218
}));
219
this._store.add(slashCommandService.registerSlashCommand({
220
command: 'yolo',
221
detail: nls.localize('yolo', "Set permissions to bypass approvals"),
222
sortText: 'z1_yolo',
223
executeImmediately: true,
224
silent: true,
225
locations: [ChatAgentLocation.Chat],
226
sessionTypes: [SessionType.Local, SessionType.CopilotCLI],
227
}, async (_prompt, _progress, _history, _location, sessionResource) => {
228
setPermissionLevelForSession(sessionResource, ChatPermissionLevel.AutoApprove);
229
}));
230
this._store.add(slashCommandService.registerSlashCommand({
231
command: 'disableYolo',
232
detail: nls.localize('disableYolo', "Set permissions back to default"),
233
sortText: 'z1_disableYolo',
234
executeImmediately: true,
235
silent: true,
236
locations: [ChatAgentLocation.Chat],
237
sessionTypes: [SessionType.Local, SessionType.CopilotCLI],
238
}, async (_prompt, _progress, _history, _location, sessionResource) => {
239
setPermissionLevelForSession(sessionResource, ChatPermissionLevel.Default);
240
}));
241
if (configurationService.getValue<boolean>(ChatConfiguration.AutopilotEnabled) !== false) {
242
this._store.add(slashCommandService.registerSlashCommand({
243
command: 'autopilot',
244
detail: nls.localize('autopilot', "Set permissions to autopilot mode"),
245
sortText: 'z1_autopilot',
246
executeImmediately: true,
247
silent: true,
248
locations: [ChatAgentLocation.Chat],
249
sessionTypes: [SessionType.Local, SessionType.CopilotCLI],
250
}, async (_prompt, _progress, _history, _location, sessionResource) => {
251
setPermissionLevelForSession(sessionResource, ChatPermissionLevel.Autopilot);
252
}));
253
this._store.add(slashCommandService.registerSlashCommand({
254
command: 'exitAutopilot',
255
detail: nls.localize('exitAutopilot', "Set permissions back to default"),
256
sortText: 'z1_exitAutopilot',
257
executeImmediately: true,
258
silent: true,
259
locations: [ChatAgentLocation.Chat],
260
sessionTypes: [SessionType.Local, SessionType.CopilotCLI],
261
}, async (_prompt, _progress, _history, _location, sessionResource) => {
262
setPermissionLevelForSession(sessionResource, ChatPermissionLevel.Default);
263
}));
264
}
265
}
266
this._store.add(slashCommandService.registerSlashCommand({
267
command: 'help',
268
detail: '',
269
sortText: 'z1_help',
270
executeImmediately: true,
271
locations: [ChatAgentLocation.Chat],
272
modes: [ChatModeKind.Ask],
273
sessionTypes: [SessionType.Local],
274
}, async (prompt, progress, _history, _location, sessionResource) => {
275
const defaultAgent = chatAgentService.getDefaultAgent(ChatAgentLocation.Chat);
276
const agents = chatAgentService.getAgents();
277
278
// Report prefix
279
if (defaultAgent?.metadata.helpTextPrefix) {
280
if (isMarkdownString(defaultAgent.metadata.helpTextPrefix)) {
281
progress.report({ content: defaultAgent.metadata.helpTextPrefix, kind: 'markdownContent' });
282
} else {
283
progress.report({ content: new MarkdownString(defaultAgent.metadata.helpTextPrefix), kind: 'markdownContent' });
284
}
285
progress.report({ content: new MarkdownString('\n\n'), kind: 'markdownContent' });
286
}
287
288
// Report agent list
289
const agentText = (await Promise.all(agents
290
.filter(a => !a.isDefault && !a.isCore)
291
.filter(a => a.locations.includes(ChatAgentLocation.Chat))
292
.map(async a => {
293
const description = a.description ? `- ${a.description}` : '';
294
const agentMarkdown = instantiationService.invokeFunction(accessor => agentToMarkdown(a, sessionResource, true, accessor));
295
const agentLine = `- ${agentMarkdown} ${description}`;
296
const commandText = a.slashCommands.map(c => {
297
const description = c.description ? `- ${c.description}` : '';
298
return `\t* ${agentSlashCommandToMarkdown(a, c, sessionResource)} ${description}`;
299
}).join('\n');
300
301
return (agentLine + '\n' + commandText).trim();
302
}))).join('\n');
303
progress.report({ content: new MarkdownString(agentText, { isTrusted: { enabledCommands: [ChatSubmitAction.ID] } }), kind: 'markdownContent' });
304
305
// Report help text ending
306
if (defaultAgent?.metadata.helpTextPostfix) {
307
progress.report({ content: new MarkdownString('\n\n'), kind: 'markdownContent' });
308
if (isMarkdownString(defaultAgent.metadata.helpTextPostfix)) {
309
progress.report({ content: defaultAgent.metadata.helpTextPostfix, kind: 'markdownContent' });
310
} else {
311
progress.report({ content: new MarkdownString(defaultAgent.metadata.helpTextPostfix), kind: 'markdownContent' });
312
}
313
}
314
315
// Without this, the response will be done before it renders and so it will not stream. This ensures that if the response starts
316
// rendering during the next 200ms, then it will be streamed. Once it starts streaming, the whole response streams even after
317
// it has received all response data has been received.
318
await timeout(200);
319
}));
320
}
321
}
322
323
/**
324
* Registers slash commands declared by chat session providers via
325
* {@link IChatSessionProviderOptionItem.slashCommand}. Each slash command is
326
* scoped to its contributing session type via a `chatSessionType == X` `when`
327
* clause, executes immediately, and updates the session option corresponding
328
* to its declaring item — so e.g. `/yolo` switches the active permission mode
329
* without sending a chat request.
330
*/
331
export class ChatSessionOptionSlashCommandsContribution extends Disposable {
332
333
static readonly ID = 'workbench.contrib.chatSessionOptionSlashCommands';
334
335
private readonly _registrationsByType = this._register(new DisposableMap<string>());
336
337
constructor(
338
@IChatSessionsService private readonly chatSessionsService: IChatSessionsService,
339
@IChatSlashCommandService private readonly slashCommandService: IChatSlashCommandService,
340
@ILogService private readonly logService: ILogService,
341
) {
342
super();
343
344
this._register(this.chatSessionsService.onDidChangeOptionGroups(chatSessionType => {
345
this.refreshForSessionType(chatSessionType);
346
}));
347
}
348
349
private refreshForSessionType(chatSessionType: string): void {
350
// Always tear down the previous registrations for this type before re-adding,
351
// so renames / removals are honored.
352
this._registrationsByType.deleteAndDispose(chatSessionType);
353
354
const groups = this.chatSessionsService.getOptionGroupsForSessionType(chatSessionType);
355
if (!groups || groups.length === 0) {
356
return;
357
}
358
359
const store = new DisposableStore();
360
const seen = new Set<string>();
361
362
for (const group of groups) {
363
for (const item of group.items) {
364
const name = item.slashCommand?.trim();
365
if (!name) {
366
continue;
367
}
368
if (seen.has(name)) {
369
this.logService.warn(`[ChatSessionOptionSlashCommands] Skipping duplicate slash command '${name}' contributed by session type '${chatSessionType}'.`);
370
continue;
371
}
372
if (this.slashCommandService.hasCommand(name, chatSessionType)) {
373
this.logService.warn(`[ChatSessionOptionSlashCommands] Slash command '${name}' contributed by session type '${chatSessionType}' is already registered; skipping.`);
374
continue;
375
}
376
seen.add(name);
377
store.add(this.registerOne(chatSessionType, group, item, name));
378
}
379
}
380
381
if (store.isDisposed || seen.size === 0) {
382
store.dispose();
383
return;
384
}
385
this._registrationsByType.set(chatSessionType, store);
386
}
387
388
private registerOne(
389
chatSessionType: string,
390
group: IChatSessionProviderOptionGroup,
391
item: IChatSessionProviderOptionItem,
392
name: string
393
) {
394
return this.slashCommandService.registerSlashCommand({
395
command: name,
396
detail: item.description ?? nls.localize('chatSessionOption.slashCommand.detail', "Switch to '{0}'", item.name),
397
sortText: `z1_${name}`,
398
executeImmediately: true,
399
silent: true,
400
locations: [ChatAgentLocation.Chat],
401
sessionTypes: [chatSessionType],
402
}, async (_prompt, _progress, _history, _location, sessionResource) => {
403
if (!sessionResource) {
404
return;
405
}
406
this.chatSessionsService.setSessionOption(sessionResource, group.id, item);
407
});
408
}
409
}
410
411