Path: blob/main/src/vs/platform/agentHost/node/copilot/copilotToolDisplay.ts
13399 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import type { PermissionRequest } from '@github/copilot-sdk';6import { hasKey } from '../../../../base/common/types.js';7import { URI } from '../../../../base/common/uri.js';8import { appendEscapedMarkdownInlineCode, escapeMarkdownLinkLabel } from '../../../../base/common/htmlContent.js';9import { hash } from '../../../../base/common/hash.js';10import { localize } from '../../../../nls.js';11import type { IAgentToolPendingConfirmationSignal } from '../../common/agentService.js';12import { stripRedundantCdPrefix } from '../../common/commandLineHelpers.js';13import { StringOrMarkdown } from '../../common/state/protocol/state.js';14import { basename } from '../../../../base/common/resources.js';1516// =============================================================================17// Copilot CLI built-in tool interfaces18//19// The Copilot CLI (via @github/copilot) exposes these built-in tools. Tool names20// and parameter shapes are not typed in the SDK -- they come from the CLI server21// as plain strings. These interfaces are derived from observing the CLI's actual22// tool events and the ShellConfig class in @github/copilot.23//24// Shell tool names follow a pattern per ShellConfig:25// shellToolName, readShellToolName, writeShellToolName,26// stopShellToolName, listShellsToolName27// For bash: bash, read_bash, write_bash, bash_shutdown, list_bash28// For powershell: powershell, read_powershell, write_powershell, list_powershell29// =============================================================================3031/**32* Known Copilot CLI tool names. These are the `toolName` values that appear33* in `tool.execution_start` events from the SDK.34*/35const enum CopilotToolName {36Bash = 'bash',37ReadBash = 'read_bash',38WriteBash = 'write_bash',39BashShutdown = 'bash_shutdown',40ListBash = 'list_bash',4142PowerShell = 'powershell',43ReadPowerShell = 'read_powershell',44WritePowerShell = 'write_powershell',45ListPowerShell = 'list_powershell',4647View = 'view',48Edit = 'edit',49Create = 'create',50Grep = 'grep',51Glob = 'glob',52ApplyPatch = 'apply_patch',53GitApplyPatch = 'git_apply_patch',54WebSearch = 'web_search',55WebFetch = 'web_fetch',56AskUser = 'ask_user',57ReportIntent = 'report_intent',58Skill = 'skill',59ExitPlanMode = 'exit_plan_mode',60}6162/** Parameters for the `bash` / `powershell` shell tools. */63interface ICopilotShellToolArgs {64command: string;65timeout?: number;66}6768/** Parameters for file tools (`view`, `edit`, `create`). */69interface ICopilotFileToolArgs {70path: string;71}7273/**74* Parameters for the `view` tool. The Copilot CLI accepts an optional75* `view_range: [startLine, endLine]` (1-based, inclusive). `endLine` may be76* `-1` to mean "to end of file".77*/78interface ICopilotViewToolArgs extends ICopilotFileToolArgs {79view_range?: number[];80}8182/**83* Normalizes a `view_range` array. Returns `undefined` unless the array has84* exactly two integer elements with `startLine >= 0`. `endLine === -1` is85* preserved as the "to end of file" sentinel; otherwise `endLine` must be86* `>= startLine`.87*/88function formatViewRange(view_range: number[] | undefined): { startLine: number; endLine: number } | undefined {89if (!Array.isArray(view_range) || view_range.length !== 2) {90return undefined;91}92const [startLine, endLine] = view_range;93if (!Number.isInteger(startLine) || !Number.isInteger(endLine)) {94return undefined;95}96if (startLine < 0) {97return undefined;98}99if (endLine !== -1 && endLine < startLine) {100return undefined;101}102return { startLine, endLine };103}104105/** Parameters for the `grep` tool. */106interface ICopilotGrepToolArgs {107pattern: string;108path?: string;109include?: string;110}111112/** Parameters for the `glob` tool. */113interface ICopilotGlobToolArgs {114pattern: string;115path?: string;116}117118/** Set of tool names that perform file edits. */119const EDIT_TOOL_NAMES: ReadonlySet<string> = new Set([120CopilotToolName.Edit,121CopilotToolName.Create,122CopilotToolName.ApplyPatch,123CopilotToolName.GitApplyPatch,124]);125126/**127* Returns true if the tool modifies files on disk.128*/129export function isEditTool(toolName: string): boolean {130return EDIT_TOOL_NAMES.has(toolName);131}132133/**134* Extracts the target file path from an edit tool's parameters, if available.135*/136export function getEditFilePath(parameters: unknown): string | undefined {137if (typeof parameters === 'string') {138try {139parameters = JSON.parse(parameters);140} catch {141return undefined;142}143}144145const args = parameters as ICopilotFileToolArgs | undefined;146return args?.path;147}148149/** Set of tool names that execute shell commands (bash or powershell). */150const SHELL_TOOL_NAMES: ReadonlySet<string> = new Set([151CopilotToolName.Bash,152CopilotToolName.PowerShell,153]);154155/** Set of tool names that write input to an interactive shell session. */156const WRITE_SHELL_TOOL_NAMES: ReadonlySet<string> = new Set([157CopilotToolName.WriteBash,158CopilotToolName.WritePowerShell,159]);160161/** Set of tool names that read output from an interactive shell session. */162const READ_SHELL_TOOL_NAMES: ReadonlySet<string> = new Set([163CopilotToolName.ReadBash,164CopilotToolName.ReadPowerShell,165]);166167/** Set of tool names that spawn subagent sessions. */168const SUBAGENT_TOOL_NAMES: ReadonlySet<string> = new Set([169'task',170]);171172/**173* Tools that should not be shown to the user. These are internal tools174* used by the CLI for its own purposes (e.g., reporting intent to the model).175*176* `skill` is hidden because the SDK already emits a richer `skill.invoked`177* lifecycle event with the resolved skill file path; the agent session178* synthesizes a tool-start/complete pair from that event so the UI can179* render a clickable file link instead of just the skill name. See180* {@link synthesizeSkillToolCall}.181*/182const HIDDEN_TOOL_NAMES: ReadonlySet<string> = new Set([183CopilotToolName.ReportIntent,184CopilotToolName.Skill,185]);186187/**188* Returns true if the tool should be hidden from the UI.189*/190export function isHiddenTool(toolName: string): boolean {191return HIDDEN_TOOL_NAMES.has(toolName);192}193194/**195* Returns true if the tool executes shell commands.196*/197export function isShellTool(toolName: string): boolean {198return SHELL_TOOL_NAMES.has(toolName);199}200201// =============================================================================202// Display helpers203//204// These functions translate Copilot CLI tool names and arguments into205// human-readable display strings. This logic lives here -- in the agent-host206// process -- so the IPC protocol stays agent-agnostic; the renderer never needs207// to know about specific tool names.208// =============================================================================209210function truncate(text: string, maxLength: number): string {211return text.length > maxLength ? text.substring(0, maxLength - 3) + '...' : text;212}213214/**215* Formats a file path as a markdown link `[](file-uri)` so it renders216* as a clickable file widget in the chat UI.217*/218function formatPathAsMarkdownLink(path: string): string {219const uri = URI.file(path);220return `[${basename(uri)}](${uri})`;221}222223/**224* Wraps a localized message containing a markdown file link into a225* `StringOrMarkdown` object so the renderer treats it as markdown.226*/227function md(value: string): StringOrMarkdown {228return { markdown: value };229}230231export function getToolDisplayName(toolName: string): string {232switch (toolName) {233case CopilotToolName.Bash: return localize('toolName.bash', "Bash");234case CopilotToolName.PowerShell: return localize('toolName.powershell', "PowerShell");235case CopilotToolName.ReadBash:236case CopilotToolName.ReadPowerShell: return localize('toolName.readShell', "Read Shell Output");237case CopilotToolName.WriteBash:238case CopilotToolName.WritePowerShell: return localize('toolName.writeShell', "Write Shell Input");239case CopilotToolName.BashShutdown: return localize('toolName.bashShutdown', "Stop Shell");240case CopilotToolName.ListBash:241case CopilotToolName.ListPowerShell: return localize('toolName.listShells', "List Shells");242case CopilotToolName.View: return localize('toolName.view', "View File");243case CopilotToolName.Edit: return localize('toolName.edit', "Edit File");244case CopilotToolName.Create: return localize('toolName.create', "Create File");245case CopilotToolName.Grep: return localize('toolName.grep', "Search");246case CopilotToolName.Glob: return localize('toolName.glob', "Find Files");247case CopilotToolName.ApplyPatch:248case CopilotToolName.GitApplyPatch: return localize('toolName.patch', "Patch");249case CopilotToolName.WebSearch: return localize('toolName.webSearch', "Web Search");250case CopilotToolName.WebFetch: return localize('toolName.webFetch', "Web Fetch");251case CopilotToolName.AskUser: return localize('toolName.askUser', "Ask User");252case CopilotToolName.ExitPlanMode: return localize('toolName.exitPlanMode', "Plan");253default: return toolName;254}255}256257export function getInvocationMessage(toolName: string, displayName: string, parameters: Record<string, unknown> | undefined): StringOrMarkdown {258if (SHELL_TOOL_NAMES.has(toolName)) {259const args = parameters as ICopilotShellToolArgs | undefined;260if (args?.command) {261const firstLine = args.command.split('\n')[0];262return md(localize('toolInvoke.shellCmd', "Running {0}", appendEscapedMarkdownInlineCode(truncate(firstLine, 80))));263}264return localize('toolInvoke.shell', "Running {0} command", displayName);265}266267if (WRITE_SHELL_TOOL_NAMES.has(toolName)) {268const args = parameters as ICopilotShellToolArgs | undefined;269if (args?.command) {270const firstLine = args.command.split('\n')[0];271return md(localize('toolInvoke.writeShellCmd', "Sending {0} to shell", appendEscapedMarkdownInlineCode(truncate(firstLine, 80))));272}273return localize('toolInvoke.writeShell', "Sending input to shell");274}275276if (READ_SHELL_TOOL_NAMES.has(toolName)) {277return localize('toolInvoke.readShell', "Reading shell output");278}279280switch (toolName) {281case CopilotToolName.View: {282const args = parameters as ICopilotViewToolArgs | undefined;283if (args?.path) {284const link = formatPathAsMarkdownLink(args.path);285const range = formatViewRange(args.view_range);286if (range) {287if (range.endLine === -1) {288return md(localize('toolInvoke.viewFileFromLine', "Reading {0}, line {1} to the end", link, range.startLine));289}290if (range.endLine !== range.startLine) {291return md(localize('toolInvoke.viewFileRange', "Reading {0}, lines {1} to {2}", link, range.startLine, range.endLine));292}293return md(localize('toolInvoke.viewFileLine', "Reading {0}, line {1}", link, range.startLine));294}295return md(localize('toolInvoke.viewFile', "Reading {0}", link));296}297return localize('toolInvoke.view', "Reading file");298}299case CopilotToolName.Edit: {300const args = parameters as ICopilotFileToolArgs | undefined;301if (args?.path) {302return md(localize('toolInvoke.editFile', "Editing {0}", formatPathAsMarkdownLink(args.path)));303}304return localize('toolInvoke.edit', "Editing file");305}306case CopilotToolName.Create: {307const args = parameters as ICopilotFileToolArgs | undefined;308if (args?.path) {309return md(localize('toolInvoke.createFile', "Creating {0}", formatPathAsMarkdownLink(args.path)));310}311return localize('toolInvoke.create', "Creating file");312}313case CopilotToolName.Grep: {314const args = parameters as ICopilotGrepToolArgs | undefined;315if (args?.pattern) {316return md(localize('toolInvoke.grepPattern', "Searching for {0}", appendEscapedMarkdownInlineCode(truncate(args.pattern, 80))));317}318return localize('toolInvoke.grep', "Searching files");319}320case CopilotToolName.Glob: {321const args = parameters as ICopilotGlobToolArgs | undefined;322if (args?.pattern) {323return md(localize('toolInvoke.globPattern', "Finding files matching {0}", appendEscapedMarkdownInlineCode(truncate(args.pattern, 80))));324}325return localize('toolInvoke.glob', "Finding files");326}327case CopilotToolName.ExitPlanMode:328return localize('toolInvoke.exitPlanMode', "Presenting plan");329default:330return localize('toolInvoke.generic', "Using \"{0}\"", displayName);331}332}333334export function getPastTenseMessage(toolName: string, displayName: string, parameters: Record<string, unknown> | undefined, success: boolean): StringOrMarkdown {335if (!success) {336return localize('toolComplete.failed', "\"{0}\" failed", displayName);337}338339if (SHELL_TOOL_NAMES.has(toolName)) {340const args = parameters as ICopilotShellToolArgs | undefined;341if (args?.command) {342const firstLine = args.command.split('\n')[0];343return md(localize('toolComplete.shellCmd', "Ran {0}", appendEscapedMarkdownInlineCode(truncate(firstLine, 80))));344}345return localize('toolComplete.shell', "Ran {0} command", displayName);346}347348if (WRITE_SHELL_TOOL_NAMES.has(toolName)) {349const args = parameters as ICopilotShellToolArgs | undefined;350if (args?.command) {351const firstLine = args.command.split('\n')[0];352return md(localize('toolComplete.writeShellCmd', "Sent {0} to shell", appendEscapedMarkdownInlineCode(truncate(firstLine, 80))));353}354return localize('toolComplete.writeShell', "Sent input to shell");355}356357if (READ_SHELL_TOOL_NAMES.has(toolName)) {358return localize('toolComplete.readShell', "Read shell output");359}360361switch (toolName) {362case CopilotToolName.View: {363const args = parameters as ICopilotViewToolArgs | undefined;364if (args?.path) {365const link = formatPathAsMarkdownLink(args.path);366const range = formatViewRange(args.view_range);367if (range) {368if (range.endLine === -1) {369return md(localize('toolComplete.viewFileFromLine', "Read {0}, line {1} to the end", link, range.startLine));370}371if (range.endLine !== range.startLine) {372return md(localize('toolComplete.viewFileRange', "Read {0}, lines {1} to {2}", link, range.startLine, range.endLine));373}374return md(localize('toolComplete.viewFileLine', "Read {0}, line {1}", link, range.startLine));375}376return md(localize('toolComplete.viewFile', "Read {0}", link));377}378return localize('toolComplete.view', "Read file");379}380case CopilotToolName.Edit: {381const args = parameters as ICopilotFileToolArgs | undefined;382if (args?.path) {383return md(localize('toolComplete.editFile', "Edited {0}", formatPathAsMarkdownLink(args.path)));384}385return localize('toolComplete.edit', "Edited file");386}387case CopilotToolName.Create: {388const args = parameters as ICopilotFileToolArgs | undefined;389if (args?.path) {390return md(localize('toolComplete.createFile', "Created {0}", formatPathAsMarkdownLink(args.path)));391}392return localize('toolComplete.create', "Created file");393}394case CopilotToolName.Grep: {395const args = parameters as ICopilotGrepToolArgs | undefined;396if (args?.pattern) {397return md(localize('toolComplete.grepPattern', "Searched for {0}", appendEscapedMarkdownInlineCode(truncate(args.pattern, 80))));398}399return localize('toolComplete.grep', "Searched files");400}401case CopilotToolName.Glob: {402const args = parameters as ICopilotGlobToolArgs | undefined;403if (args?.pattern) {404return md(localize('toolComplete.globPattern', "Found files matching {0}", appendEscapedMarkdownInlineCode(truncate(args.pattern, 80))));405}406return localize('toolComplete.glob', "Found files");407}408case CopilotToolName.ExitPlanMode:409return localize('toolComplete.exitPlanMode', "Exited plan mode");410default:411return localize('toolComplete.generic', "Used \"{0}\"", displayName);412}413}414415// =============================================================================416// Skill event synthesis417//418// The Copilot SDK emits a `skill` tool call (which we hide) and, separately, a419// `skill.invoked` lifecycle event with the resolved skill file path. We turn420// the latter into a synthesized tool-start/complete pair so clients can render421// a clickable file link to the SKILL.md the agent loaded -- matching the422// existing `view`-tool display style. Live and replay paths share this helper423// so they stay in lock-step (see also the mirrored-pair gotcha for tool-call424// display in this file).425// =============================================================================426427/** Subset of the SDK's `skill.invoked` payload that the synth helper needs. */428export interface ICopilotSkillInvokedData {429readonly name: string;430readonly path?: string;431readonly description?: string;432}433434/**435* Builds a stable synthetic tool call id for a `skill.invoked` event so436* reconnect/replay produces the same id as the original live emit. The id437* is used unencoded as a path segment (e.g. by `ChatResponseResource.createUri`),438* so it must not contain characters like `/` -- we hash any fallback values439* that could carry filesystem paths or arbitrary text.440*/441export function getSkillSyntheticToolCallId(eventId: string | undefined, data: ICopilotSkillInvokedData): string {442if (eventId) {443return `synth-skill-${eventId}`;444}445const seed = data.path ?? data.name;446return `synth-skill-${hash(seed).toString(16)}`;447}448449/**450* Synthesized data for a `skill.invoked` tool call. Used by both the live451* session handler and the history-replay mapper so the two paths render452* identically. Callers wrap this into protocol actions or {@link Turn}453* data; this helper avoids any agent-protocol coupling.454*/455export interface ISynthesizedSkillToolCall {456readonly toolCallId: string;457readonly toolName: string;458readonly displayName: string;459readonly invocationMessage: StringOrMarkdown;460readonly pastTenseMessage: StringOrMarkdown;461}462463/**464* Synthesizes the data for a `skill.invoked` tool call (a tool-start /465* tool-complete pair). Returns the constituent fields without coupling to466* any specific event or action shape — callers compose them into protocol467* actions or {@link Turn} entries as needed.468*/469export function synthesizeSkillToolCall(470data: ICopilotSkillInvokedData,471eventId: string | undefined,472): ISynthesizedSkillToolCall {473const toolCallId = getSkillSyntheticToolCallId(eventId, data);474const displayName = localize('toolName.skill', "Read Skill");475// Use the skill name as the link text rather than the basename: every skill476// file is named SKILL.md, so `Reading skill [plan]` reads better than the477// always-identical `Reading skill [SKILL.md]`. The client may further upgrade478// this link to a rich pill based on the `SKILL.md` basename. Skill names and479// paths come from the SDK / agent host and are escaped to prevent markdown480// injection from a malicious skill author.481// Escape only the characters that would break out of markdown link text482// syntax (`\` and `]`); a full markdown escape would leave visible483// backslashes in renderers (like the skill pill) that extract link text484// without re-parsing markdown.485const escapedName = escapeMarkdownLinkLabel(data.name);486const skillLink = data.path ? `[${escapedName}](${URI.file(data.path)})` : undefined;487const invocationMessage: StringOrMarkdown = skillLink488? md(localize('toolInvoke.skill', "Reading skill {0}", skillLink))489: localize('toolInvoke.skillName', "Reading skill {0}", data.name);490const pastTenseMessage: StringOrMarkdown = skillLink491? md(localize('toolComplete.skill', "Read skill {0}", skillLink))492: localize('toolComplete.skillName', "Read skill {0}", data.name);493return {494toolCallId,495toolName: CopilotToolName.Skill,496displayName,497invocationMessage,498pastTenseMessage,499};500}501502export function getToolInputString(toolName: string, parameters: Record<string, unknown> | undefined, rawArguments: string | undefined): string | undefined {503if (!parameters && !rawArguments) {504return undefined;505}506507if (SHELL_TOOL_NAMES.has(toolName) || WRITE_SHELL_TOOL_NAMES.has(toolName)) {508const args = parameters as ICopilotShellToolArgs | undefined;509// Custom tool overrides may wrap the args: { kind: 'custom-tool', args: { command: '...' } }510const command = args?.command ?? (args as Record<string, unknown> | undefined)?.args;511if (typeof command === 'string') {512return command;513}514if (typeof command === 'object' && command !== null && hasKey(command, { command: true })) {515return (command as ICopilotShellToolArgs).command;516}517return rawArguments;518}519520switch (toolName) {521case CopilotToolName.Grep: {522const args = parameters as ICopilotGrepToolArgs | undefined;523return args?.pattern ?? rawArguments;524}525default:526// For other tools, show the formatted JSON arguments527if (parameters) {528try {529return JSON.stringify(parameters, null, 2);530} catch {531return rawArguments;532}533}534return rawArguments;535}536}537538/**539* Returns a rendering hint for the given tool. Currently only 'terminal' is540* supported, which tells the renderer to display the tool as a terminal command541* block.542*/543export function getToolKind(toolName: string): 'terminal' | 'subagent' | undefined {544if (SHELL_TOOL_NAMES.has(toolName)) {545return 'terminal';546}547if (SUBAGENT_TOOL_NAMES.has(toolName)) {548return 'subagent';549}550return undefined;551}552553/**554* Extracts subagent metadata (agent name, description) from the parsed555* arguments of a Copilot SDK subagent tool call. The Copilot `task` tool556* uses `agent_type` (snake_case), which this normalizes into the generic557* `subagentAgentName` / `subagentDescription` shape used by the rest of the558* agent host code.559*560* Only call this for tools where {@link getToolKind} returned `'subagent'`.561*/562export function getSubagentMetadata(parameters: Record<string, unknown> | undefined): { agentName?: string; description?: string } {563if (!parameters) {564return {};565}566const agentName = typeof parameters.agent_type === 'string' && parameters.agent_type.length > 0567? parameters.agent_type568: undefined;569const description = typeof parameters.description === 'string' && parameters.description.length > 0570? parameters.description571: undefined;572return { agentName, description };573}574575/**576* Returns the shell language identifier for syntax highlighting.577* Used when creating terminal tool-specific data for the renderer.578*/579export function getShellLanguage(toolName: string): string {580switch (toolName) {581case CopilotToolName.PowerShell:582case CopilotToolName.WritePowerShell:583case CopilotToolName.ReadPowerShell: return 'powershell';584default: return 'shellscript';585}586}587588// =============================================================================589// Permission display590//591// Derives display fields from SDK permission requests for the tool592// confirmation UI. Colocated with the tool-start display helpers above so593// that formatting utilities (formatPathAsMarkdownLink, md, etc.) are shared.594// =============================================================================595596export function tryStringify(value: unknown): string | undefined {597try {598return JSON.stringify(value);599} catch {600return undefined;601}602}603604/**605* Extends the SDK's {@link PermissionRequest} with the known extra properties606* that arrive on the index-signature. The SDK defines these as `[key: string]: unknown`607* so this interface adds proper types for the fields we actually use.608*/609export interface ITypedPermissionRequest extends PermissionRequest {610/** File path — set for `read` permission requests. */611path?: string;612/** File path — set for `write` permission requests. */613fileName?: string;614/** Full shell command text — set for `shell` permission requests. */615fullCommandText?: string;616/** Human-readable intention describing the operation. */617intention?: string;618/** MCP server name — set for `mcp` permission requests. */619serverName?: string;620/** Tool name — set for `mcp` and `custom-tool` permission requests. */621toolName?: string;622/** Tool arguments — set for `custom-tool` permission requests. */623args?: Record<string, unknown>;624/** URL — set for `url` permission requests. */625url?: string;626/** Unified diff of the proposed change — set for `write` permission requests. */627diff?: string;628/** New file contents that will be written — set for `write` permission requests. */629newFileContents?: string;630}631632/** Safely extract a string value from an SDK field that may be `unknown` at runtime. */633function str(value: unknown): string | undefined {634return typeof value === 'string' ? value : undefined;635}636637/**638* Derives display fields from a permission request for the tool confirmation UI.639*/640export function getPermissionDisplay(request: ITypedPermissionRequest, workingDirectory?: URI): {641confirmationTitle: string;642invocationMessage: StringOrMarkdown;643toolInput?: string;644/** Normalized permission kind for auto-approval routing. */645permissionKind: IAgentToolPendingConfirmationSignal['permissionKind'];646/** File path extracted from the request. */647permissionPath?: string;648} {649const path = str(request.path) ?? str(request.fileName);650const fullCommandText = str(request.fullCommandText);651const intention = str(request.intention);652const serverName = str(request.serverName);653const toolName = str(request.toolName);654655switch (request.kind) {656case 'shell': {657// Strip a redundant `cd <workingDirectory> && …` prefix so the658// confirmation dialog shows the simplified command.659const shellParams: Record<string, unknown> | undefined = fullCommandText ? { command: fullCommandText } : undefined;660stripRedundantCdPrefix(CopilotToolName.Bash, shellParams, workingDirectory);661const cleanedCommand = typeof shellParams?.command === 'string' ? shellParams.command : fullCommandText;662return {663confirmationTitle: localize('copilot.permission.shell.title', "Run in terminal?"),664invocationMessage: intention ?? getInvocationMessage(CopilotToolName.Bash, getToolDisplayName(CopilotToolName.Bash), cleanedCommand ? { command: cleanedCommand } : undefined),665toolInput: cleanedCommand,666permissionKind: 'shell',667permissionPath: path,668};669}670case 'custom-tool': {671// Custom tool overrides (e.g. our shell tool). Extract the actual672// tool args from the SDK's wrapper envelope.673const args = typeof request.args === 'object' && request.args !== null ? request.args as Record<string, unknown> : undefined;674const sdkToolName = str(request.toolName);675if (args && sdkToolName && isShellTool(sdkToolName) && typeof args.command === 'string') {676stripRedundantCdPrefix(sdkToolName, args, workingDirectory);677const command = args.command as string;678return {679confirmationTitle: localize('copilot.permission.shell.title', "Run in terminal?"),680invocationMessage: getInvocationMessage(sdkToolName, getToolDisplayName(sdkToolName), { command }),681toolInput: command,682permissionKind: 'shell',683permissionPath: path,684};685}686return {687confirmationTitle: localize('copilot.permission.default.title', "Allow tool call?"),688invocationMessage: md(localize('copilot.permission.default.message', "Allow the model to call {0}?", appendEscapedMarkdownInlineCode(toolName ?? request.kind))),689toolInput: args ? tryStringify(args) : tryStringify(request),690permissionKind: request.kind,691permissionPath: path,692};693}694case 'write':695return {696confirmationTitle: localize('copilot.permission.write.title', "Write file?"),697invocationMessage: getInvocationMessage(CopilotToolName.Edit, getToolDisplayName(CopilotToolName.Edit), path ? { path } : undefined),698toolInput: tryStringify(path ? { path } : request) ?? undefined,699permissionKind: 'write',700permissionPath: path,701};702case 'mcp': {703const title = toolName ?? localize('copilot.permission.mcp.defaultTool', "MCP Tool");704return {705confirmationTitle: serverName706? localize('copilot.permission.mcp.title', "Allow tool from {0}?", serverName)707: localize('copilot.permission.default.title', "Allow tool call?"),708invocationMessage: serverName ? `${serverName}: ${title}` : title,709toolInput: tryStringify({ serverName, toolName }) ?? undefined,710permissionKind: 'mcp',711permissionPath: path,712};713}714case 'read':715return {716confirmationTitle: localize('copilot.permission.read.title', "Read file?"),717invocationMessage: intention ?? getInvocationMessage(CopilotToolName.View, getToolDisplayName(CopilotToolName.View), path ? { path } : undefined),718toolInput: tryStringify(path ? { path, intention } : request) ?? undefined,719permissionKind: 'read',720permissionPath: path,721};722case 'url': {723const url = str(request.url);724// Parse through URL for punycode escaping, but preserve the raw value if parsing fails.725const normalizedUrl = url ? (URL.canParse(url) ? new URL(url).href : url) : undefined;726return {727confirmationTitle: localize('copilot.permission.url.title', "Fetch URL?"),728invocationMessage: md(localize('copilot.permission.url.message', "Allow fetching web content?")),729toolInput: normalizedUrl ? JSON.stringify({ url: normalizedUrl }) : undefined,730permissionKind: 'url',731};732}733default:734return {735confirmationTitle: localize('copilot.permission.default.title', "Allow tool call?"),736invocationMessage: md(localize('copilot.permission.default.message', "Allow the model to call {0}?", appendEscapedMarkdownInlineCode(toolName ?? request.kind))),737toolInput: tryStringify(request) ?? undefined,738permissionKind: request.kind,739permissionPath: path,740};741}742}743744745