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