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

1201 LOC · 1138 covered · 63 uncovered · 283 ranges · 1868 concepts · 150 introducers · 866 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 > /*--------------------------------------------------------------------------------------------- copilotToolDisplay.ts ×31
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 type { PermissionRequest } from '@github/copilot-sdk';
7 > import { hasKey } from '../../../../base/common/types.js';
8 > import { URI } from '../../../../base/common/uri.js';
9 > import { appendEscapedMarkdownInlineCode, escapeMarkdownLinkLabel, MarkdownString } from '../../../../base/common/htmlContent.js';
10 > import { hash } from '../../../../base/common/hash.js';
11 > import { localize } from '../../../../nls.js';
12 > import type { IAgentToolPendingConfirmationSignal } from '../../common/agentService.js';
13 > import { stripRedundantCdPrefix } from '../../common/commandLineHelpers.js';
14 > import { StringOrMarkdown } from '../../common/state/protocol/state.js';
15 > import { basename } from '../../../../base/common/resources.js';
16 > import { getServerToolDisplay } from '../shared/serverToolGroups.js';
17 >
18 > // =============================================================================
19 > // Copilot CLI built-in tool interfaces
20 > //
21 > // The Copilot CLI (via @github/copilot-sdk) exposes these built-in tools. Tool names
22 > // and parameter shapes are not typed in the SDK -- they come from the CLI server
23 > // as plain strings. These interfaces are derived from observing the CLI's actual
24 > // tool events and the Copilot Chat extension's CLI display table.
25 > //
26 > // Shell tool names follow a pattern per ShellConfig:
27 > // shellToolName, readShellToolName, writeShellToolName,
28 > // stopShellToolName, listShellsToolName
29 > // For bash: bash, read_bash, write_bash, stop_bash/bash_shutdown, list_bash
30 > // For powershell: powershell, read_powershell, write_powershell, stop_powershell/powershell_shutdown, list_powershell
31 > // =============================================================================
32 >
33 > /**
34 > * Known Copilot CLI tool names. These are the `toolName` values that appear
35 > * in `tool.execution_start` events from the SDK.
36 > */
37 > const enum CopilotToolName {
38 > StrReplaceEditor = 'str_replace_editor',
39 > StrReplace = 'str_replace',
40 > Insert = 'insert',
41 >
42 > Bash = 'bash',
43 > ReadBash = 'read_bash',
44 > WriteBash = 'write_bash',
45 > StopBash = 'stop_bash',
46 > BashShutdown = 'bash_shutdown',
47 > ListBash = 'list_bash',
48 >
49 > PowerShell = 'powershell',
50 > ReadPowerShell = 'read_powershell',
51 > WritePowerShell = 'write_powershell',
52 > StopPowerShell = 'stop_powershell',
53 > PowerShellShutdown = 'powershell_shutdown',
54 > ListPowerShell = 'list_powershell',
55 >
56 > View = 'view',
57 > Edit = 'edit',
58 > Create = 'create',
59 > Grep = 'grep',
60 > Rg = 'rg',
61 > Glob = 'glob',
62 > SearchCodeSubagent = 'search_code_subagent',
63 > ReplyToComment = 'reply_to_comment',
64 > CodeReview = 'code_review',
65 > ApplyPatch = 'apply_patch',
66 > GitApplyPatch = 'git_apply_patch',
67 > WebSearch = 'web_search',
68 > WebFetch = 'web_fetch',
69 > AskUser = 'ask_user',
70 > ReportIntent = 'report_intent',
71 > Think = 'think',
72 > ReportProgress = 'report_progress',
73 > UpdateTodo = 'update_todo',
74 > ShowFile = 'show_file',
75 > FetchCopilotCliDocumentation = 'fetch_copilot_cli_documentation',
76 > ProposeWork = 'propose_work',
77 > TaskComplete = 'task_complete',
78 > Skill = 'skill',
79 > Task = 'task',
80 > ListAgents = 'list_agents',
81 > ReadAgent = 'read_agent',
82 > ExitPlanMode = 'exit_plan_mode',
83 > Sql = 'sql',
84 > Lsp = 'lsp',
85 > CreatePullRequest = 'create_pull_request',
86 > GhAdvisoryDatabase = 'gh-advisory-database',
87 > StoreMemory = 'store_memory',
88 > ParallelValidation = 'parallel_validation',
89 > WriteAgent = 'write_agent',
90 > McpReload = 'mcp_reload',
91 > McpValidate = 'mcp_validate',
92 > ToolSearchToolRegex = 'tool_search_tool_regex',
93 > CodeqlChecker = 'codeql_checker',
94 > }
95 >
96 > /** Parameters for the `bash` / `powershell` shell tools. */
97 > interface ICopilotShellToolArgs {
98 > command: string;
99 > timeout?: number;
100 > }
101 >
102 > /** Parameters for file tools (`view`, `edit`, `create`). */
103 > interface ICopilotFileToolArgs {
104 > path: string;
105 > }
106 >
107 > /**
108 > * Parameters for the `view` tool. The Copilot CLI accepts an optional
109 > * `view_range: [startLine, endLine]` (1-based, inclusive). `endLine` may be
110 > * `-1` to mean "to end of file".
111 > */
112 > interface ICopilotViewToolArgs extends ICopilotFileToolArgs {
113 > view_range?: number[];
114 > }
115 >
116 > /**
117 > * Normalizes a `view_range` array. Returns `undefined` unless the array has
118 > * exactly two integer elements with `startLine >= 0`. `endLine === -1` is
119 > * preserved as the "to end of file" sentinel; otherwise `endLine` must be
120 > * `>= startLine`.
121 > */
122 > function formatViewRange(view_range: number[] | undefined): { startLine: number; endLine: number } | undefined { copilotToolDisplay.ts ×4
123 > if (!Array.isArray(view_range) || view_range.length !== 2) {
124 > return undefined; copilotToolDisplay.ts ×2
125 > }
126 > const [startLine, endLine] = view_range; copilotToolDisplay.ts ×2
127 > if (!Number.isInteger(startLine) || !Number.isInteger(endLine)) { copilotToolDisplay.ts ×4
128 > return undefined; copilotToolDisplay.ts ×3
129 > }
130 > if (startLine < 0) { copilotToolDisplay.ts ×2
131 > return undefined; copilotToolDisplay.ts ×3
132 > }
133 > if (endLine !== -1 && endLine < startLine) { copilotToolDisplay.ts ×4
134 > return undefined; copilotToolDisplay.ts ×3
135 > }
136 > return { startLine, endLine }; copilotToolDisplay.ts ×3
137 > }
139 > /**
140 > * Parameters for the `grep` tool. The Copilot CLI's `grep` accepts the same
141 > * rich rg-flag schema as `rg`; the older narrower shape (e.g. `include`) is
142 > * no longer used.
143 > */
144 > interface ICopilotGrepToolArgs {
145 > pattern: string;
146 > path?: string;
147 > output_mode?: 'content' | 'files_with_matches' | 'count';
148 > glob?: string;
149 > type?: string;
150 > '-i'?: boolean;
151 > '-A'?: number;
152 > '-B'?: number;
153 > '-C'?: number;
154 > '-n'?: boolean;
155 > head_limit?: number;
156 > multiline?: boolean;
157 > }
158 >
159 > /**
160 > * Parameters for the `rg` tool. Mirrors {@link ICopilotGrepToolArgs} today but
161 > * is kept as a distinct interface so the two tools can drift independently if
162 > * the SDK ever differentiates them.
163 > */
164 > interface ICopilotRgToolArgs {
165 > pattern: string;
166 > path?: string;
167 > output_mode?: 'content' | 'files_with_matches' | 'count';
168 > glob?: string;
169 > type?: string;
170 > '-i'?: boolean;
171 > '-A'?: number;
172 > '-B'?: number;
173 > '-C'?: number;
174 > '-n'?: boolean;
175 > head_limit?: number;
176 > multiline?: boolean;
177 > }
178 >
179 > /** Parameters for the `glob` tool. */
180 > interface ICopilotGlobToolArgs {
181 > pattern: string;
182 > path?: string;
183 > }
184 >
185 > /** Parameters for the `sql` tool. */
186 > interface ICopilotSqlToolArgs {
187 > description?: string;
188 > query?: string;
189 > }
190 >
191 > /** Parameters for the `web_fetch` tool. */
192 > interface ICopilotWebFetchToolArgs {
193 > url: string;
194 > }
195 >
196 > /**
197 > * Parameters shared by the agent-coordination tools (`read_agent`,
198 > * `write_agent`). The Copilot CLI identifies the target agent by its
199 > * human-readable `agent_id` (e.g. `math-helper`).
200 > */
201 > interface ICopilotAgentToolArgs {
202 > agent_id?: string;
203 > }
204 >
205 > /**
206 > * Reads a well-formed `agent_id` from untrusted tool parameters. Since these are
207 > * parsed from JSON they may not match the expected shape, so the id is returned
208 > * only when it is a non-empty string and is therefore safe to render as inline
209 > * markdown code.
210 > */
211 > function getAgentId(parameters: Record<string, unknown> | undefined): string | undefined { copilotToolDisplay.ts ×3
212 > const agentId = (parameters as ICopilotAgentToolArgs | undefined)?.agent_id;
213 > return typeof agentId === 'string' && agentId.length > 0 ? agentId : undefined;
214 > }
216 > /**
217 > * Parameters for the `apply_patch` / `git_apply_patch` tools. The patch text
218 > * itself lives in `input` using the V4A diff format (file headers like
219 > * `*** Update File: <path>`), so file paths must be parsed out of the body
220 > * rather than read from a top-level field.
221 > */
222 > interface ICopilotApplyPatchToolArgs {
223 > input?: string;
224 > /** Some SDK callers send the patch under `patch` instead of `input`. */
225 > patch?: string;
226 > explanation?: string;
227 > }
228 >
229 > /**
230 > * Headers of the V4A patch format the `apply_patch` tool accepts. Tolerates
231 > * leading whitespace; trims the captured path.
232 > */
233 > const APPLY_PATCH_FILE_HEADERS = [
234 > /^\s*\*\*\*\s+Update File:\s*(.+?)\s*$/,
235 > /^\s*\*\*\*\s+Add File:\s*(.+?)\s*$/,
236 > /^\s*\*\*\*\s+Delete File:\s*(.+?)\s*$/,
237 > /^\s*\*\*\*\s+Move to:\s*(.+?)\s*$/,
238 > ];
239 >
240 > /**
241 > * Extracts the set of file paths affected by an `apply_patch` payload. Reads
242 > * the `*** Update File:` / `*** Add File:` / `*** Delete File:` / `*** Move to:`
243 > * headers from the V4A diff body. Returns paths in document order with
244 > * duplicates removed.
245 > *
246 > * Accepts either a structured args object ({@link ICopilotApplyPatchToolArgs})
247 > * or a bare patch string. The Copilot SDK delivers `apply_patch` with
248 > * `arguments` as a raw V4A patch string (custom tool format), not as a JSON
249 > * object, so the string fallback is the common case for apply_patch.
250 > */
251 > function getApplyPatchFiles(args: string | ICopilotApplyPatchToolArgs | undefined): string[] { copilotToolDisplay.ts ×3
252 > const text = typeof args === 'string' ? args : (args?.input ?? args?.patch);
253 > if (typeof text !== 'string' || text.length === 0) {
254 return [];
255 }
256 > const seen = new Set<string>(); copilotToolDisplay.ts ×3
257 > const out: string[] = [];
258 > for (const line of text.split('\n')) {
259 > for (const re of APPLY_PATCH_FILE_HEADERS) {
260 > const m = re.exec(line);
261 > if (m) {
262 > const path = m[1]; copilotToolDisplay.ts ×1
263 > if (path && !seen.has(path)) {
264 > seen.add(path);
265 > out.push(path);
266 > }
267 > break;
268 > }
270 > }
271 > return out;
272 > }
274 > /** Set of tool names that perform file edits. */
275 > const EDIT_TOOL_NAMES: ReadonlySet<string> = new Set([
276 > CopilotToolName.Edit,
277 > CopilotToolName.StrReplace,
278 > CopilotToolName.Insert,
279 > CopilotToolName.Create,
280 > CopilotToolName.ApplyPatch,
281 > CopilotToolName.GitApplyPatch,
282 > ]);
283 >
284 > const STR_REPLACE_EDITOR_EDIT_COMMANDS: ReadonlySet<string> = new Set([
285 > CopilotToolName.Edit,
286 > CopilotToolName.StrReplace,
287 > CopilotToolName.Insert,
288 > CopilotToolName.Create,
289 > ]);
290 >
291 > /**
292 > * Returns true if the tool modifies files on disk.
293 > */
294 > export function isEditTool(toolName: string, command?: string): boolean {
295 > if (EDIT_TOOL_NAMES.has(toolName)) { copilotToolDisplay.ts ×1
296 > return true; copilotToolDisplay.ts ×1
297 > }
298 > if (toolName === CopilotToolName.StrReplaceEditor) { copilotToolDisplay.ts ×1
299 > return command !== undefined && STR_REPLACE_EDITOR_EDIT_COMMANDS.has(command); copilotToolDisplay.ts ×1
300 > }
301 > return false; copilotToolDisplay.ts ×1
302 > }
304 > /**
305 > * Extracts the target file path from an edit tool's parameters, if available.
306 > * For `apply_patch` / `git_apply_patch` the first file in the V4A patch body
307 > * is returned. Callers that need every affected file (for snapshotting all
308 > * edits in a multi-file patch) should use {@link getEditFilePaths} instead.
309 > */
310 > export function getEditFilePath(parameters: unknown): string | undefined {
311 > return getEditFilePaths(parameters)[0]; copilotToolDisplay.ts ×1
312 > }
314 > /**
315 > * Extracts every file path an edit tool will touch. For `edit` / `create` this
316 > * is the single `path` parameter; for `apply_patch` / `git_apply_patch` this
317 > * is the unique set of files declared in the V4A patch body, in document
318 > * order. Returns an empty array if no paths can be determined.
319 > */
320 > export function getEditFilePaths(parameters: unknown): string[] {
321 > if (typeof parameters === 'string') { copilotToolDisplay.ts ×4
322 > // Could be either a JSON-encoded args object or a raw V4A patch copilotToolDisplay.ts ×2
323 > // string. Copilot SDK delivers `apply_patch` arguments as a bare
324 > // patch string (custom tool format), so when JSON parsing fails
325 > // fall back to treating it as the patch body.
326 > try {
327 > parameters = JSON.parse(parameters);
328 > } catch {
329 > return getApplyPatchFiles(parameters as string); copilotToolDisplay.ts ×1
330 > }
331 > // JSON.parse may have returned a string (e.g. a JSON-encoded patch copilotToolDisplay.ts ×1
332 > // body that round-trips through tryStringify on the call site).
333 > if (typeof parameters === 'string') {
334 return getApplyPatchFiles(parameters);
335 }
338 > if (!parameters || typeof parameters !== 'object') { copilotToolDisplay.ts ×4
339 > return []; copilotToolDisplay.ts ×1
340 > }
342 > const patchArgs = parameters as ICopilotApplyPatchToolArgs;
343 > if (typeof patchArgs.input === 'string' || typeof patchArgs.patch === 'string') { copilotToolDisplay.ts ×4
344 > return getApplyPatchFiles(patchArgs); copilotToolDisplay.ts ×1
345 > }
347 > const args = parameters as ICopilotFileToolArgs;
348 > return typeof args.path === 'string' ? [args.path] : []; copilotToolDisplay.ts ×4
349 > }
351 > /** Set of tool names that execute shell commands (bash or powershell). */
352 > const SHELL_TOOL_NAMES: ReadonlySet<string> = new Set([
353 > CopilotToolName.Bash,
354 > CopilotToolName.PowerShell,
355 > ]);
356 >
357 > /** Set of tool names that write input to an interactive shell session. */
358 > const WRITE_SHELL_TOOL_NAMES: ReadonlySet<string> = new Set([
359 > CopilotToolName.WriteBash,
360 > CopilotToolName.WritePowerShell,
361 > ]);
362 >
363 > /** Set of tool names that read output from an interactive shell session. */
364 > const READ_SHELL_TOOL_NAMES: ReadonlySet<string> = new Set([
365 > CopilotToolName.ReadBash,
366 > CopilotToolName.ReadPowerShell,
367 > ]);
368 >
369 > /** Set of tool names that spawn subagent sessions. */
370 > const SUBAGENT_TOOL_NAMES: ReadonlySet<string> = new Set([
371 > 'task',
372 > ]);
373 >
374 > /** Set of tool names that perform file/text search. */
375 > const SEARCH_TOOL_NAMES: ReadonlySet<string> = new Set([
376 > CopilotToolName.Grep,
377 > CopilotToolName.Rg,
378 > CopilotToolName.Glob,
379 > ]);
380 >
381 > /**
382 > * Tools that should not be shown to the user. These are internal tools
383 > * used by the CLI for its own purposes (e.g., reporting intent to the model).
384 > *
385 > * `skill` is hidden because the SDK already emits a richer `skill.invoked`
386 > * lifecycle event with the resolved skill file path; the agent session
387 > * synthesizes a tool-start/complete pair from that event so the UI can
388 > * render a clickable file link instead of just the skill name. See
389 > * {@link synthesizeSkillToolCall}.
390 > */
391 > const HIDDEN_TOOL_NAMES: ReadonlySet<string> = new Set([
392 > CopilotToolName.ReportIntent,
393 > CopilotToolName.Skill,
394 > ]);
395 >
396 > /**
397 > * Returns true if the tool should be hidden from the UI.
398 > */
399 > export function isHiddenTool(toolName: string): boolean {
400 > return HIDDEN_TOOL_NAMES.has(toolName); copilotToolDisplay.ts ×1
401 > }
403 > /**
404 > * Returns true for the auto-approved agent-coordination tools (list/read/write
405 > * agents). These are client-contributed tools that never go through the
406 > * permission flow, so the agent host auto-readies them at start to surface a
407 > * tailored invocation message instead of the generic fallback.
408 > */
409 > export function isAgentCoordinationTool(toolName: string): boolean {
410 > return toolName === CopilotToolName.ListAgents copilotAgentSession.ts ×2
411 > || toolName === CopilotToolName.ReadAgent copilotAgentSession.ts ×1
412 > || toolName === CopilotToolName.WriteAgent;
415 > /**
416 > * Returns true when the tool is Copilot's internal Autopilot completion signal.
417 > */
418 > export function isTaskCompleteTool(toolName: string): boolean {
419 > return toolName === CopilotToolName.TaskComplete; copilotToolDisplay.ts ×1
420 > }
422 > /**
423 > * Extracts the user-facing Autopilot completion summary from the tool output,
424 > * falling back to the original `summary` argument for older/incomplete events.
425 > */
426 > export function getTaskCompleteSummary(parameters: Record<string, unknown> | undefined, toolOutput: string | undefined): string | undefined {
427 > if (toolOutput && toolOutput.trim().length > 0) { copilotToolDisplay.ts ×2
428 > return toolOutput; copilotToolDisplay.ts ×1
429 > }
430 > const summary = parameters?.summary; copilotToolDisplay.ts ×1
431 > return typeof summary === 'string' && summary.trim().length > 0 ? summary : undefined; copilotToolDisplay.ts ×2
432 > }
434 > /**
435 > * Formats the Autopilot completion summary as the markdown response part
436 > * content, including the localized prefix.
437 > */
438 > export function getTaskCompleteMarkdown(parameters: Record<string, unknown> | undefined, toolOutput: string | undefined): string | undefined {
439 > const summary = getTaskCompleteSummary(parameters, toolOutput); copilotToolDisplay.ts ×1
440 > if (!summary) {
441 > return undefined; copilotToolDisplay.ts ×1
442 > }
443 > return '\n\n' + localize('toolMarkdown.taskComplete', "**Task completed:** {0}", summary); copilotToolDisplay.ts ×1
444 > }
446 > /**
447 > * Returns true if the tool should render as a markdown response part instead
448 > * of a tool-call entry.
449 > */
450 > export function isMarkdownRenderedTool(toolName: string): boolean {
451 > return isTaskCompleteTool(toolName); copilotToolDisplay.ts ×1
452 > }
454 > /**
455 > * Returns markdown content for tools rendered as inline markdown response
456 > * parts.
457 > */
458 > export function getToolMarkdownContent(toolName: string, parameters: Record<string, unknown> | undefined): string | undefined {
459 > if (!isMarkdownRenderedTool(toolName)) { copilotToolDisplay.ts ×1
460 > return undefined; copilotToolDisplay.ts ×1
461 > }
462 > const summary = getTaskCompleteSummary(parameters, undefined); copilotToolDisplay.ts ×1
463 > if (!summary) {
464 > return undefined; copilotToolDisplay.ts ×1
465 > }
466 > return getTaskCompleteMarkdown(parameters, undefined); copilotToolDisplay.ts ×1
467 > }
469 > /**
470 > * Returns true if the tool executes shell commands.
471 > */
472 > export function isShellTool(toolName: string): boolean {
473 > return SHELL_TOOL_NAMES.has(toolName); copilotToolDisplay.ts ×1
474 > }
476 > /**
477 > * Extracts the intention for a shell tool call from its `description`
478 > * argument. The Copilot shell tools (`bash`/`powershell`) carry a short
479 > * human-readable description of what the command does, which matches the
480 > * model's intention summary. Non-shell tools have no such argument, so this
481 > * returns `undefined` for them.
482 > */
483 > export function getShellIntention(toolName: string, parameters: Record<string, unknown> | undefined): string | undefined {
484 > if (isShellTool(toolName) && typeof parameters?.description === 'string' && parameters.description.length > 0) { copilotToolDisplay.ts ×1
485 > return parameters.description; copilotToolDisplay.ts ×1
486 > }
487 > return undefined; copilotToolDisplay.ts ×1
488 > }
490 > // =============================================================================
491 > // Display helpers
492 > //
493 > // These functions translate Copilot CLI tool names and arguments into
494 > // human-readable display strings. This logic lives here -- in the agent-host
495 > // process -- so the IPC protocol stays agent-agnostic; the renderer never needs
496 > // to know about specific tool names.
497 > // =============================================================================
498 >
499 > function truncate(text: string, maxLength: number): string { copilotToolDisplay.ts ×1
500 > return text.length > maxLength ? text.substring(0, maxLength - 3) + '...' : text;
501 > }
503 > /**
504 > * Formats a file path as a markdown link `[](file-uri)` so it renders
505 > * as a clickable file widget in the chat UI.
506 > */
507 > function formatPathAsMarkdownLink(path: string): string { copilotToolDisplay.ts ×1
508 > const uri = URI.file(path);
509 > return `[${escapeMarkdownLinkLabel(basename(uri))}](${uri})`;
510 > }
512 > function formatUrlAsMarkdownLink(url: string): string { copilotToolDisplay.ts ×4
513 > return new MarkdownString().appendLink(url, truncate(url, 80)).value;
514 > }
516 > /**
517 > * Wraps a localized message containing a markdown file link into a
518 > * `StringOrMarkdown` object so the renderer treats it as markdown.
519 > */
520 > function md(value: string): StringOrMarkdown { copilotToolDisplay.ts ×1
521 > return { markdown: value };
522 > }
524 > export function getToolDisplayName(toolName: string): string {
525 > const serverDisplay = getServerToolDisplay(toolName, undefined)?.displayName; copilotToolDisplay.ts ×2
526 > if (serverDisplay !== undefined) {
527 > return serverDisplay; copilotToolDisplay.ts ×1
528 > }
529 > switch (toolName) { copilotToolDisplay.ts ×1
530 > case CopilotToolName.StrReplaceEditor:
531 > case CopilotToolName.Edit: copilotToolDisplay.ts ×2
532 > case CopilotToolName.StrReplace:
533 > case CopilotToolName.Insert: return localize('toolName.edit', "Edit File");
534 > case CopilotToolName.Create: return localize('toolName.create', "Create File");
535 > case CopilotToolName.View: return localize('toolName.read', "Read");
536 > case CopilotToolName.Bash:
537 > case CopilotToolName.PowerShell: return localize('toolName.shell', "Run Shell Command");
538 > case CopilotToolName.ReadBash:
539 > case CopilotToolName.ReadPowerShell: return localize('toolName.readTerminal', "Read Terminal");
540 > case CopilotToolName.WriteBash: return localize('toolName.writeBash', "Write to Bash");
541 > case CopilotToolName.WritePowerShell: return localize('toolName.writePowerShell', "Write to PowerShell");
542 > case CopilotToolName.StopBash:
543 > case CopilotToolName.StopPowerShell:
544 > case CopilotToolName.BashShutdown:
545 > case CopilotToolName.PowerShellShutdown: return localize('toolName.stopShell', "Stop Terminal Session");
546 > case CopilotToolName.ListBash:
547 > case CopilotToolName.ListPowerShell: return localize('toolName.listShellSessions', "List Shell Sessions");
548 > case CopilotToolName.Grep:
549 > case CopilotToolName.Rg:
550 > case CopilotToolName.Glob: return localize('toolName.search', "Search");
551 > case CopilotToolName.SearchCodeSubagent: return localize('toolName.searchCode', "Search Code");
552 > case CopilotToolName.ApplyPatch: return localize('toolName.applyPatch', "Apply Patch");
553 > case CopilotToolName.GitApplyPatch: return localize('toolName.patch', "Patch");
554 > case CopilotToolName.CodeqlChecker: return localize('toolName.codeqlChecker', "CodeQL Security Scan");
555 > case CopilotToolName.CodeReview: return localize('toolName.codeReview', "Code Review");
556 > case CopilotToolName.ReplyToComment: return localize('toolName.replyToComment', "Reply to Comment");
557 > case CopilotToolName.Think: return localize('toolName.think', "Thinking");
558 > case CopilotToolName.ReportIntent: return localize('toolName.reportIntent', "Report Intent");
559 > case CopilotToolName.ReportProgress: return localize('toolName.reportProgress', "Progress update");
560 > case CopilotToolName.WebSearch: return localize('toolName.webSearch', "Web Search");
561 > case CopilotToolName.WebFetch: return localize('toolName.fetchWebContent', "Fetch Web Content");
562 > case CopilotToolName.UpdateTodo: return localize('toolName.updateTodo', "Update Todo");
563 > case CopilotToolName.ShowFile: return localize('toolName.showFile', "Show File");
564 > case CopilotToolName.FetchCopilotCliDocumentation: return localize('toolName.fetchCopilotCliDocumentation', "Fetch Documentation");
565 > case CopilotToolName.ProposeWork: return localize('toolName.proposeWork', "Propose Work");
566 > case CopilotToolName.TaskComplete: return localize('toolName.taskComplete', "Task Complete");
567 > case CopilotToolName.AskUser: return localize('toolName.askUser', "Ask User");
568 > case CopilotToolName.Skill: return localize('toolName.invokeSkill', "Invoke Skill");
569 > case CopilotToolName.Task: return localize('toolName.task', "Delegate Task");
570 > case CopilotToolName.ListAgents: return localize('toolName.listAgents', "List Agents");
571 > case CopilotToolName.ReadAgent: return localize('toolName.readAgent', "Read Agent");
572 > case CopilotToolName.ExitPlanMode: return localize('toolName.exitPlanModeFull', "Exit Plan Mode");
573 > case CopilotToolName.Sql: return localize('toolName.sql', "Execute SQL");
574 > case CopilotToolName.Lsp: return localize('toolName.lsp', "Language Server");
575 > case CopilotToolName.CreatePullRequest: return localize('toolName.createPullRequest', "Create Pull Request");
576 > case CopilotToolName.GhAdvisoryDatabase: return localize('toolName.ghAdvisoryDatabase', "Check Dependencies");
577 > case CopilotToolName.StoreMemory: return localize('toolName.storeMemory', "Store Memory");
578 > case CopilotToolName.ParallelValidation: return localize('toolName.parallelValidation', "Validate Changes");
579 > case CopilotToolName.WriteAgent: return localize('toolName.writeAgent', "Write to Agent");
580 > case CopilotToolName.McpReload: return localize('toolName.mcpReload', "Reload MCP Config");
581 > case CopilotToolName.McpValidate: return localize('toolName.mcpValidate', "Validate MCP Config");
582 > case CopilotToolName.ToolSearchToolRegex: return localize('toolName.toolSearchToolRegex', "Search Tools");
583 > default: return toolName;
584 > }
585 > }
587 > export function getInvocationMessage(toolName: string, displayName: string, parameters: Record<string, unknown> | undefined): StringOrMarkdown {
588 > const serverDisplay = getServerToolDisplay(toolName, parameters)?.invocationMessage; copilotToolDisplay.ts ×14
589 > if (serverDisplay !== undefined) {
590 > return serverDisplay; copilotToolDisplay.ts ×2
591 > }
593 > if (SHELL_TOOL_NAMES.has(toolName)) {
594 > const args = parameters as ICopilotShellToolArgs | undefined; copilotToolDisplay.ts ×1
595 > if (args?.command) {
596 > const firstLine = args.command.split('\n')[0]; copilotToolDisplay.ts ×1
597 > return md(localize('toolInvoke.shellCmd', "Running {0}", appendEscapedMarkdownInlineCode(truncate(firstLine, 80))));
598 > }
599 > return localize('toolInvoke.shell', "Running {0} command", displayName); copilotToolDisplay.ts ×1
600 > }
602 > if (WRITE_SHELL_TOOL_NAMES.has(toolName)) {
603 > const args = parameters as ICopilotShellToolArgs | undefined; copilotToolDisplay.ts ×1
604 > if (args?.command) {
605 > const firstLine = args.command.split('\n')[0]; copilotToolDisplay.ts ×1
606 > return md(localize('toolInvoke.writeShellCmd', "Sending {0} to shell", appendEscapedMarkdownInlineCode(truncate(firstLine, 80))));
607 > }
608 > return localize('toolInvoke.writeShell', "Sending input to shell"); copilotToolDisplay.ts ×1
609 > }
611 > if (READ_SHELL_TOOL_NAMES.has(toolName)) {
612 > return localize('toolInvoke.readTerminal', "Reading Terminal"); copilotToolDisplay.ts ×1
613 > }
615 > switch (toolName) {
616 > case CopilotToolName.View: {
617 > const args = parameters as ICopilotViewToolArgs | undefined; copilotToolDisplay.ts ×1
618 > if (args?.path) {
619 > const link = formatPathAsMarkdownLink(args.path); copilotToolDisplay.ts ×4
620 > const range = formatViewRange(args.view_range);
621 > if (range) {
622 > if (range.endLine === -1) { copilotToolDisplay.ts ×3
623 > return md(localize('toolInvoke.viewFileFromLine', "Reading {0}, line {1} to the end", link, range.startLine)); copilotToolDisplay.ts ×2
624 > }
625 > if (range.endLine !== range.startLine) { copilotToolDisplay.ts ×2
626 > return md(localize('toolInvoke.viewFileRange', "Reading {0}, lines {1} to {2}", link, range.startLine, range.endLine)); copilotToolDisplay.ts ×2
627 > }
628 > return md(localize('toolInvoke.viewFileLine', "Reading {0}, line {1}", link, range.startLine)); copilotToolDisplay.ts ×2
629 > }
630 > return md(localize('toolInvoke.viewFile', "Reading {0}", link)); copilotToolDisplay.ts ×2
631 > }
632 > return localize('toolInvoke.view', "Reading file"); copilotToolDisplay.ts ×7
633 > }
634 > case CopilotToolName.Edit: { copilotToolDisplay.ts ×14
635 > const args = parameters as ICopilotFileToolArgs | undefined; copilotToolDisplay.ts ×1
636 > if (args?.path) {
637 > return md(localize('toolInvoke.editFile', "Editing {0}", formatPathAsMarkdownLink(args.path))); copilotToolDisplay.ts ×2
638 > }
639 > return localize('toolInvoke.edit', "Editing file"); copilotToolDisplay.ts ×1
640 > }
641 > case CopilotToolName.Create: { copilotToolDisplay.ts ×14
642 > const args = parameters as ICopilotFileToolArgs | undefined; copilotToolDisplay.ts ×2
643 > if (args?.path) {
644 > return md(localize('toolInvoke.createFile', "Creating {0}", formatPathAsMarkdownLink(args.path)));
645 > }
646 return localize('toolInvoke.create', "Creating file");
647 }
648 > case CopilotToolName.Grep: { copilotToolDisplay.ts ×14
649 > const args = parameters as ICopilotGrepToolArgs | undefined; copilotToolDisplay.ts ×1
650 > if (args?.pattern) {
651 > return md(localize('toolInvoke.grepPattern', "Searching for {0}", appendEscapedMarkdownInlineCode(truncate(args.pattern, 80))));
652 > }
653 return localize('toolInvoke.grep', "Searching files");
654 }
655 > case CopilotToolName.Rg: { copilotToolDisplay.ts ×14
656 > const args = parameters as ICopilotRgToolArgs | undefined; copilotToolDisplay.ts ×1
657 > if (args?.pattern) {
658 > return md(localize('toolInvoke.grepPattern', "Searching for {0}", appendEscapedMarkdownInlineCode(truncate(args.pattern, 80)))); copilotToolDisplay.ts ×2
659 > }
660 > return localize('toolInvoke.grep', "Searching files"); copilotToolDisplay.ts ×1
661 > }
662 > case CopilotToolName.Glob: { copilotToolDisplay.ts ×14
663 > const args = parameters as ICopilotGlobToolArgs | undefined; copilotToolDisplay.ts ×7
664 > if (args?.pattern) {
665 return md(localize('toolInvoke.globPattern', "Finding files matching {0}", appendEscapedMarkdownInlineCode(truncate(args.pattern, 80))));
666 }
667 > return localize('toolInvoke.glob', "Finding files"); copilotToolDisplay.ts ×7
668 > }
669 > case CopilotToolName.ApplyPatch: copilotToolDisplay.ts ×14
670 > case CopilotToolName.GitApplyPatch: {
671 > const files = getEditFilePaths(parameters); copilotToolDisplay.ts ×1
672 > if (files.length === 1) {
673 > return md(localize('toolInvoke.patchFile', "Editing {0}", formatPathAsMarkdownLink(files[0]))); copilotToolDisplay.ts ×1
674 > }
675 > if (files.length > 1) { copilotToolDisplay.ts ×2
676 > return md(localize('toolInvoke.patchFiles', "Editing {0}", files.map(formatPathAsMarkdownLink).join(', '))); copilotToolDisplay.ts ×2
677 > }
678 > return localize('toolInvoke.patch', "Editing files"); copilotToolDisplay.ts ×2
679 > }
680 > case CopilotToolName.Sql: { copilotToolDisplay.ts ×14
681 > const args = parameters as ICopilotSqlToolArgs | undefined; copilotToolDisplay.ts ×2
682 > return args?.description || localize('toolInvoke.sql', "Executing SQL query");
683 > }
684 > case CopilotToolName.WebFetch: { copilotToolDisplay.ts ×14
685 > const args = parameters as ICopilotWebFetchToolArgs | undefined; copilotToolDisplay.ts ×2
686 > if (args?.url) {
687 > return md(localize('toolInvoke.webFetch', "Fetching {0}", formatUrlAsMarkdownLink(args.url))); copilotToolDisplay.ts ×4
688 > }
689 > return localize('toolInvoke.webFetchGeneric', "Fetching URL"); copilotToolDisplay.ts ×2
690 > }
691 > case CopilotToolName.ExitPlanMode: copilotToolDisplay.ts ×14
692 return localize('toolInvoke.exitPlanMode', "Presenting plan");
693 > case CopilotToolName.Task: copilotToolDisplay.ts ×14
694 > return localize('toolInvoke.task', "Delegating task"); copilotToolDisplay.ts ×1
695 > // The agent-coordination tools (list/read/write agents) are fast, so copilotToolDisplay.ts ×14
696 > // they use a single message for both the running and completed states:
697 > // the past-tense phrasing. See getPastTenseMessage.
698 > case CopilotToolName.ListAgents:
699 > case CopilotToolName.ReadAgent:
700 > case CopilotToolName.WriteAgent:
701 > return getPastTenseMessage(toolName, displayName, parameters, true); copilotToolDisplay.ts ×1
703 > return displayName; copilotToolDisplay.ts ×1
705 > }
707 > export function getPastTenseMessage(toolName: string, displayName: string, parameters: Record<string, unknown> | undefined, success: boolean, resultText?: string): StringOrMarkdown {
708 > if (!success) { copilotToolDisplay.ts ×17
709 > return localize('toolComplete.failed', "\"{0}\" failed", displayName); copilotToolDisplay.ts ×1
710 > }
712 > const serverDisplay = getServerToolDisplay(toolName, parameters, { text: resultText, success })?.pastTenseMessage;
713 > if (serverDisplay !== undefined) { copilotToolDisplay.ts ×17
714 > return serverDisplay; copilotToolDisplay.ts ×2
715 > }
717 > if (SHELL_TOOL_NAMES.has(toolName)) {
718 > const args = parameters as ICopilotShellToolArgs | undefined; copilotToolDisplay.ts ×1
719 > if (args?.command) {
720 > const firstLine = args.command.split('\n')[0]; copilotToolDisplay.ts ×1
721 > return md(localize('toolComplete.shellCmd', "Ran {0}", appendEscapedMarkdownInlineCode(truncate(firstLine, 80))));
722 > }
723 > return localize('toolComplete.shell', "Ran {0} command", displayName); buildSessionEvents.ts ×1
724 > }
726 > if (WRITE_SHELL_TOOL_NAMES.has(toolName)) {
727 > const args = parameters as ICopilotShellToolArgs | undefined; copilotToolDisplay.ts ×1
728 > if (args?.command) {
729 > const firstLine = args.command.split('\n')[0]; copilotToolDisplay.ts ×1
730 > return md(localize('toolComplete.writeShellCmd', "Sent {0} to shell", appendEscapedMarkdownInlineCode(truncate(firstLine, 80))));
731 > }
732 > return localize('toolComplete.writeShell', "Sent input to shell"); copilotToolDisplay.ts ×1
733 > }
735 > if (READ_SHELL_TOOL_NAMES.has(toolName)) {
736 > return localize('toolComplete.readTerminal', "Read Terminal"); copilotToolDisplay.ts ×1
737 > }
739 > switch (toolName) {
740 > case CopilotToolName.View: {
741 > const args = parameters as ICopilotViewToolArgs | undefined; copilotToolDisplay.ts ×1
742 > if (args?.path) {
743 > const link = formatPathAsMarkdownLink(args.path); copilotToolDisplay.ts ×1
744 > const range = formatViewRange(args.view_range);
745 > if (range) {
746 > if (range.endLine === -1) { copilotToolDisplay.ts ×3
747 > return md(localize('toolComplete.viewFileFromLine', "Read {0}, line {1} to the end", link, range.startLine)); copilotToolDisplay.ts ×2
748 > }
749 > if (range.endLine !== range.startLine) { copilotToolDisplay.ts ×2
750 > return md(localize('toolComplete.viewFileRange', "Read {0}, lines {1} to {2}", link, range.startLine, range.endLine)); copilotToolDisplay.ts ×2
751 > }
752 > return md(localize('toolComplete.viewFileLine', "Read {0}, line {1}", link, range.startLine)); copilotToolDisplay.ts ×2
753 > }
754 > return md(localize('toolComplete.viewFile', "Read {0}", link)); copilotToolDisplay.ts ×1
755 > }
756 > return localize('toolComplete.view', "Read file"); copilotToolDisplay.ts ×7
757 > }
758 > case CopilotToolName.Edit: { copilotToolDisplay.ts ×17
759 > const args = parameters as ICopilotFileToolArgs | undefined; copilotToolDisplay.ts ×2
760 > if (args?.path) {
761 return md(localize('toolComplete.editFile', "Edited {0}", formatPathAsMarkdownLink(args.path)));
762 }
763 > return localize('toolComplete.edit', "Edited file"); copilotToolDisplay.ts ×2
764 > }
765 > case CopilotToolName.Create: { copilotToolDisplay.ts ×17
766 const args = parameters as ICopilotFileToolArgs | undefined;
767 if (args?.path) {
768 return md(localize('toolComplete.createFile', "Created {0}", formatPathAsMarkdownLink(args.path)));
769 }
770 return localize('toolComplete.create', "Created file");
771 }
772 > case CopilotToolName.Grep: { copilotToolDisplay.ts ×17
773 > const args = parameters as ICopilotGrepToolArgs | undefined; copilotToolDisplay.ts ×1
774 > if (args?.pattern) {
775 > return md(localize('toolComplete.grepPattern', "Searched for {0}", appendEscapedMarkdownInlineCode(truncate(args.pattern, 80))));
776 > }
777 return localize('toolComplete.grep', "Searched files");
778 }
779 > case CopilotToolName.Rg: { copilotToolDisplay.ts ×17
780 > const args = parameters as ICopilotRgToolArgs | undefined; copilotToolDisplay.ts ×2
781 > if (args?.pattern) {
782 > return md(localize('toolComplete.grepPattern', "Searched for {0}", appendEscapedMarkdownInlineCode(truncate(args.pattern, 80))));
783 > }
784 return localize('toolComplete.grep', "Searched files");
785 }
786 > case CopilotToolName.Glob: { copilotToolDisplay.ts ×17
787 > const args = parameters as ICopilotGlobToolArgs | undefined; copilotToolDisplay.ts ×7
788 > if (args?.pattern) {
789 return md(localize('toolComplete.globPattern', "Found files matching {0}", appendEscapedMarkdownInlineCode(truncate(args.pattern, 80))));
790 }
791 > return localize('toolComplete.glob', "Found files"); copilotToolDisplay.ts ×7
792 > }
793 > case CopilotToolName.ApplyPatch: copilotToolDisplay.ts ×17
794 > case CopilotToolName.GitApplyPatch: {
795 > const files = getEditFilePaths(parameters); copilotToolDisplay.ts ×1
796 > if (files.length === 1) {
797 > return md(localize('toolComplete.patchFile', "Edited {0}", formatPathAsMarkdownLink(files[0]))); copilotToolDisplay.ts ×1
798 > }
799 > if (files.length > 1) { copilotToolDisplay.ts ×2
800 > return md(localize('toolComplete.patchFiles', "Edited {0}", files.map(formatPathAsMarkdownLink).join(', '))); copilotToolDisplay.ts ×2
801 > }
802 > return localize('toolComplete.patch', "Edited files"); copilotToolDisplay.ts ×2
803 > }
804 > case CopilotToolName.Sql: { copilotToolDisplay.ts ×17
805 > const args = parameters as ICopilotSqlToolArgs | undefined; copilotToolDisplay.ts ×2
806 > return args?.description || localize('toolComplete.sql', "Executed SQL query");
807 > }
808 > case CopilotToolName.WebFetch: { copilotToolDisplay.ts ×17
809 > const args = parameters as ICopilotWebFetchToolArgs | undefined; copilotToolDisplay.ts ×2
810 > if (args?.url) {
811 > return md(localize('toolComplete.webFetch', "Fetched {0}", formatUrlAsMarkdownLink(args.url))); copilotToolDisplay.ts ×4
812 > }
813 > return localize('toolComplete.webFetchGeneric', "Fetched URL"); copilotToolDisplay.ts ×2
814 > }
815 > case CopilotToolName.ExitPlanMode: copilotToolDisplay.ts ×17
816 return localize('toolComplete.exitPlanMode', "Exited plan mode");
817 > case CopilotToolName.Task: copilotToolDisplay.ts ×17
818 > return localize('toolComplete.task', "Delegated task"); copilotToolDisplay.ts ×1
819 > case CopilotToolName.ListAgents: copilotToolDisplay.ts ×17
820 > return localize('toolComplete.listAgents', "Listed agents"); copilotToolDisplay.ts ×1
821 > case CopilotToolName.ReadAgent: { copilotToolDisplay.ts ×17
822 > const agentId = getAgentId(parameters); copilotToolDisplay.ts ×3
823 > if (agentId) {
824 > return md(localize('toolComplete.readAgent', "Read agent {0}", appendEscapedMarkdownInlineCode(agentId))); copilotToolDisplay.ts ×2
825 > }
826 > return localize('toolComplete.readAgentGeneric', "Read agent"); copilotToolDisplay.ts ×2
827 > }
828 > case CopilotToolName.WriteAgent: { copilotToolDisplay.ts ×17
829 > const agentId = getAgentId(parameters); copilotToolDisplay.ts ×3
830 > if (agentId) {
831 > return md(localize('toolComplete.writeAgent', "Wrote to agent {0}", appendEscapedMarkdownInlineCode(agentId))); copilotToolDisplay.ts ×2
832 > }
833 > return localize('toolComplete.writeAgentGeneric', "Wrote to agent"); copilotToolDisplay.ts ×2
834 > }
836 > return displayName; copilotToolDisplay.ts ×1
838 > }
840 > // =============================================================================
841 > // Skill event synthesis
842 > //
843 > // The Copilot SDK emits a `skill` tool call (which we hide) and, separately, a
844 > // `skill.invoked` lifecycle event with the resolved skill file path. We turn
845 > // the latter into a synthesized tool-start/complete pair so clients can render
846 > // a clickable file link to the SKILL.md the agent loaded -- matching the
847 > // existing `view`-tool display style. Live and replay paths share this helper
848 > // so they stay in lock-step (see also the mirrored-pair gotcha for tool-call
849 > // display in this file).
850 > // =============================================================================
851 >
852 > /** Subset of the SDK's `skill.invoked` payload that the synth helper needs. */
853 > export interface ICopilotSkillInvokedData {
854 > readonly name: string;
855 > readonly path?: string;
856 > readonly description?: string;
857 > }
858 >
859 > /**
860 > * Builds a stable synthetic tool call id for a `skill.invoked` event so
861 > * reconnect/replay produces the same id as the original live emit. The id
862 > * is used unencoded as a path segment (e.g. by `ChatResponseResource.createUri`),
863 > * so it must not contain characters like `/` -- we hash any fallback values
864 > * that could carry filesystem paths or arbitrary text.
865 > */
866 > export function getSkillSyntheticToolCallId(eventId: string | undefined, data: ICopilotSkillInvokedData): string {
867 > if (eventId) { copilotToolDisplay.ts ×5
868 > return `synth-skill-${eventId}`; copilotToolDisplay.ts ×1
869 > }
870 > const seed = data.path ?? data.name; copilotToolDisplay.ts ×1
871 > return `synth-skill-${hash(seed).toString(16)}`; copilotToolDisplay.ts ×5
872 > }
874 > /**
875 > * Synthesized data for a `skill.invoked` tool call. Used by both the live
876 > * session handler and the history-replay mapper so the two paths render
877 > * identically. Callers wrap this into protocol actions or {@link Turn}
878 > * data; this helper avoids any agent-protocol coupling.
879 > */
880 > export interface ISynthesizedSkillToolCall {
881 > readonly toolCallId: string;
882 > readonly toolName: string;
883 > readonly displayName: string;
884 > readonly invocationMessage: StringOrMarkdown;
885 > readonly pastTenseMessage: StringOrMarkdown;
886 > }
887 >
888 > /**
889 > * Synthesizes the data for a `skill.invoked` tool call (a tool-start /
890 > * tool-complete pair). Returns the constituent fields without coupling to
891 > * any specific event or action shape — callers compose them into protocol
892 > * actions or {@link Turn} entries as needed.
893 > */
894 > export function synthesizeSkillToolCall(
895 > data: ICopilotSkillInvokedData, copilotToolDisplay.ts ×5
896 > eventId: string | undefined,
897 > ): ISynthesizedSkillToolCall {
898 > const toolCallId = getSkillSyntheticToolCallId(eventId, data);
899 > const displayName = localize('toolName.skill', "Read Skill");
900 > // Use the skill name as the link text rather than the basename: every skill
901 > // file is named SKILL.md, so `Reading skill [plan]` reads better than the
902 > // always-identical `Reading skill [SKILL.md]`. The client may further upgrade
903 > // this link to a rich pill based on the `SKILL.md` basename. Skill names and
904 > // paths come from the SDK / agent host and are escaped to prevent markdown
905 > // injection from a malicious skill author.
906 > // Escape only the characters that would break out of markdown link text
907 > // syntax (`\` and `]`); a full markdown escape would leave visible
908 > // backslashes in renderers (like the skill pill) that extract link text
909 > // without re-parsing markdown.
910 > const escapedName = escapeMarkdownLinkLabel(data.name);
911 > const skillLink = data.path ? `[${escapedName}](${URI.file(data.path)})` : undefined;
912 > const invocationMessage: StringOrMarkdown = skillLink
913 > ? md(localize('toolInvoke.skill', "Reading skill {0}", skillLink))
914 > : localize('toolInvoke.skillName', "Reading skill {0}", data.name); copilotToolDisplay.ts ×2
915 > const pastTenseMessage: StringOrMarkdown = skillLink copilotToolDisplay.ts ×5
916 > ? md(localize('toolComplete.skill', "Read skill {0}", skillLink))
917 > : localize('toolComplete.skillName', "Read skill {0}", data.name); copilotToolDisplay.ts ×2
918 > return { copilotToolDisplay.ts ×5
919 > toolCallId,
920 > toolName: CopilotToolName.Skill,
921 > displayName,
922 > invocationMessage,
923 > pastTenseMessage,
924 > };
925 > }
927 > export function getToolInputString(toolName: string, parameters: Record<string, unknown> | undefined, rawArguments: string | undefined): string | undefined {
928 > if (!parameters && !rawArguments) { copilotToolDisplay.ts ×6
929 > return undefined; copilotToolDisplay.ts ×1
930 > }
932 > if (SHELL_TOOL_NAMES.has(toolName) || WRITE_SHELL_TOOL_NAMES.has(toolName)) { copilotToolDisplay.ts ×6
933 > const args = parameters as ICopilotShellToolArgs | undefined; copilotToolDisplay.ts ×2
934 > // Custom tool overrides may wrap the args: { kind: 'custom-tool', args: { command: '...' } }
935 > const command = args?.command ?? (args as Record<string, unknown> | undefined)?.args;
936 > if (typeof command === 'string') {
937 > return command; copilotToolDisplay.ts ×1
938 > }
939 > if (typeof command === 'object' && command !== null && hasKey(command, { command: true })) { copilotToolDisplay.ts ×2
940 return (command as ICopilotShellToolArgs).command;
941 }
942 > return rawArguments; copilotToolDisplay.ts ×1
943 > }
945 > switch (toolName) {
946 > case CopilotToolName.Grep: {
947 > const args = parameters as ICopilotGrepToolArgs | undefined; copilotToolDisplay.ts ×1
948 > return args?.pattern ?? rawArguments;
949 > }
950 > case CopilotToolName.Rg: { copilotToolDisplay.ts ×6
951 > const args = parameters as ICopilotRgToolArgs | undefined; copilotToolDisplay.ts ×1
952 > return args?.pattern ?? rawArguments;
953 > }
954 > case CopilotToolName.WebFetch: { copilotToolDisplay.ts ×6
955 > const args = parameters as ICopilotWebFetchToolArgs | undefined; copilotToolDisplay.ts ×4
956 > return args?.url ?? rawArguments;
957 > }
958 > default: copilotToolDisplay.ts ×6
959 > // For other tools, show the formatted JSON arguments copilotToolDisplay.ts ×2
960 > if (parameters) {
961 > try {
962 > return JSON.stringify(parameters, null, 2);
963 > } catch {
964 return rawArguments;
965 }
967 return rawArguments;
969 > }
971 > /**
972 > * Returns a rendering hint for the given tool. Currently 'terminal', 'subagent',
973 > * and 'search' are supported, which tell the renderer to display the tool with
974 > * a terminal command block, a subagent widget, or a search icon respectively.
975 > */
976 > export function getToolKind(toolName: string): 'terminal' | 'subagent' | 'search' | undefined {
977 > if (SHELL_TOOL_NAMES.has(toolName)) { copilotToolDisplay.ts ×1
978 > return 'terminal'; copilotToolDisplay.ts ×1
979 > }
980 > if (SUBAGENT_TOOL_NAMES.has(toolName)) { copilotToolDisplay.ts ×1
981 > return 'subagent'; copilotToolDisplay.ts ×1
982 > }
983 > if (SEARCH_TOOL_NAMES.has(toolName)) { copilotToolDisplay.ts ×1
984 > return 'search'; copilotToolDisplay.ts ×1
985 > }
986 > return undefined; copilotToolDisplay.ts ×1
987 > }
989 > /**
990 > * Extracts subagent metadata (agent name, description) from the parsed
991 > * arguments of a Copilot SDK subagent tool call. The Copilot `task` tool
992 > * uses `agent_type` (snake_case), which this normalizes into the generic
993 > * `subagentAgentName` / `subagentDescription` shape used by the rest of the
994 > * agent host code.
995 > *
996 > * Only call this for tools where {@link getToolKind} returned `'subagent'`.
997 > */
998 > export function getSubagentMetadata(parameters: Record<string, unknown> | undefined): { agentName?: string; description?: string } {
999 > if (!parameters) { copilotToolDisplay.ts ×4
1000 return {};
1001 }
1002 > const agentName = typeof parameters.agent_type === 'string' && parameters.agent_type.length > 0 copilotToolDisplay.ts ×4
1003 > ? parameters.agent_type copilotAgentSession.ts ×5
1004 > : undefined; copilotToolDisplay.ts ×1
1005 > const description = typeof parameters.description === 'string' && parameters.description.length > 0 copilotToolDisplay.ts ×4
1006 > ? parameters.description copilotToolDisplay.ts ×1
1007 > : undefined; copilotToolDisplay.ts ×7
1008 > return { agentName, description }; copilotToolDisplay.ts ×4
1009 > }
1011 > /**
1012 > * Returns the shell language identifier for syntax highlighting.
1013 > * Used when creating terminal tool-specific data for the renderer.
1014 > */
1015 > export function getShellLanguage(toolName: string): string {
1016 > switch (toolName) { copilotToolDisplay.ts ×1
1017 > case CopilotToolName.PowerShell:
1018 > case CopilotToolName.WritePowerShell:
1019 > case CopilotToolName.ReadPowerShell: return 'powershell';
1020 > default: return 'shellscript';
1021 > }
1022 > }
1024 > // =============================================================================
1025 > // Permission display
1026 > //
1027 > // Derives display fields from SDK permission requests for the tool
1028 > // confirmation UI. Colocated with the tool-start display helpers above so
1029 > // that formatting utilities (formatPathAsMarkdownLink, md, etc.) are shared.
1030 > // =============================================================================
1031 >
1032 > export function tryStringify(value: unknown): string | undefined {
1034 > return JSON.stringify(value);
1035 > } catch {
1036 return undefined;
1037 }
1040 > /**
1041 > * Loose, optional-field projection of the SDK's {@link PermissionRequest}
1042 > * discriminated union. Lets the rest of the agent host read the well-known
1043 > * fields without `switch (request.kind)` narrowing at every access site.
1044 > *
1045 > * The SDK's `PermissionRequest` (a union with required per-variant fields) is
1046 > * structurally assignable to this interface — every variant carries `kind`
1047 > * and `toolCallId?`, and the variant-specific fields are listed here as
1048 > * optional. Use this type at the agent-host boundary so call sites and tests
1049 > * can rely on a single shape.
1050 > */
1051 > export interface ITypedPermissionRequest {
1052 > /** Permission kind discriminator from the SDK. */
1053 > kind: PermissionRequest['kind'];
1054 > /** Tool call ID that triggered this permission request, when available. */
1055 > toolCallId?: string;
1056 > /** File path — set for `read` permission requests. */
1057 > path?: string;
1058 > /** File path — set for `write` permission requests. */
1059 > fileName?: string;
1060 > /** Full shell command text — set for `shell` permission requests. */
1061 > fullCommandText?: string;
1062 > /**
1063 > * True when the model requested this `shell` command run outside the
1064 > * sandbox (via `requestSandboxBypass`) and the host opted in via
1065 > * `sandbox.allowBypass`.
1066 > */
1067 > requestSandboxBypass?: boolean;
1068 > /** Human-readable intention describing the operation. */
1069 > intention?: string;
1070 > /** MCP server name — set for `mcp` permission requests. */
1071 > serverName?: string;
1072 > /** Tool name — set for `mcp` and `custom-tool` permission requests. */
1073 > toolName?: string;
1074 > /** Tool arguments — set for `custom-tool` permission requests. */
1075 > args?: Record<string, unknown>;
1076 > /** URL — set for `url` permission requests. */
1077 > url?: string;
1078 > /** Unified diff of the proposed change — set for `write` permission requests. */
1079 > diff?: string;
1080 > /** New file contents that will be written — set for `write` permission requests. */
1081 > newFileContents?: string;
1082 > }
1083 >
1084 > /** Safely extract a string value from an SDK field that may be `unknown` at runtime. */
1085 > function str(value: unknown): string | undefined { copilotToolDisplay.ts ×10
1086 > return typeof value === 'string' ? value : undefined;
1087 > }
1089 > /**
1090 > * Derives display fields from a permission request for the tool confirmation UI.
1091 > */
1092 > export function getPermissionDisplay(request: ITypedPermissionRequest, workingDirectory?: URI, isNewFile?: boolean): {
1093 > confirmationTitle: string; copilotToolDisplay.ts ×10
1094 > invocationMessage: StringOrMarkdown;
1095 > toolInput?: string;
1096 > /** Normalized permission kind for auto-approval routing. */
1097 > permissionKind: IAgentToolPendingConfirmationSignal['permissionKind'];
1098 > /** File path extracted from the request. */
1099 > permissionPath?: string;
1100 > } {
1101 > const path = str(request.path) ?? str(request.fileName);
1102 > const fullCommandText = str(request.fullCommandText);
1103 > const intention = str(request.intention);
1104 > const serverName = str(request.serverName);
1105 > const toolName = str(request.toolName);
1106 >
1107 > const shellConfirmationTitle = request.requestSandboxBypass
1108 > ? localize('copilot.permission.shell.bypass.title', "Run in terminal outside the sandbox?") copilotToolDisplay.ts ×1
1109 > : localize('copilot.permission.shell.title', "Run in terminal?"); copilotToolDisplay.ts ×1
1111 > switch (request.kind) {
1112 > case 'shell': {
1113 > // Strip a redundant `cd <workingDirectory> && …` prefix so the copilotToolDisplay.ts ×1
1114 > // confirmation dialog shows the simplified command.
1115 > const shellParams: Record<string, unknown> | undefined = fullCommandText ? { command: fullCommandText } : undefined;
1116 > stripRedundantCdPrefix(CopilotToolName.Bash, shellParams, workingDirectory);
1117 > const cleanedCommand = typeof shellParams?.command === 'string' ? shellParams.command : fullCommandText;
1118 > return {
1119 > confirmationTitle: shellConfirmationTitle,
1120 > invocationMessage: intention ?? getInvocationMessage(CopilotToolName.Bash, getToolDisplayName(CopilotToolName.Bash), cleanedCommand ? { command: cleanedCommand } : undefined),
1121 > toolInput: cleanedCommand,
1122 > permissionKind: 'shell',
1123 > permissionPath: path,
1124 > };
1125 > }
1126 > case 'custom-tool': { copilotToolDisplay.ts ×10
1127 > // Custom tool overrides (e.g. our shell tool). Extract the actual copilotToolDisplay.ts ×2
1128 > // tool args from the SDK's wrapper envelope.
1129 > const args = typeof request.args === 'object' && request.args !== null ? request.args as Record<string, unknown> : undefined;
1130 > const sdkToolName = str(request.toolName);
1131 > if (args && sdkToolName && isShellTool(sdkToolName) && typeof args.command === 'string') {
1132 > stripRedundantCdPrefix(sdkToolName, args, workingDirectory); copilotToolDisplay.ts ×1
1133 > const command = args.command as string;
1134 > return {
1135 > confirmationTitle: shellConfirmationTitle,
1136 > invocationMessage: getInvocationMessage(sdkToolName, getToolDisplayName(sdkToolName), { command }),
1137 > toolInput: command,
1138 > permissionKind: 'shell',
1139 > permissionPath: path,
1140 > };
1141 > }
1142 > return { copilotToolDisplay.ts ×1
1143 > confirmationTitle: localize('copilot.permission.default.title', "Allow tool call?"),
1144 > invocationMessage: md(localize('copilot.permission.default.message', "Allow the model to call {0}?", appendEscapedMarkdownInlineCode(toolName ?? request.kind))), copilotToolDisplay.ts ×2
1145 > toolInput: args ? tryStringify(args) : tryStringify(request),
1146 > permissionKind: request.kind,
1147 > permissionPath: path,
1148 > };
1149 > }
1150 > case 'write': { copilotToolDisplay.ts ×10
1151 > const toolName = isNewFile ? CopilotToolName.Create : CopilotToolName.Edit; copilotToolDisplay.ts ×2
1152 > return {
1153 > confirmationTitle: isNewFile
1154 > ? localize('copilot.permission.create.title', "Create file?") copilotToolDisplay.ts ×2
1155 > : localize('copilot.permission.write.title', "Write file?"), copilotToolDisplay.ts ×2
1156 > invocationMessage: getInvocationMessage(toolName, getToolDisplayName(toolName), path ? { path } : undefined), copilotToolDisplay.ts ×2
1157 > toolInput: tryStringify(path ? { path } : request) ?? undefined,
1158 > permissionKind: 'write',
1159 > permissionPath: path,
1160 > };
1161 > }
1162 > case 'mcp': { copilotToolDisplay.ts ×10
1163 const title = toolName ?? localize('copilot.permission.mcp.defaultTool', "MCP Tool");
1164 return {
1165 confirmationTitle: serverName
1166 ? localize('copilot.permission.mcp.title', "Allow tool from {0}?", serverName)
1167 : localize('copilot.permission.default.title', "Allow tool call?"),
1168 invocationMessage: serverName ? `${serverName}: ${title}` : title,
1169 toolInput: tryStringify({ serverName, toolName }) ?? undefined,
1170 permissionKind: 'mcp',
1171 permissionPath: path,
1172 };
1173 }
1174 > case 'read': copilotToolDisplay.ts ×10
1175 > return { copilotToolDisplay.ts ×1
1176 > confirmationTitle: localize('copilot.permission.read.title', "Allow reading file outside of workspace?"),
1177 > invocationMessage: getInvocationMessage(CopilotToolName.View, getToolDisplayName(CopilotToolName.View), path ? { path } : undefined),
1178 > permissionKind: 'read',
1179 > permissionPath: path,
1180 > };
1181 > case 'url': { copilotToolDisplay.ts ×10
1182 const url = str(request.url);
1183 // Parse through URL for punycode escaping, but preserve the raw value if parsing fails.
1184 const normalizedUrl = url ? (URL.canParse(url) ? new URL(url).href : url) : undefined;
1185 return {
1186 confirmationTitle: localize('copilot.permission.url.title', "Fetch URL?"),
1187 invocationMessage: md(localize('copilot.permission.url.message', "Allow fetching web content?")),
1188 toolInput: normalizedUrl ? JSON.stringify({ url: normalizedUrl }) : undefined,
1189 permissionKind: 'url',
1190 };
1191 }
1192 > default: copilotToolDisplay.ts ×10
1193 return {
1194 confirmationTitle: localize('copilot.permission.default.title', "Allow tool call?"),
1195 invocationMessage: md(localize('copilot.permission.default.message', "Allow the model to call {0}?", appendEscapedMarkdownInlineCode(toolName ?? request.kind))),
1196 toolInput: tryStringify(request) ?? undefined,
1197 permissionKind: request.kind,
1198 permissionPath: path,
1199 };
1201 > }