src/vs/platform/agentHost/node/copilot/copilotSlashCommandCompletionProvider.ts

230 LOC · 226 covered · 4 uncovered · 53 ranges · 432 concepts · 24 introducers · 226 tests

File neighbourhood

The centred file is linked to every concept that introduces one of its ranges, every test that runs code from the file, and the gray connector concepts standing between those tests and the file's own introducer concepts. Undirected links join concepts to every file where they introduce source and concepts to the tests they introduce; arrows show specialization between the displayed concepts and bridge only concepts omitted from this view. Concept colors match the source ranges below; connector concepts have no source color and are shown in gray.

Focused file, its introducer and connector concepts, their introduced files, and tests that run code from the file

In the embedded map, ordinary wheel input scrolls the page; use the visible controls to zoom and drag to pan. Open the full-screen map for canvas navigation: wheel pans, Ctrl/Command plus wheel zooms, and arrow keys pan when this region is focused. On touch screens, open the full-screen map to pan or pinch. If JavaScript or WebGL is unavailable, use the related-file, concept, and source links on this page.

Graph controls are ready.

Interactive rendering requires JavaScript and WebGL. Use the related-file, concept, and source links on this page while the interactive map is unavailable.

1 > /*--------------------------------------------------------------------------------------------- copilotSlashCommandCompletionProvider.ts ×6
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 { CancellationToken } from '../../../../base/common/cancellation.js';
7 > import { AgentSession } from '../../common/agentService.js';
8 > import { CompletionItem, CompletionItemKind, CompletionsParams } from '../../common/state/protocol/commands.js';
9 > import { Customization, CustomizationType, DirectoryCustomization, MessageAttachmentKind, PluginCustomization, SkillCustomization } from '../../common/state/protocol/state.js';
10 > import { getCompletionAction, toCommandCompletionAttachmentMeta } from '../../common/meta/agentCompletionAttachmentMeta.js';
11 > import { getCopilotConfigSlashCommandItems, ICopilotConfigSlashCommandState, isCopilotConfigSlashCommand } from '../../common/copilotConfigSlashCommands.js';
12 > import { CompletionTriggerCharacter, IAgentHostCompletionItemProvider } from '../agentHostCompletions.js';
13 > import { extractLeadingSlashToken, extractWhitespaceDelimitedSlashToken, matchesSlashCompletion } from '../agentHostSlashCompletion.js';
14 > import { SYNCED_CUSTOMIZATION_SCHEME } from '../../common/agentHostFileSystemService.js';
15 > import type { CopilotSession } from '@github/copilot-sdk';
16 >
17 > export { parseLeadingSlashCommand } from '../../common/agentHostSlashCommand.js';
18 >
19 > const HIDDEN_RUNTIME_COMMANDS = new Set<string>(['agent', 'app', 'changelog', 'context', 'copy', 'exit', 'extensions', 'feedback', 'help', 'ide', 'instructions', 'login', 'logout', 'mcp', 'model', 'new', 'plugin', 'rename', 'restart', 'resume', 'sandbox', 'session', 'settings', 'skills', 'statusline', 'streamer-mode', 'subagents', 'tasks', 'terminal-setup', 'theme', 'undo', 'update', 'user', 'voice', 'worktree', 'autopilot', 'yolo', 'cd', 'cwd', 'after', 'before', 'add-dir', 'allow-all', 'list-dirs', 'reset-allowed-tools']);
20 >
21 > export const DEFAULT_RUNTIME_SLASH_COMMAND_COMPLETION_WAIT_MS = 300;
22 >
23 > /**
24 > * Lookup hooks used by {@link CopilotSlashCommandCompletionProvider} to
25 > * retrieve runtime slash command metadata and apply feature gating.
26 > */
27 > export interface ICopilotSlashCommandSessionInfo {
28 > /**
29 > * Whether the experimental rubber duck critic subagent is enabled via
30 > * the agent host config. When provided and `false`, `/rubber-duck` is hidden.
31 > */
32 > isRubberDuckEnabled?(): boolean;
33 > /** Runtime slash commands discovered from the SDK session. */
34 > getRuntimeSlashCommands?(sessionId: string, options?: ICopilotRuntimeSlashCommandQueryOptions): Promise<readonly ICopilotRuntimeSlashCommandInfo[]>;
35 > getSessionCustomizations: (session: string) => Promise<readonly Customization[]>;
36 > /**
37 > * The session's current config state (`mode` / `autoApprove` axes), used to
38 > * filter config-action slash command completions so only the state-changing
39 > * forms are offered. When omitted, all forms are offered.
40 > */
41 > getSessionConfigState?(sessionId: string): ICopilotConfigSlashCommandState | undefined;
42 > }
43 >
44 > export interface ICopilotRuntimeSlashCommandQueryOptions {
45 > readonly maxWaitMs?: number;
46 > }
47 >
48 > /**
49 > * Completion provider for Copilot CLI slash commands. Only fires for
50 > * sessions whose URI scheme is `copilotcli` and only when the input begins
51 > * with `/`.
52 > *
53 > * The returned items carry a {@link MessageAttachmentKind.Simple}
54 > * attachment, which the workbench bridge maps into command/skill completion
55 > * attachments. Runtime command dispatch is text-side in `CopilotAgentSession.send`;
56 > * client-side config commands also share the same leading slash parser.
57 > */
58 > export class CopilotSlashCommandCompletionProvider implements IAgentHostCompletionItemProvider {
59 > readonly kinds: ReadonlySet<CompletionItemKind> = new Set([CompletionItemKind.UserMessage]);
60 > readonly triggerCharacters = [CompletionTriggerCharacter.Slash] as const;
61 >
62 > constructor(
63 > private readonly copilotcliId: string, copilotSlashCommandCompletionProvider.ts ×1
64 > private readonly _sessionInfo: ICopilotSlashCommandSessionInfo,
65 > private readonly _runtimeSlashCommandCompletionWaitMs: number = DEFAULT_RUNTIME_SLASH_COMMAND_COMPLETION_WAIT_MS,
66 > ) { }
68 > async provideCompletionItems(params: CompletionsParams, _token: CancellationToken): Promise<readonly CompletionItem[]> {
69 > if (AgentSession.provider(params.channel) !== this.copilotcliId) { copilotSlashCommandCompletionProvider.ts ×3
71 > }
72 > const leadingTokenForSkills = extractWhitespaceDelimitedSlashToken(params.text, params.offset); copilotSlashCommandCompletionProvider.ts ×1
73 > const leadingTokenForCommands = extractLeadingSlashToken(params.text, params.offset);
74 > const leading = leadingTokenForCommands ?? leadingTokenForSkills;
75 > const returnJustSkills = !leadingTokenForCommands && !!leadingTokenForSkills; copilotSlashCommandCompletionProvider.ts ×3
76 > if (!leading) {
78 > }
80 > // Raw session id is the URI path without the leading slash.
81 > const sessionId = AgentSession.id(params.channel);
82 > // `/abc` → typed = 'abc'; empty after just '/' → typed = ''.
83 > const typed = leading.typed;
84 > return await this._getRuntimeSlashCommandCompletionInfo(sessionId, typed, leading, returnJustSkills);
87 > private async _getKnownSkills(sessionId: string) {
88 > const knownCommands = new Set<string>(); copilotSlashCommandCompletionProvider.ts ×7
89 > const customizations = await this._sessionInfo.getSessionCustomizations(sessionId) ?? [];
90 > for (const c of customizations) {
91 > if (c.type === CustomizationType.McpServer || !c.enabled || !c.children) { copilotSlashCommandCompletionProvider.ts ×1
93 > }
94 > for (const child of c.children) { copilotSlashCommandCompletionProvider.ts ×4
95 > if (child.type === CustomizationType.Skill) {
96 > knownCommands.add(this._toSlashCommandCandidate(c, child));
97 > }
98 > }
99 > }
100 > return knownCommands; copilotSlashCommandCompletionProvider.ts ×7
101 > }
103 > private _toSlashCommandCandidate(container: PluginCustomization | DirectoryCustomization, skill: SkillCustomization): string {
104 > // see getCanonicalPluginCommandId copilotSlashCommandCompletionProvider.ts ×4
105 > let slashCommandName = skill.name;
106 > if (container.type === CustomizationType.Plugin && !isSyncedCustomization(container) && skill.name !== container.name) {
107 > slashCommandName = `${container.name}:${skill.name}`; copilotSlashCommandCompletionProvider.ts ×1
108 > }
109 > return slashCommandName; copilotSlashCommandCompletionProvider.ts ×4
110 > }
112 > private async _getRuntimeSlashCommandCompletionInfo(sessionId: string, typed: string, { rangeStart, rangeEnd }: { rangeStart: number; rangeEnd: number }, returnJustSkills: boolean): Promise<CompletionItem[]> {
113 > const [runtimeCommands, knownSkills] = await Promise.all([ copilotSlashCommandCompletionProvider.ts ×7
114 > this._sessionInfo.getRuntimeSlashCommands?.(sessionId, { maxWaitMs: this._runtimeSlashCommandCompletionWaitMs }) ?? [],
115 > this._getKnownSkills(sessionId)
116 > ]);
117 > const typedLower = typed.toLowerCase();
118 > const rubberDuckEnabled = this._sessionInfo?.isRubberDuckEnabled?.() ?? true;
119 > const completionItems: CompletionItem[] = [];
120 > const addedAliases = new Set<string>();
121 >
122 > for (const command of runtimeCommands) {
124 continue;
125 }
126 > if (returnJustSkills && command.kind !== 'skill') { copilotSlashCommandCompletionProvider.ts ×11
128 > }
129 > if (command.kind === 'skill' && knownSkills.has(command.name)) { copilotSlashCommandCompletionProvider.ts ×11
130 > // This is a known skill, so we don't want to show it in the runtime command completion list. copilotSlashCommandCompletionProvider.ts ×1
131 > continue;
132 > }
133 > if (HIDDEN_RUNTIME_COMMANDS.has(command.name) || command.aliases?.some(alias => HIDDEN_RUNTIME_COMMANDS.has(alias))) { copilotSlashCommandCompletionProvider.ts ×11
134 continue;
135 }
136 > // Config-action commands (permission/mode toggles) are surfaced below copilotSlashCommandCompletionProvider.ts ×1
137 > // as workbench-defined items; skip any runtime command that collides
138 > // with them (e.g. a runtime `plan`) to avoid duplicate suggestions.
139 > if (isCopilotConfigSlashCommand(command.name) || command.aliases?.some(alias => isCopilotConfigSlashCommand(alias))) { copilotSlashCommandCompletionProvider.ts ×11
141 > }
142 > if (!rubberDuckEnabled && command.name === 'rubber-duck') { copilotSlashCommandCompletionProvider.ts ×11
144 > }
145 > if (!matchesSlashCompletion(typedLower, command.name) && !command.aliases?.some(alias => matchesSlashCompletion(typedLower, alias))) { copilotSlashCommandCompletionProvider.ts ×11
147 > }
148 > // Use structured input choices as options; if there are none, emit a single item for the command and surface any free-text hint as a prompt. copilotSlashCommandCompletionProvider.ts ×3
149 > const options: (NonNullable<NonNullable<ICopilotRuntimeSlashCommandInfo['input']>['choices']>[number] & { argumentHint?: string })[] = [];
150 >
151 > // If we have a hint, then this means we have a structured command with sub commands or options.
152 > // I.e. the standalone command is also valie.
153 > if (command.input?.hint || !command.input?.choices?.length) { copilotSlashCommandCompletionProvider.ts ×11
154 > options.push({ name: '', description: command.description, argumentHint: command.input?.hint }); copilotSlashCommandCompletionProvider.ts ×1
155 > }
156 > if (command.input?.choices?.length) { copilotSlashCommandCompletionProvider.ts ×11
157 > options.push(...command.input.choices); copilotSlashCommandCompletionProvider.ts ×1
158 > }
160 > // Generate completion items for each alias and option combination.
161 > // If there are no options, generate a single completion item for the alias.
162 > const aliases = Array.from(new Set([command.name].concat(command.aliases ?? []))); copilotSlashCommandCompletionProvider.ts ×11
163 > aliases
164 > .filter(alias => !addedAliases.has(alias))
165 > .forEach(alias => {
167 > .forEach(option => {
168 > // Add a trailing space after the command (and sub command/option if present).
169 > // This is so user can continue to type additional arguments after the command and option.
170 > const insertText = `/${alias}${option.name ? ' ' + option.name : ''} `;
171 > const description = option.description ?? command.description;
172 > const argumentHint = option.argumentHint;
173 > addedAliases.add(alias);
174 >
175 > completionItems.push({
176 > insertText,
177 > rangeStart: rangeStart,
178 > rangeEnd: rangeEnd,
179 > attachment: {
180 > type: MessageAttachmentKind.Simple,
181 > label: insertText,
182 > _meta: toCommandCompletionAttachmentMeta({
183 > command: command.name,
184 > ...(description !== undefined ? { description } : {}),
185 > ...(argumentHint !== undefined ? { argumentHint } : {})
186 > }),
187 > },
188 > });
189 > });
191 > }
193 > // Prepend workbench-defined config-action commands (permission/mode
194 > // toggles). These are not runtime SDK commands; they carry an `action`
195 > // bag on their `_meta` that the workbench interprets on accept. Only
196 > // offered for leading `/command` tokens (not the whitespace-delimited
197 > // skill form).
198 > if (!returnJustSkills) {
199 > const configState = this._sessionInfo.getSessionConfigState?.(sessionId); copilotSlashCommandCompletionProvider.ts ×2
200 > for (const item of getCopilotConfigSlashCommandItems(typed, configState)) {
201 > completionItems.push({ copilotSlashCommandCompletionProvider.ts ×1
202 > insertText: item.insertText,
203 > rangeStart,
204 > rangeEnd,
205 > attachment: {
206 > type: MessageAttachmentKind.Simple,
207 > label: item.label,
208 > _meta: toCommandCompletionAttachmentMeta({
209 > command: item.command,
210 > description: item.description,
211 > ...(item.argumentHint !== undefined ? { argumentHint: item.argumentHint } : {}),
212 > action: { applyConfig: item.applyConfig },
213 > }),
214 > },
215 > });
216 > }
219 > const getSortText = (item: CompletionItem): string => {
220 > return getCompletionAction(item.attachment._meta) ? item.attachment.label : item.insertText; copilotSlashCommandCompletionProvider.ts ×1
221 > };
222 > return completionItems.sort((a, b) => getSortText(a).localeCompare(getSortText(b))); copilotSlashCommandCompletionProvider.ts ×7
223 > }
225 >
226 > export type ICopilotRuntimeSlashCommandInfo = Awaited<ReturnType<CopilotSession['rpc']['commands']['list']>>['commands'][number];
227 >
228 > function isSyncedCustomization(container: PluginCustomization): boolean { copilotSlashCommandCompletionProvider.ts ×4
229 > return container.uri.startsWith(SYNCED_CUSTOMIZATION_SCHEME + ':');
230 > }