Path: blob/main/extensions/copilot/src/extension/chatSessions/claude/common/toolInvocationFormatter.ts
13405 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 { AgentInput, BashInput, FileReadInput, GlobInput, GrepInput } from '@anthropic-ai/claude-agent-sdk/sdk-tools';6import Anthropic from '@anthropic-ai/sdk';7import * as l10n from '@vscode/l10n';8import type { ChatSimpleToolResultData, ChatTerminalToolInvocationData } from 'vscode';9import { URI } from '../../../../util/vs/base/common/uri';10import { ChatSubagentToolInvocationData, ChatToolInvocationPart, MarkdownString } from '../../../../vscodeTypes';11import { ClaudeToolNames, ExitPlanModeInput, LSInput } from './claudeTools';1213// #region Tool Result Content Extraction1415/**16* Extracts text content from a tool result's content field.17* Tool results can be a string, an array of content blocks, or undefined.18*/19function extractToolResultContent(content: Anthropic.Messages.ToolResultBlockParam['content']): string {20if (!content) {21return '';22}23if (typeof content === 'string') {24return content;25}26// Array of content blocks - extract text from each27return content28.filter((block): block is Anthropic.Messages.TextBlockParam => block.type === 'text')29.map(block => block.text)30.join('\n');31}3233// #endregion3435// #region Tool Invocation Completion Handlers3637/**38* Completes a tool invocation by populating toolSpecificData with the result.39* This enables VS Code's chat UI to display tool outputs.40*/41export function completeToolInvocation(42toolUse: Anthropic.Beta.Messages.BetaToolUseBlock,43toolResult: Anthropic.Messages.ToolResultBlockParam,44invocation: ChatToolInvocationPart45): void {46const resultContent = extractToolResultContent(toolResult.content);4748switch (toolUse.name as ClaudeToolNames) {49case ClaudeToolNames.Bash:50completeBashInvocation(invocation, toolUse, resultContent);51break;52case ClaudeToolNames.Read:53case ClaudeToolNames.LS:54completeReadInvocation(invocation, toolUse, resultContent);55break;56case ClaudeToolNames.Glob:57case ClaudeToolNames.Grep:58completeSearchInvocation(invocation, toolUse, resultContent);59break;60case ClaudeToolNames.Edit:61case ClaudeToolNames.MultiEdit:62case ClaudeToolNames.Write:63case ClaudeToolNames.TodoWrite:64// These tools have their own UI handling (edit diffs, todo list)65break;66case ClaudeToolNames.Agent:67case ClaudeToolNames.Task:68completeTaskInvocation(invocation, resultContent);69break;70default:71completeGenericInvocation(invocation, toolUse, resultContent);72break;73}74}7576/**77* Completes a bash tool invocation with terminal-specific output formatting.78* Parses exit code from output and formats for ChatTerminalToolInvocationData.79*/80function completeBashInvocation(81invocation: ChatToolInvocationPart,82toolUse: Anthropic.Beta.Messages.BetaToolUseBlock,83resultContent: string84): void {85// Parse exit code from the end of the result (format: "Exit code: X" or similar patterns)86const exitCodeMatch = /(?:exit code|exited with)[:=\s]*(\d+)/i.exec(resultContent);87const exitCode = exitCodeMatch ? parseInt(exitCodeMatch[1], 10) : undefined;8889// Remove exit code line from output for cleaner display90let text = resultContent;91if (exitCode !== undefined) {92text = resultContent.replace(/(?:exit code|exited with)[:=\s]*\d+\s*$/i, '').trimEnd();93}9495// Convert \n to \r\n for proper terminal display96text = text.replace(/\n/g, '\r\n');9798const toolSpecificData: ChatTerminalToolInvocationData = {99commandLine: {100original: (toolUse.input as BashInput)?.command ?? '',101},102language: 'bash',103state: exitCode !== undefined ? { exitCode } : undefined,104output: text ? { text } : undefined105};106invocation.toolSpecificData = toolSpecificData;107}108109/**110* Completes a read/ls tool invocation with simple input/output display.111*/112function completeReadInvocation(113invocation: ChatToolInvocationPart,114toolUse: Anthropic.Beta.Messages.BetaToolUseBlock,115resultContent: string116): void {117if (!resultContent) {118return;119}120121const input = toolUse.name === ClaudeToolNames.LS122? (toolUse.input as LSInput)?.path ?? ''123: (toolUse.input as FileReadInput)?.file_path ?? '';124125const toolSpecificData: ChatSimpleToolResultData = {126input,127output: resultContent128};129invocation.toolSpecificData = toolSpecificData;130}131132/**133* Completes a glob/grep tool invocation with simple input/output display.134*/135function completeSearchInvocation(136invocation: ChatToolInvocationPart,137toolUse: Anthropic.Beta.Messages.BetaToolUseBlock,138resultContent: string139): void {140if (!resultContent) {141return;142}143144const input = toolUse.name === ClaudeToolNames.Glob145? (toolUse.input as GlobInput)?.pattern ?? ''146: (toolUse.input as GrepInput)?.pattern ?? '';147148const toolSpecificData: ChatSimpleToolResultData = {149input,150output: resultContent151};152invocation.toolSpecificData = toolSpecificData;153}154155/**156* Completes a Task tool invocation by setting the result on its ChatSubagentToolInvocationData.157* The toolSpecificData was already populated by formatTaskInvocation with description/agentName/prompt;158* this adds the result text from the subagent's execution.159*/160function completeTaskInvocation(161invocation: ChatToolInvocationPart,162resultContent: string163): void {164if (invocation.toolSpecificData instanceof ChatSubagentToolInvocationData) {165invocation.toolSpecificData.result = resultContent;166}167}168169/**170* Generic completion handler for tools without specific formatting.171* Displays input arguments and output as plain text.172*/173function completeGenericInvocation(174invocation: ChatToolInvocationPart,175toolUse: Anthropic.Beta.Messages.BetaToolUseBlock,176resultContent: string177): void {178if (!resultContent) {179return;180}181182const toolSpecificData: ChatSimpleToolResultData = {183input: toolUse.input ? JSON.stringify(toolUse.input, null, 2) : '',184output: resultContent185};186invocation.toolSpecificData = toolSpecificData;187}188189// #endregion190191// #region Tool Invocation Creation192193/**194* Creates a formatted tool invocation part based on the tool type and input195*/196export function createFormattedToolInvocation(197toolUse: Anthropic.Beta.Messages.BetaToolUseBlock,198complete?: boolean199): ChatToolInvocationPart | undefined {200const invocation = new ChatToolInvocationPart(toolUse.name, toolUse.id);201if (complete !== undefined) {202invocation.isConfirmed = complete;203invocation.isComplete = complete;204}205206switch (toolUse.name as ClaudeToolNames) {207case ClaudeToolNames.Bash:208formatBashInvocation(invocation, toolUse);209break;210case ClaudeToolNames.Read:211formatReadInvocation(invocation, toolUse);212break;213case ClaudeToolNames.Glob:214formatGlobInvocation(invocation, toolUse);215break;216case ClaudeToolNames.Grep:217formatGrepInvocation(invocation, toolUse);218break;219case ClaudeToolNames.LS:220formatLSInvocation(invocation, toolUse);221break;222case ClaudeToolNames.Edit:223case ClaudeToolNames.MultiEdit:224case ClaudeToolNames.Write:225return; // edit diff is shown226case ClaudeToolNames.ExitPlanMode:227formatExitPlanModeInvocation(invocation, toolUse);228break;229case ClaudeToolNames.Agent:230case ClaudeToolNames.Task:231formatTaskInvocation(invocation, toolUse);232break;233case ClaudeToolNames.TodoWrite:234// Suppress this, it's too common235return;236default:237formatGenericInvocation(invocation, toolUse);238break;239}240241return invocation;242}243244function formatBashInvocation(invocation: ChatToolInvocationPart, toolUse: Anthropic.Beta.Messages.BetaToolUseBlock): void {245invocation.invocationMessage = '';246invocation.toolSpecificData = {247commandLine: {248original: (toolUse.input as BashInput)?.command,249},250language: 'bash'251};252}253254function formatReadInvocation(invocation: ChatToolInvocationPart, toolUse: Anthropic.Beta.Messages.BetaToolUseBlock): void {255const filePath: string = (toolUse.input as FileReadInput)?.file_path ?? '';256const display = filePath ? formatUriForMessage(filePath) : '';257invocation.invocationMessage = new MarkdownString(l10n.t("Read {0}", display));258}259260function formatGlobInvocation(invocation: ChatToolInvocationPart, toolUse: Anthropic.Beta.Messages.BetaToolUseBlock): void {261const pattern: string = (toolUse.input as GlobInput)?.pattern ?? '';262invocation.invocationMessage = new MarkdownString(l10n.t("Searched for files matching `{0}`", pattern));263}264265function formatGrepInvocation(invocation: ChatToolInvocationPart, toolUse: Anthropic.Beta.Messages.BetaToolUseBlock): void {266const pattern: string = (toolUse.input as GrepInput)?.pattern ?? '';267invocation.invocationMessage = new MarkdownString(l10n.t("Searched for regex `{0}`", pattern));268}269270function formatLSInvocation(invocation: ChatToolInvocationPart, toolUse: Anthropic.Beta.Messages.BetaToolUseBlock): void {271const path: string = (toolUse.input as LSInput)?.path ?? '';272const display = path ? formatUriForMessage(path) : '';273invocation.invocationMessage = new MarkdownString(l10n.t("Read {0}", display));274}275276function formatExitPlanModeInvocation(invocation: ChatToolInvocationPart, toolUse: Anthropic.Beta.Messages.BetaToolUseBlock): void {277invocation.invocationMessage = l10n.t("Here is Claude's plan:\n\n{0}", (toolUse.input as ExitPlanModeInput)?.plan ?? '');278}279280function formatTaskInvocation(invocation: ChatToolInvocationPart, toolUse: Anthropic.Beta.Messages.BetaToolUseBlock): void {281const description = (toolUse.input as AgentInput)?.description ?? '';282invocation.invocationMessage = new MarkdownString(l10n.t("Completed Task: \"{0}\"", description));283284const input = toolUse.input as AgentInput;285invocation.toolSpecificData = new ChatSubagentToolInvocationData(286input.description,287input.subagent_type,288input.prompt);289}290291function formatGenericInvocation(invocation: ChatToolInvocationPart, toolUse: Anthropic.Beta.Messages.BetaToolUseBlock): void {292invocation.invocationMessage = l10n.t("Used tool: {0}", toolUse.name);293}294295function formatUriForMessage(path: string): string {296return `[](${URI.file(path).toString()})`;297}298299// #endregion300301302