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.
/*---------------------------------------------------------------------------------------------
copilotToolDisplay.ts ×31
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import type { PermissionRequest } from '@github/copilot-sdk';
import { hasKey } from '../../../../base/common/types.js';
import { URI } from '../../../../base/common/uri.js';
import { appendEscapedMarkdownInlineCode, escapeMarkdownLinkLabel, MarkdownString } from '../../../../base/common/htmlContent.js';
import { hash } from '../../../../base/common/hash.js';
import { localize } from '../../../../nls.js';
import type { IAgentToolPendingConfirmationSignal } from '../../common/agentService.js';
import { stripRedundantCdPrefix } from '../../common/commandLineHelpers.js';
import { StringOrMarkdown } from '../../common/state/protocol/state.js';
import { basename } from '../../../../base/common/resources.js';
import { getServerToolDisplay } from '../shared/serverToolGroups.js';
// =============================================================================
// Copilot CLI built-in tool interfaces
//
// The Copilot CLI (via @github/copilot-sdk) exposes these built-in tools. Tool names
// and parameter shapes are not typed in the SDK -- they come from the CLI server
// as plain strings. These interfaces are derived from observing the CLI's actual
// tool events and the Copilot Chat extension's CLI display table.
//
// Shell tool names follow a pattern per ShellConfig:
// shellToolName, readShellToolName, writeShellToolName,
// stopShellToolName, listShellsToolName
// For bash: bash, read_bash, write_bash, stop_bash/bash_shutdown, list_bash
// For powershell: powershell, read_powershell, write_powershell, stop_powershell/powershell_shutdown, list_powershell
// =============================================================================
/**
* Known Copilot CLI tool names. These are the `toolName` values that appear
* in `tool.execution_start` events from the SDK.
*/
const enum CopilotToolName {
StrReplaceEditor = 'str_replace_editor',
StrReplace = 'str_replace',
Insert = 'insert',
Bash = 'bash',
ReadBash = 'read_bash',
WriteBash = 'write_bash',
StopBash = 'stop_bash',
BashShutdown = 'bash_shutdown',
ListBash = 'list_bash',
PowerShell = 'powershell',
ReadPowerShell = 'read_powershell',
WritePowerShell = 'write_powershell',
StopPowerShell = 'stop_powershell',
PowerShellShutdown = 'powershell_shutdown',
ListPowerShell = 'list_powershell',
View = 'view',
Edit = 'edit',
Create = 'create',
Grep = 'grep',
Rg = 'rg',
Glob = 'glob',
SearchCodeSubagent = 'search_code_subagent',
ReplyToComment = 'reply_to_comment',
CodeReview = 'code_review',
ApplyPatch = 'apply_patch',
GitApplyPatch = 'git_apply_patch',
WebSearch = 'web_search',
WebFetch = 'web_fetch',
AskUser = 'ask_user',
ReportIntent = 'report_intent',
Think = 'think',
ReportProgress = 'report_progress',
UpdateTodo = 'update_todo',
ShowFile = 'show_file',
FetchCopilotCliDocumentation = 'fetch_copilot_cli_documentation',
ProposeWork = 'propose_work',
TaskComplete = 'task_complete',
Skill = 'skill',
Task = 'task',
ListAgents = 'list_agents',
ReadAgent = 'read_agent',
ExitPlanMode = 'exit_plan_mode',
Sql = 'sql',
Lsp = 'lsp',
CreatePullRequest = 'create_pull_request',
GhAdvisoryDatabase = 'gh-advisory-database',
StoreMemory = 'store_memory',
ParallelValidation = 'parallel_validation',
WriteAgent = 'write_agent',
McpReload = 'mcp_reload',
McpValidate = 'mcp_validate',
ToolSearchToolRegex = 'tool_search_tool_regex',
CodeqlChecker = 'codeql_checker',
}
/** Parameters for the `bash` / `powershell` shell tools. */
interface ICopilotShellToolArgs {
command: string;
timeout?: number;
}
/** Parameters for file tools (`view`, `edit`, `create`). */
interface ICopilotFileToolArgs {
path: string;
}
/**
* Parameters for the `view` tool. The Copilot CLI accepts an optional
* `view_range: [startLine, endLine]` (1-based, inclusive). `endLine` may be
* `-1` to mean "to end of file".
*/
interface ICopilotViewToolArgs extends ICopilotFileToolArgs {
view_range?: number[];
}
/**
* Normalizes a `view_range` array. Returns `undefined` unless the array has
* exactly two integer elements with `startLine >= 0`. `endLine === -1` is
* preserved as the "to end of file" sentinel; otherwise `endLine` must be
* `>= startLine`.
*/
function formatViewRange(view_range: number[] | undefined): { startLine: number; endLine: number } | undefined {
copilotToolDisplay.ts ×4
if (!Array.isArray(view_range) || view_range.length !== 2) {
}
}
}
}
}
/**
* Parameters for the `grep` tool. The Copilot CLI's `grep` accepts the same
* rich rg-flag schema as `rg`; the older narrower shape (e.g. `include`) is
* no longer used.
*/
interface ICopilotGrepToolArgs {
pattern: string;
path?: string;
output_mode?: 'content' | 'files_with_matches' | 'count';
glob?: string;
type?: string;
'-i'?: boolean;
'-A'?: number;
'-B'?: number;
'-C'?: number;
'-n'?: boolean;
head_limit?: number;
multiline?: boolean;
}
/**
* Parameters for the `rg` tool. Mirrors {@link ICopilotGrepToolArgs} today but
* is kept as a distinct interface so the two tools can drift independently if
* the SDK ever differentiates them.
*/
interface ICopilotRgToolArgs {
pattern: string;
path?: string;
output_mode?: 'content' | 'files_with_matches' | 'count';
glob?: string;
type?: string;
'-i'?: boolean;
'-A'?: number;
'-B'?: number;
'-C'?: number;
'-n'?: boolean;
head_limit?: number;
multiline?: boolean;
}
/** Parameters for the `glob` tool. */
interface ICopilotGlobToolArgs {
pattern: string;
path?: string;
}
/** Parameters for the `sql` tool. */
interface ICopilotSqlToolArgs {
description?: string;
query?: string;
}
/** Parameters for the `web_fetch` tool. */
interface ICopilotWebFetchToolArgs {
url: string;
}
/**
* Parameters shared by the agent-coordination tools (`read_agent`,
* `write_agent`). The Copilot CLI identifies the target agent by its
* human-readable `agent_id` (e.g. `math-helper`).
*/
interface ICopilotAgentToolArgs {
agent_id?: string;
}
/**
* Reads a well-formed `agent_id` from untrusted tool parameters. Since these are
* parsed from JSON they may not match the expected shape, so the id is returned
* only when it is a non-empty string and is therefore safe to render as inline
* markdown code.
*/
function getAgentId(parameters: Record<string, unknown> | undefined): string | undefined {
copilotToolDisplay.ts ×3
const agentId = (parameters as ICopilotAgentToolArgs | undefined)?.agent_id;
return typeof agentId === 'string' && agentId.length > 0 ? agentId : undefined;
}
/**
* Parameters for the `apply_patch` / `git_apply_patch` tools. The patch text
* itself lives in `input` using the V4A diff format (file headers like
* `*** Update File: <path>`), so file paths must be parsed out of the body
* rather than read from a top-level field.
*/
interface ICopilotApplyPatchToolArgs {
input?: string;
/** Some SDK callers send the patch under `patch` instead of `input`. */
patch?: string;
explanation?: string;
}
/**
* Headers of the V4A patch format the `apply_patch` tool accepts. Tolerates
* leading whitespace; trims the captured path.
*/
const APPLY_PATCH_FILE_HEADERS = [
/^\s*\*\*\*\s+Update File:\s*(.+?)\s*$/,
/^\s*\*\*\*\s+Add File:\s*(.+?)\s*$/,
/^\s*\*\*\*\s+Delete File:\s*(.+?)\s*$/,
/^\s*\*\*\*\s+Move to:\s*(.+?)\s*$/,
];
/**
* Extracts the set of file paths affected by an `apply_patch` payload. Reads
* the `*** Update File:` / `*** Add File:` / `*** Delete File:` / `*** Move to:`
* headers from the V4A diff body. Returns paths in document order with
* duplicates removed.
*
* Accepts either a structured args object ({@link ICopilotApplyPatchToolArgs})
* or a bare patch string. The Copilot SDK delivers `apply_patch` with
* `arguments` as a raw V4A patch string (custom tool format), not as a JSON
* object, so the string fallback is the common case for apply_patch.
*/
function getApplyPatchFiles(args: string | ICopilotApplyPatchToolArgs | undefined): string[] {
copilotToolDisplay.ts ×3
const text = typeof args === 'string' ? args : (args?.input ?? args?.patch);
if (typeof text !== 'string' || text.length === 0) {
return [];
}
const out: string[] = [];
for (const line of text.split('\n')) {
for (const re of APPLY_PATCH_FILE_HEADERS) {
const m = re.exec(line);
if (m) {
if (path && !seen.has(path)) {
seen.add(path);
out.push(path);
}
break;
}
}
return out;
}
/** Set of tool names that perform file edits. */
const EDIT_TOOL_NAMES: ReadonlySet<string> = new Set([
CopilotToolName.Edit,
CopilotToolName.StrReplace,
CopilotToolName.Insert,
CopilotToolName.Create,
CopilotToolName.ApplyPatch,
CopilotToolName.GitApplyPatch,
]);
const STR_REPLACE_EDITOR_EDIT_COMMANDS: ReadonlySet<string> = new Set([
CopilotToolName.Edit,
CopilotToolName.StrReplace,
CopilotToolName.Insert,
CopilotToolName.Create,
]);
/**
* Returns true if the tool modifies files on disk.
*/
export function isEditTool(toolName: string, command?: string): boolean {
}
return command !== undefined && STR_REPLACE_EDITOR_EDIT_COMMANDS.has(command);
copilotToolDisplay.ts ×1
}
}
/**
* Extracts the target file path from an edit tool's parameters, if available.
* For `apply_patch` / `git_apply_patch` the first file in the V4A patch body
* is returned. Callers that need every affected file (for snapshotting all
* edits in a multi-file patch) should use {@link getEditFilePaths} instead.
*/
export function getEditFilePath(parameters: unknown): string | undefined {
}
/**
* Extracts every file path an edit tool will touch. For `edit` / `create` this
* is the single `path` parameter; for `apply_patch` / `git_apply_patch` this
* is the unique set of files declared in the V4A patch body, in document
* order. Returns an empty array if no paths can be determined.
*/
export function getEditFilePaths(parameters: unknown): string[] {
// string. Copilot SDK delivers `apply_patch` arguments as a bare
// patch string (custom tool format), so when JSON parsing fails
// fall back to treating it as the patch body.
try {
parameters = JSON.parse(parameters);
} catch {
}
// body that round-trips through tryStringify on the call site).
if (typeof parameters === 'string') {
return getApplyPatchFiles(parameters);
}
}
const patchArgs = parameters as ICopilotApplyPatchToolArgs;
if (typeof patchArgs.input === 'string' || typeof patchArgs.patch === 'string') {
copilotToolDisplay.ts ×4
}
const args = parameters as ICopilotFileToolArgs;
}
/** Set of tool names that execute shell commands (bash or powershell). */
const SHELL_TOOL_NAMES: ReadonlySet<string> = new Set([
CopilotToolName.Bash,
CopilotToolName.PowerShell,
]);
/** Set of tool names that write input to an interactive shell session. */
const WRITE_SHELL_TOOL_NAMES: ReadonlySet<string> = new Set([
CopilotToolName.WriteBash,
CopilotToolName.WritePowerShell,
]);
/** Set of tool names that read output from an interactive shell session. */
const READ_SHELL_TOOL_NAMES: ReadonlySet<string> = new Set([
CopilotToolName.ReadBash,
CopilotToolName.ReadPowerShell,
]);
/** Set of tool names that spawn subagent sessions. */
const SUBAGENT_TOOL_NAMES: ReadonlySet<string> = new Set([
'task',
]);
/** Set of tool names that perform file/text search. */
const SEARCH_TOOL_NAMES: ReadonlySet<string> = new Set([
CopilotToolName.Grep,
CopilotToolName.Rg,
CopilotToolName.Glob,
]);
/**
* Tools that should not be shown to the user. These are internal tools
* used by the CLI for its own purposes (e.g., reporting intent to the model).
*
* `skill` is hidden because the SDK already emits a richer `skill.invoked`
* lifecycle event with the resolved skill file path; the agent session
* synthesizes a tool-start/complete pair from that event so the UI can
* render a clickable file link instead of just the skill name. See
* {@link synthesizeSkillToolCall}.
*/
const HIDDEN_TOOL_NAMES: ReadonlySet<string> = new Set([
CopilotToolName.ReportIntent,
CopilotToolName.Skill,
]);
/**
* Returns true if the tool should be hidden from the UI.
*/
export function isHiddenTool(toolName: string): boolean {
}
/**
* Returns true for the auto-approved agent-coordination tools (list/read/write
* agents). These are client-contributed tools that never go through the
* permission flow, so the agent host auto-readies them at start to surface a
* tailored invocation message instead of the generic fallback.
*/
export function isAgentCoordinationTool(toolName: string): boolean {
|| toolName === CopilotToolName.WriteAgent;
/**
* Returns true when the tool is Copilot's internal Autopilot completion signal.
*/
export function isTaskCompleteTool(toolName: string): boolean {
}
/**
* Extracts the user-facing Autopilot completion summary from the tool output,
* falling back to the original `summary` argument for older/incomplete events.
*/
export function getTaskCompleteSummary(parameters: Record<string, unknown> | undefined, toolOutput: string | undefined): string | undefined {
}
return typeof summary === 'string' && summary.trim().length > 0 ? summary : undefined;
copilotToolDisplay.ts ×2
}
/**
* Formats the Autopilot completion summary as the markdown response part
* content, including the localized prefix.
*/
export function getTaskCompleteMarkdown(parameters: Record<string, unknown> | undefined, toolOutput: string | undefined): string | undefined {
if (!summary) {
}
return '\n\n' + localize('toolMarkdown.taskComplete', "**Task completed:** {0}", summary);
copilotToolDisplay.ts ×1
}
/**
* Returns true if the tool should render as a markdown response part instead
* of a tool-call entry.
*/
export function isMarkdownRenderedTool(toolName: string): boolean {
}
/**
* Returns markdown content for tools rendered as inline markdown response
* parts.
*/
export function getToolMarkdownContent(toolName: string, parameters: Record<string, unknown> | undefined): string | undefined {
}
if (!summary) {
}
}
/**
* Returns true if the tool executes shell commands.
*/
export function isShellTool(toolName: string): boolean {
}
/**
* Extracts the intention for a shell tool call from its `description`
* argument. The Copilot shell tools (`bash`/`powershell`) carry a short
* human-readable description of what the command does, which matches the
* model's intention summary. Non-shell tools have no such argument, so this
* returns `undefined` for them.
*/
export function getShellIntention(toolName: string, parameters: Record<string, unknown> | undefined): string | undefined {
if (isShellTool(toolName) && typeof parameters?.description === 'string' && parameters.description.length > 0) {
copilotToolDisplay.ts ×1
}
}
// =============================================================================
// Display helpers
//
// These functions translate Copilot CLI tool names and arguments into
// human-readable display strings. This logic lives here -- in the agent-host
// process -- so the IPC protocol stays agent-agnostic; the renderer never needs
// to know about specific tool names.
// =============================================================================
return text.length > maxLength ? text.substring(0, maxLength - 3) + '...' : text;
}
/**
* Formats a file path as a markdown link `[](file-uri)` so it renders
* as a clickable file widget in the chat UI.
*/
const uri = URI.file(path);
return `[${escapeMarkdownLinkLabel(basename(uri))}](${uri})`;
}
return new MarkdownString().appendLink(url, truncate(url, 80)).value;
}
/**
* Wraps a localized message containing a markdown file link into a
* `StringOrMarkdown` object so the renderer treats it as markdown.
*/
return { markdown: value };
}
export function getToolDisplayName(toolName: string): string {
const serverDisplay = getServerToolDisplay(toolName, undefined)?.displayName;
copilotToolDisplay.ts ×2
if (serverDisplay !== undefined) {
}
case CopilotToolName.StrReplaceEditor:
case CopilotToolName.StrReplace:
case CopilotToolName.Insert: return localize('toolName.edit', "Edit File");
case CopilotToolName.Create: return localize('toolName.create', "Create File");
case CopilotToolName.View: return localize('toolName.read', "Read");
case CopilotToolName.Bash:
case CopilotToolName.PowerShell: return localize('toolName.shell', "Run Shell Command");
case CopilotToolName.ReadBash:
case CopilotToolName.ReadPowerShell: return localize('toolName.readTerminal', "Read Terminal");
case CopilotToolName.WriteBash: return localize('toolName.writeBash', "Write to Bash");
case CopilotToolName.WritePowerShell: return localize('toolName.writePowerShell', "Write to PowerShell");
case CopilotToolName.StopBash:
case CopilotToolName.StopPowerShell:
case CopilotToolName.BashShutdown:
case CopilotToolName.PowerShellShutdown: return localize('toolName.stopShell', "Stop Terminal Session");
case CopilotToolName.ListBash:
case CopilotToolName.ListPowerShell: return localize('toolName.listShellSessions', "List Shell Sessions");
case CopilotToolName.Grep:
case CopilotToolName.Rg:
case CopilotToolName.Glob: return localize('toolName.search', "Search");
case CopilotToolName.SearchCodeSubagent: return localize('toolName.searchCode', "Search Code");
case CopilotToolName.ApplyPatch: return localize('toolName.applyPatch', "Apply Patch");
case CopilotToolName.GitApplyPatch: return localize('toolName.patch', "Patch");
case CopilotToolName.CodeqlChecker: return localize('toolName.codeqlChecker', "CodeQL Security Scan");
case CopilotToolName.CodeReview: return localize('toolName.codeReview', "Code Review");
case CopilotToolName.ReplyToComment: return localize('toolName.replyToComment', "Reply to Comment");
case CopilotToolName.Think: return localize('toolName.think', "Thinking");
case CopilotToolName.ReportIntent: return localize('toolName.reportIntent', "Report Intent");
case CopilotToolName.ReportProgress: return localize('toolName.reportProgress', "Progress update");
case CopilotToolName.WebSearch: return localize('toolName.webSearch', "Web Search");
case CopilotToolName.WebFetch: return localize('toolName.fetchWebContent', "Fetch Web Content");
case CopilotToolName.UpdateTodo: return localize('toolName.updateTodo', "Update Todo");
case CopilotToolName.ShowFile: return localize('toolName.showFile', "Show File");
case CopilotToolName.FetchCopilotCliDocumentation: return localize('toolName.fetchCopilotCliDocumentation', "Fetch Documentation");
case CopilotToolName.ProposeWork: return localize('toolName.proposeWork', "Propose Work");
case CopilotToolName.TaskComplete: return localize('toolName.taskComplete', "Task Complete");
case CopilotToolName.AskUser: return localize('toolName.askUser', "Ask User");
case CopilotToolName.Skill: return localize('toolName.invokeSkill', "Invoke Skill");
case CopilotToolName.Task: return localize('toolName.task', "Delegate Task");
case CopilotToolName.ListAgents: return localize('toolName.listAgents', "List Agents");
case CopilotToolName.ReadAgent: return localize('toolName.readAgent', "Read Agent");
case CopilotToolName.ExitPlanMode: return localize('toolName.exitPlanModeFull', "Exit Plan Mode");
case CopilotToolName.Sql: return localize('toolName.sql', "Execute SQL");
case CopilotToolName.Lsp: return localize('toolName.lsp', "Language Server");
case CopilotToolName.CreatePullRequest: return localize('toolName.createPullRequest', "Create Pull Request");
case CopilotToolName.GhAdvisoryDatabase: return localize('toolName.ghAdvisoryDatabase', "Check Dependencies");
case CopilotToolName.StoreMemory: return localize('toolName.storeMemory', "Store Memory");
case CopilotToolName.ParallelValidation: return localize('toolName.parallelValidation', "Validate Changes");
case CopilotToolName.WriteAgent: return localize('toolName.writeAgent', "Write to Agent");
case CopilotToolName.McpReload: return localize('toolName.mcpReload', "Reload MCP Config");
case CopilotToolName.McpValidate: return localize('toolName.mcpValidate', "Validate MCP Config");
case CopilotToolName.ToolSearchToolRegex: return localize('toolName.toolSearchToolRegex', "Search Tools");
default: return toolName;
}
}
export function getInvocationMessage(toolName: string, displayName: string, parameters: Record<string, unknown> | undefined): StringOrMarkdown {
const serverDisplay = getServerToolDisplay(toolName, parameters)?.invocationMessage;
copilotToolDisplay.ts ×14
if (serverDisplay !== undefined) {
}
if (SHELL_TOOL_NAMES.has(toolName)) {
if (args?.command) {
return md(localize('toolInvoke.shellCmd', "Running {0}", appendEscapedMarkdownInlineCode(truncate(firstLine, 80))));
}
return localize('toolInvoke.shell', "Running {0} command", displayName);
copilotToolDisplay.ts ×1
}
if (WRITE_SHELL_TOOL_NAMES.has(toolName)) {
if (args?.command) {
return md(localize('toolInvoke.writeShellCmd', "Sending {0} to shell", appendEscapedMarkdownInlineCode(truncate(firstLine, 80))));
}
}
if (READ_SHELL_TOOL_NAMES.has(toolName)) {
}
switch (toolName) {
case CopilotToolName.View: {
if (args?.path) {
const range = formatViewRange(args.view_range);
if (range) {
return md(localize('toolInvoke.viewFileFromLine', "Reading {0}, line {1} to the end", link, range.startLine));
copilotToolDisplay.ts ×2
}
return md(localize('toolInvoke.viewFileRange', "Reading {0}, lines {1} to {2}", link, range.startLine, range.endLine));
copilotToolDisplay.ts ×2
}
return md(localize('toolInvoke.viewFileLine', "Reading {0}, line {1}", link, range.startLine));
copilotToolDisplay.ts ×2
}
}
}
if (args?.path) {
return md(localize('toolInvoke.editFile', "Editing {0}", formatPathAsMarkdownLink(args.path)));
copilotToolDisplay.ts ×2
}
}
if (args?.path) {
return md(localize('toolInvoke.createFile', "Creating {0}", formatPathAsMarkdownLink(args.path)));
}
return localize('toolInvoke.create', "Creating file");
}
if (args?.pattern) {
return md(localize('toolInvoke.grepPattern', "Searching for {0}", appendEscapedMarkdownInlineCode(truncate(args.pattern, 80))));
}
return localize('toolInvoke.grep', "Searching files");
}
if (args?.pattern) {
return md(localize('toolInvoke.grepPattern', "Searching for {0}", appendEscapedMarkdownInlineCode(truncate(args.pattern, 80))));
copilotToolDisplay.ts ×2
}
}
if (args?.pattern) {
return md(localize('toolInvoke.globPattern', "Finding files matching {0}", appendEscapedMarkdownInlineCode(truncate(args.pattern, 80))));
}
}
case CopilotToolName.GitApplyPatch: {
if (files.length === 1) {
return md(localize('toolInvoke.patchFile', "Editing {0}", formatPathAsMarkdownLink(files[0])));
copilotToolDisplay.ts ×1
}
return md(localize('toolInvoke.patchFiles', "Editing {0}", files.map(formatPathAsMarkdownLink).join(', ')));
copilotToolDisplay.ts ×2
}
}
return args?.description || localize('toolInvoke.sql', "Executing SQL query");
}
if (args?.url) {
return md(localize('toolInvoke.webFetch', "Fetching {0}", formatUrlAsMarkdownLink(args.url)));
copilotToolDisplay.ts ×4
}
}
return localize('toolInvoke.exitPlanMode', "Presenting plan");
// The agent-coordination tools (list/read/write agents) are fast, so
copilotToolDisplay.ts ×14
// they use a single message for both the running and completed states:
// the past-tense phrasing. See getPastTenseMessage.
case CopilotToolName.ListAgents:
case CopilotToolName.ReadAgent:
case CopilotToolName.WriteAgent:
}
export function getPastTenseMessage(toolName: string, displayName: string, parameters: Record<string, unknown> | undefined, success: boolean, resultText?: string): StringOrMarkdown {
return localize('toolComplete.failed', "\"{0}\" failed", displayName);
copilotToolDisplay.ts ×1
}
const serverDisplay = getServerToolDisplay(toolName, parameters, { text: resultText, success })?.pastTenseMessage;
}
if (SHELL_TOOL_NAMES.has(toolName)) {
if (args?.command) {
return md(localize('toolComplete.shellCmd', "Ran {0}", appendEscapedMarkdownInlineCode(truncate(firstLine, 80))));
}
return localize('toolComplete.shell', "Ran {0} command", displayName);
buildSessionEvents.ts ×1
}
if (WRITE_SHELL_TOOL_NAMES.has(toolName)) {
if (args?.command) {
return md(localize('toolComplete.writeShellCmd', "Sent {0} to shell", appendEscapedMarkdownInlineCode(truncate(firstLine, 80))));
}
}
if (READ_SHELL_TOOL_NAMES.has(toolName)) {
}
switch (toolName) {
case CopilotToolName.View: {
if (args?.path) {
const range = formatViewRange(args.view_range);
if (range) {
return md(localize('toolComplete.viewFileFromLine', "Read {0}, line {1} to the end", link, range.startLine));
copilotToolDisplay.ts ×2
}
return md(localize('toolComplete.viewFileRange', "Read {0}, lines {1} to {2}", link, range.startLine, range.endLine));
copilotToolDisplay.ts ×2
}
return md(localize('toolComplete.viewFileLine', "Read {0}, line {1}", link, range.startLine));
copilotToolDisplay.ts ×2
}
}
}
if (args?.path) {
return md(localize('toolComplete.editFile', "Edited {0}", formatPathAsMarkdownLink(args.path)));
}
}
const args = parameters as ICopilotFileToolArgs | undefined;
if (args?.path) {
return md(localize('toolComplete.createFile', "Created {0}", formatPathAsMarkdownLink(args.path)));
}
return localize('toolComplete.create', "Created file");
}
if (args?.pattern) {
return md(localize('toolComplete.grepPattern', "Searched for {0}", appendEscapedMarkdownInlineCode(truncate(args.pattern, 80))));
}
return localize('toolComplete.grep', "Searched files");
}
if (args?.pattern) {
return md(localize('toolComplete.grepPattern', "Searched for {0}", appendEscapedMarkdownInlineCode(truncate(args.pattern, 80))));
}
return localize('toolComplete.grep', "Searched files");
}
if (args?.pattern) {
return md(localize('toolComplete.globPattern', "Found files matching {0}", appendEscapedMarkdownInlineCode(truncate(args.pattern, 80))));
}
}
case CopilotToolName.GitApplyPatch: {
if (files.length === 1) {
return md(localize('toolComplete.patchFile', "Edited {0}", formatPathAsMarkdownLink(files[0])));
copilotToolDisplay.ts ×1
}
return md(localize('toolComplete.patchFiles', "Edited {0}", files.map(formatPathAsMarkdownLink).join(', ')));
copilotToolDisplay.ts ×2
}
}
return args?.description || localize('toolComplete.sql', "Executed SQL query");
}
if (args?.url) {
return md(localize('toolComplete.webFetch', "Fetched {0}", formatUrlAsMarkdownLink(args.url)));
copilotToolDisplay.ts ×4
}
}
return localize('toolComplete.exitPlanMode', "Exited plan mode");
if (agentId) {
return md(localize('toolComplete.readAgent', "Read agent {0}", appendEscapedMarkdownInlineCode(agentId)));
copilotToolDisplay.ts ×2
}
}
if (agentId) {
return md(localize('toolComplete.writeAgent', "Wrote to agent {0}", appendEscapedMarkdownInlineCode(agentId)));
copilotToolDisplay.ts ×2
}
}
}
// =============================================================================
// Skill event synthesis
//
// The Copilot SDK emits a `skill` tool call (which we hide) and, separately, a
// `skill.invoked` lifecycle event with the resolved skill file path. We turn
// the latter into a synthesized tool-start/complete pair so clients can render
// a clickable file link to the SKILL.md the agent loaded -- matching the
// existing `view`-tool display style. Live and replay paths share this helper
// so they stay in lock-step (see also the mirrored-pair gotcha for tool-call
// display in this file).
// =============================================================================
/** Subset of the SDK's `skill.invoked` payload that the synth helper needs. */
export interface ICopilotSkillInvokedData {
readonly name: string;
readonly path?: string;
readonly description?: string;
}
/**
* Builds a stable synthetic tool call id for a `skill.invoked` event so
* reconnect/replay produces the same id as the original live emit. The id
* is used unencoded as a path segment (e.g. by `ChatResponseResource.createUri`),
* so it must not contain characters like `/` -- we hash any fallback values
* that could carry filesystem paths or arbitrary text.
*/
export function getSkillSyntheticToolCallId(eventId: string | undefined, data: ICopilotSkillInvokedData): string {
}
}
/**
* Synthesized data for a `skill.invoked` tool call. Used by both the live
* session handler and the history-replay mapper so the two paths render
* identically. Callers wrap this into protocol actions or {@link Turn}
* data; this helper avoids any agent-protocol coupling.
*/
export interface ISynthesizedSkillToolCall {
readonly toolCallId: string;
readonly toolName: string;
readonly displayName: string;
readonly invocationMessage: StringOrMarkdown;
readonly pastTenseMessage: StringOrMarkdown;
}
/**
* Synthesizes the data for a `skill.invoked` tool call (a tool-start /
* tool-complete pair). Returns the constituent fields without coupling to
* any specific event or action shape — callers compose them into protocol
* actions or {@link Turn} entries as needed.
*/
export function synthesizeSkillToolCall(
eventId: string | undefined,
): ISynthesizedSkillToolCall {
const toolCallId = getSkillSyntheticToolCallId(eventId, data);
const displayName = localize('toolName.skill', "Read Skill");
// Use the skill name as the link text rather than the basename: every skill
// file is named SKILL.md, so `Reading skill [plan]` reads better than the
// always-identical `Reading skill [SKILL.md]`. The client may further upgrade
// this link to a rich pill based on the `SKILL.md` basename. Skill names and
// paths come from the SDK / agent host and are escaped to prevent markdown
// injection from a malicious skill author.
// Escape only the characters that would break out of markdown link text
// syntax (`\` and `]`); a full markdown escape would leave visible
// backslashes in renderers (like the skill pill) that extract link text
// without re-parsing markdown.
const escapedName = escapeMarkdownLinkLabel(data.name);
const skillLink = data.path ? `[${escapedName}](${URI.file(data.path)})` : undefined;
const invocationMessage: StringOrMarkdown = skillLink
? md(localize('toolInvoke.skill', "Reading skill {0}", skillLink))
? md(localize('toolComplete.skill', "Read skill {0}", skillLink))
toolCallId,
toolName: CopilotToolName.Skill,
displayName,
invocationMessage,
pastTenseMessage,
};
}
export function getToolInputString(toolName: string, parameters: Record<string, unknown> | undefined, rawArguments: string | undefined): string | undefined {
}
if (SHELL_TOOL_NAMES.has(toolName) || WRITE_SHELL_TOOL_NAMES.has(toolName)) {
copilotToolDisplay.ts ×6
// Custom tool overrides may wrap the args: { kind: 'custom-tool', args: { command: '...' } }
const command = args?.command ?? (args as Record<string, unknown> | undefined)?.args;
if (typeof command === 'string') {
}
if (typeof command === 'object' && command !== null && hasKey(command, { command: true })) {
copilotToolDisplay.ts ×2
return (command as ICopilotShellToolArgs).command;
}
}
switch (toolName) {
case CopilotToolName.Grep: {
return args?.pattern ?? rawArguments;
}
return args?.pattern ?? rawArguments;
}
return args?.url ?? rawArguments;
}
if (parameters) {
try {
return JSON.stringify(parameters, null, 2);
} catch {
return rawArguments;
}
return rawArguments;
}
/**
* Returns a rendering hint for the given tool. Currently 'terminal', 'subagent',
* and 'search' are supported, which tell the renderer to display the tool with
* a terminal command block, a subagent widget, or a search icon respectively.
*/
export function getToolKind(toolName: string): 'terminal' | 'subagent' | 'search' | undefined {
}
}
}
}
/**
* Extracts subagent metadata (agent name, description) from the parsed
* arguments of a Copilot SDK subagent tool call. The Copilot `task` tool
* uses `agent_type` (snake_case), which this normalizes into the generic
* `subagentAgentName` / `subagentDescription` shape used by the rest of the
* agent host code.
*
* Only call this for tools where {@link getToolKind} returned `'subagent'`.
*/
export function getSubagentMetadata(parameters: Record<string, unknown> | undefined): { agentName?: string; description?: string } {
return {};
}
const agentName = typeof parameters.agent_type === 'string' && parameters.agent_type.length > 0
copilotToolDisplay.ts ×4
const description = typeof parameters.description === 'string' && parameters.description.length > 0
copilotToolDisplay.ts ×4
}
/**
* Returns the shell language identifier for syntax highlighting.
* Used when creating terminal tool-specific data for the renderer.
*/
export function getShellLanguage(toolName: string): string {
case CopilotToolName.PowerShell:
case CopilotToolName.WritePowerShell:
case CopilotToolName.ReadPowerShell: return 'powershell';
default: return 'shellscript';
}
}
// =============================================================================
// Permission display
//
// Derives display fields from SDK permission requests for the tool
// confirmation UI. Colocated with the tool-start display helpers above so
// that formatting utilities (formatPathAsMarkdownLink, md, etc.) are shared.
// =============================================================================
export function tryStringify(value: unknown): string | undefined {
return JSON.stringify(value);
} catch {
return undefined;
}
/**
* Loose, optional-field projection of the SDK's {@link PermissionRequest}
* discriminated union. Lets the rest of the agent host read the well-known
* fields without `switch (request.kind)` narrowing at every access site.
*
* The SDK's `PermissionRequest` (a union with required per-variant fields) is
* structurally assignable to this interface — every variant carries `kind`
* and `toolCallId?`, and the variant-specific fields are listed here as
* optional. Use this type at the agent-host boundary so call sites and tests
* can rely on a single shape.
*/
export interface ITypedPermissionRequest {
/** Permission kind discriminator from the SDK. */
kind: PermissionRequest['kind'];
/** Tool call ID that triggered this permission request, when available. */
toolCallId?: string;
/** File path — set for `read` permission requests. */
path?: string;
/** File path — set for `write` permission requests. */
fileName?: string;
/** Full shell command text — set for `shell` permission requests. */
fullCommandText?: string;
/**
* True when the model requested this `shell` command run outside the
* sandbox (via `requestSandboxBypass`) and the host opted in via
* `sandbox.allowBypass`.
*/
requestSandboxBypass?: boolean;
/** Human-readable intention describing the operation. */
intention?: string;
/** MCP server name — set for `mcp` permission requests. */
serverName?: string;
/** Tool name — set for `mcp` and `custom-tool` permission requests. */
toolName?: string;
/** Tool arguments — set for `custom-tool` permission requests. */
args?: Record<string, unknown>;
/** URL — set for `url` permission requests. */
url?: string;
/** Unified diff of the proposed change — set for `write` permission requests. */
diff?: string;
/** New file contents that will be written — set for `write` permission requests. */
newFileContents?: string;
}
/** Safely extract a string value from an SDK field that may be `unknown` at runtime. */
return typeof value === 'string' ? value : undefined;
}
/**
* Derives display fields from a permission request for the tool confirmation UI.
*/
export function getPermissionDisplay(request: ITypedPermissionRequest, workingDirectory?: URI, isNewFile?: boolean): {
invocationMessage: StringOrMarkdown;
toolInput?: string;
/** Normalized permission kind for auto-approval routing. */
permissionKind: IAgentToolPendingConfirmationSignal['permissionKind'];
/** File path extracted from the request. */
permissionPath?: string;
} {
const path = str(request.path) ?? str(request.fileName);
const fullCommandText = str(request.fullCommandText);
const intention = str(request.intention);
const serverName = str(request.serverName);
const toolName = str(request.toolName);
const shellConfirmationTitle = request.requestSandboxBypass
? localize('copilot.permission.shell.bypass.title', "Run in terminal outside the sandbox?")
copilotToolDisplay.ts ×1
switch (request.kind) {
case 'shell': {
// confirmation dialog shows the simplified command.
const shellParams: Record<string, unknown> | undefined = fullCommandText ? { command: fullCommandText } : undefined;
stripRedundantCdPrefix(CopilotToolName.Bash, shellParams, workingDirectory);
const cleanedCommand = typeof shellParams?.command === 'string' ? shellParams.command : fullCommandText;
return {
confirmationTitle: shellConfirmationTitle,
invocationMessage: intention ?? getInvocationMessage(CopilotToolName.Bash, getToolDisplayName(CopilotToolName.Bash), cleanedCommand ? { command: cleanedCommand } : undefined),
toolInput: cleanedCommand,
permissionKind: 'shell',
permissionPath: path,
};
}
// tool args from the SDK's wrapper envelope.
const args = typeof request.args === 'object' && request.args !== null ? request.args as Record<string, unknown> : undefined;
const sdkToolName = str(request.toolName);
if (args && sdkToolName && isShellTool(sdkToolName) && typeof args.command === 'string') {
const command = args.command as string;
return {
confirmationTitle: shellConfirmationTitle,
invocationMessage: getInvocationMessage(sdkToolName, getToolDisplayName(sdkToolName), { command }),
toolInput: command,
permissionKind: 'shell',
permissionPath: path,
};
}
confirmationTitle: localize('copilot.permission.default.title', "Allow tool call?"),
invocationMessage: md(localize('copilot.permission.default.message', "Allow the model to call {0}?", appendEscapedMarkdownInlineCode(toolName ?? request.kind))),
copilotToolDisplay.ts ×2
toolInput: args ? tryStringify(args) : tryStringify(request),
permissionKind: request.kind,
permissionPath: path,
};
}
const toolName = isNewFile ? CopilotToolName.Create : CopilotToolName.Edit;
copilotToolDisplay.ts ×2
return {
confirmationTitle: isNewFile
invocationMessage: getInvocationMessage(toolName, getToolDisplayName(toolName), path ? { path } : undefined),
copilotToolDisplay.ts ×2
toolInput: tryStringify(path ? { path } : request) ?? undefined,
permissionKind: 'write',
permissionPath: path,
};
}
const title = toolName ?? localize('copilot.permission.mcp.defaultTool', "MCP Tool");
return {
confirmationTitle: serverName
? localize('copilot.permission.mcp.title', "Allow tool from {0}?", serverName)
: localize('copilot.permission.default.title', "Allow tool call?"),
invocationMessage: serverName ? `${serverName}: ${title}` : title,
toolInput: tryStringify({ serverName, toolName }) ?? undefined,
permissionKind: 'mcp',
permissionPath: path,
};
}
confirmationTitle: localize('copilot.permission.read.title', "Allow reading file outside of workspace?"),
invocationMessage: getInvocationMessage(CopilotToolName.View, getToolDisplayName(CopilotToolName.View), path ? { path } : undefined),
permissionKind: 'read',
permissionPath: path,
};
const url = str(request.url);
// Parse through URL for punycode escaping, but preserve the raw value if parsing fails.
const normalizedUrl = url ? (URL.canParse(url) ? new URL(url).href : url) : undefined;
return {
confirmationTitle: localize('copilot.permission.url.title', "Fetch URL?"),
invocationMessage: md(localize('copilot.permission.url.message', "Allow fetching web content?")),
toolInput: normalizedUrl ? JSON.stringify({ url: normalizedUrl }) : undefined,
permissionKind: 'url',
};
}
return {
confirmationTitle: localize('copilot.permission.default.title', "Allow tool call?"),
invocationMessage: md(localize('copilot.permission.default.message', "Allow the model to call {0}?", appendEscapedMarkdownInlineCode(toolName ?? request.kind))),
toolInput: tryStringify(request) ?? undefined,
permissionKind: request.kind,
permissionPath: path,
};
}