Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/extension/chatSessions/claude/common/toolInvocationFormatter.ts
13405 views
1
/*---------------------------------------------------------------------------------------------
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 { AgentInput, BashInput, FileReadInput, GlobInput, GrepInput } from '@anthropic-ai/claude-agent-sdk/sdk-tools';
7
import Anthropic from '@anthropic-ai/sdk';
8
import * as l10n from '@vscode/l10n';
9
import type { ChatSimpleToolResultData, ChatTerminalToolInvocationData } from 'vscode';
10
import { URI } from '../../../../util/vs/base/common/uri';
11
import { ChatSubagentToolInvocationData, ChatToolInvocationPart, MarkdownString } from '../../../../vscodeTypes';
12
import { ClaudeToolNames, ExitPlanModeInput, LSInput } from './claudeTools';
13
14
// #region Tool Result Content Extraction
15
16
/**
17
* Extracts text content from a tool result's content field.
18
* Tool results can be a string, an array of content blocks, or undefined.
19
*/
20
function extractToolResultContent(content: Anthropic.Messages.ToolResultBlockParam['content']): string {
21
if (!content) {
22
return '';
23
}
24
if (typeof content === 'string') {
25
return content;
26
}
27
// Array of content blocks - extract text from each
28
return content
29
.filter((block): block is Anthropic.Messages.TextBlockParam => block.type === 'text')
30
.map(block => block.text)
31
.join('\n');
32
}
33
34
// #endregion
35
36
// #region Tool Invocation Completion Handlers
37
38
/**
39
* Completes a tool invocation by populating toolSpecificData with the result.
40
* This enables VS Code's chat UI to display tool outputs.
41
*/
42
export function completeToolInvocation(
43
toolUse: Anthropic.Beta.Messages.BetaToolUseBlock,
44
toolResult: Anthropic.Messages.ToolResultBlockParam,
45
invocation: ChatToolInvocationPart
46
): void {
47
const resultContent = extractToolResultContent(toolResult.content);
48
49
switch (toolUse.name as ClaudeToolNames) {
50
case ClaudeToolNames.Bash:
51
completeBashInvocation(invocation, toolUse, resultContent);
52
break;
53
case ClaudeToolNames.Read:
54
case ClaudeToolNames.LS:
55
completeReadInvocation(invocation, toolUse, resultContent);
56
break;
57
case ClaudeToolNames.Glob:
58
case ClaudeToolNames.Grep:
59
completeSearchInvocation(invocation, toolUse, resultContent);
60
break;
61
case ClaudeToolNames.Edit:
62
case ClaudeToolNames.MultiEdit:
63
case ClaudeToolNames.Write:
64
case ClaudeToolNames.TodoWrite:
65
// These tools have their own UI handling (edit diffs, todo list)
66
break;
67
case ClaudeToolNames.Agent:
68
case ClaudeToolNames.Task:
69
completeTaskInvocation(invocation, resultContent);
70
break;
71
default:
72
completeGenericInvocation(invocation, toolUse, resultContent);
73
break;
74
}
75
}
76
77
/**
78
* Completes a bash tool invocation with terminal-specific output formatting.
79
* Parses exit code from output and formats for ChatTerminalToolInvocationData.
80
*/
81
function completeBashInvocation(
82
invocation: ChatToolInvocationPart,
83
toolUse: Anthropic.Beta.Messages.BetaToolUseBlock,
84
resultContent: string
85
): void {
86
// Parse exit code from the end of the result (format: "Exit code: X" or similar patterns)
87
const exitCodeMatch = /(?:exit code|exited with)[:=\s]*(\d+)/i.exec(resultContent);
88
const exitCode = exitCodeMatch ? parseInt(exitCodeMatch[1], 10) : undefined;
89
90
// Remove exit code line from output for cleaner display
91
let text = resultContent;
92
if (exitCode !== undefined) {
93
text = resultContent.replace(/(?:exit code|exited with)[:=\s]*\d+\s*$/i, '').trimEnd();
94
}
95
96
// Convert \n to \r\n for proper terminal display
97
text = text.replace(/\n/g, '\r\n');
98
99
const toolSpecificData: ChatTerminalToolInvocationData = {
100
commandLine: {
101
original: (toolUse.input as BashInput)?.command ?? '',
102
},
103
language: 'bash',
104
state: exitCode !== undefined ? { exitCode } : undefined,
105
output: text ? { text } : undefined
106
};
107
invocation.toolSpecificData = toolSpecificData;
108
}
109
110
/**
111
* Completes a read/ls tool invocation with simple input/output display.
112
*/
113
function completeReadInvocation(
114
invocation: ChatToolInvocationPart,
115
toolUse: Anthropic.Beta.Messages.BetaToolUseBlock,
116
resultContent: string
117
): void {
118
if (!resultContent) {
119
return;
120
}
121
122
const input = toolUse.name === ClaudeToolNames.LS
123
? (toolUse.input as LSInput)?.path ?? ''
124
: (toolUse.input as FileReadInput)?.file_path ?? '';
125
126
const toolSpecificData: ChatSimpleToolResultData = {
127
input,
128
output: resultContent
129
};
130
invocation.toolSpecificData = toolSpecificData;
131
}
132
133
/**
134
* Completes a glob/grep tool invocation with simple input/output display.
135
*/
136
function completeSearchInvocation(
137
invocation: ChatToolInvocationPart,
138
toolUse: Anthropic.Beta.Messages.BetaToolUseBlock,
139
resultContent: string
140
): void {
141
if (!resultContent) {
142
return;
143
}
144
145
const input = toolUse.name === ClaudeToolNames.Glob
146
? (toolUse.input as GlobInput)?.pattern ?? ''
147
: (toolUse.input as GrepInput)?.pattern ?? '';
148
149
const toolSpecificData: ChatSimpleToolResultData = {
150
input,
151
output: resultContent
152
};
153
invocation.toolSpecificData = toolSpecificData;
154
}
155
156
/**
157
* Completes a Task tool invocation by setting the result on its ChatSubagentToolInvocationData.
158
* The toolSpecificData was already populated by formatTaskInvocation with description/agentName/prompt;
159
* this adds the result text from the subagent's execution.
160
*/
161
function completeTaskInvocation(
162
invocation: ChatToolInvocationPart,
163
resultContent: string
164
): void {
165
if (invocation.toolSpecificData instanceof ChatSubagentToolInvocationData) {
166
invocation.toolSpecificData.result = resultContent;
167
}
168
}
169
170
/**
171
* Generic completion handler for tools without specific formatting.
172
* Displays input arguments and output as plain text.
173
*/
174
function completeGenericInvocation(
175
invocation: ChatToolInvocationPart,
176
toolUse: Anthropic.Beta.Messages.BetaToolUseBlock,
177
resultContent: string
178
): void {
179
if (!resultContent) {
180
return;
181
}
182
183
const toolSpecificData: ChatSimpleToolResultData = {
184
input: toolUse.input ? JSON.stringify(toolUse.input, null, 2) : '',
185
output: resultContent
186
};
187
invocation.toolSpecificData = toolSpecificData;
188
}
189
190
// #endregion
191
192
// #region Tool Invocation Creation
193
194
/**
195
* Creates a formatted tool invocation part based on the tool type and input
196
*/
197
export function createFormattedToolInvocation(
198
toolUse: Anthropic.Beta.Messages.BetaToolUseBlock,
199
complete?: boolean
200
): ChatToolInvocationPart | undefined {
201
const invocation = new ChatToolInvocationPart(toolUse.name, toolUse.id);
202
if (complete !== undefined) {
203
invocation.isConfirmed = complete;
204
invocation.isComplete = complete;
205
}
206
207
switch (toolUse.name as ClaudeToolNames) {
208
case ClaudeToolNames.Bash:
209
formatBashInvocation(invocation, toolUse);
210
break;
211
case ClaudeToolNames.Read:
212
formatReadInvocation(invocation, toolUse);
213
break;
214
case ClaudeToolNames.Glob:
215
formatGlobInvocation(invocation, toolUse);
216
break;
217
case ClaudeToolNames.Grep:
218
formatGrepInvocation(invocation, toolUse);
219
break;
220
case ClaudeToolNames.LS:
221
formatLSInvocation(invocation, toolUse);
222
break;
223
case ClaudeToolNames.Edit:
224
case ClaudeToolNames.MultiEdit:
225
case ClaudeToolNames.Write:
226
return; // edit diff is shown
227
case ClaudeToolNames.ExitPlanMode:
228
formatExitPlanModeInvocation(invocation, toolUse);
229
break;
230
case ClaudeToolNames.Agent:
231
case ClaudeToolNames.Task:
232
formatTaskInvocation(invocation, toolUse);
233
break;
234
case ClaudeToolNames.TodoWrite:
235
// Suppress this, it's too common
236
return;
237
default:
238
formatGenericInvocation(invocation, toolUse);
239
break;
240
}
241
242
return invocation;
243
}
244
245
function formatBashInvocation(invocation: ChatToolInvocationPart, toolUse: Anthropic.Beta.Messages.BetaToolUseBlock): void {
246
invocation.invocationMessage = '';
247
invocation.toolSpecificData = {
248
commandLine: {
249
original: (toolUse.input as BashInput)?.command,
250
},
251
language: 'bash'
252
};
253
}
254
255
function formatReadInvocation(invocation: ChatToolInvocationPart, toolUse: Anthropic.Beta.Messages.BetaToolUseBlock): void {
256
const filePath: string = (toolUse.input as FileReadInput)?.file_path ?? '';
257
const display = filePath ? formatUriForMessage(filePath) : '';
258
invocation.invocationMessage = new MarkdownString(l10n.t("Read {0}", display));
259
}
260
261
function formatGlobInvocation(invocation: ChatToolInvocationPart, toolUse: Anthropic.Beta.Messages.BetaToolUseBlock): void {
262
const pattern: string = (toolUse.input as GlobInput)?.pattern ?? '';
263
invocation.invocationMessage = new MarkdownString(l10n.t("Searched for files matching `{0}`", pattern));
264
}
265
266
function formatGrepInvocation(invocation: ChatToolInvocationPart, toolUse: Anthropic.Beta.Messages.BetaToolUseBlock): void {
267
const pattern: string = (toolUse.input as GrepInput)?.pattern ?? '';
268
invocation.invocationMessage = new MarkdownString(l10n.t("Searched for regex `{0}`", pattern));
269
}
270
271
function formatLSInvocation(invocation: ChatToolInvocationPart, toolUse: Anthropic.Beta.Messages.BetaToolUseBlock): void {
272
const path: string = (toolUse.input as LSInput)?.path ?? '';
273
const display = path ? formatUriForMessage(path) : '';
274
invocation.invocationMessage = new MarkdownString(l10n.t("Read {0}", display));
275
}
276
277
function formatExitPlanModeInvocation(invocation: ChatToolInvocationPart, toolUse: Anthropic.Beta.Messages.BetaToolUseBlock): void {
278
invocation.invocationMessage = l10n.t("Here is Claude's plan:\n\n{0}", (toolUse.input as ExitPlanModeInput)?.plan ?? '');
279
}
280
281
function formatTaskInvocation(invocation: ChatToolInvocationPart, toolUse: Anthropic.Beta.Messages.BetaToolUseBlock): void {
282
const description = (toolUse.input as AgentInput)?.description ?? '';
283
invocation.invocationMessage = new MarkdownString(l10n.t("Completed Task: \"{0}\"", description));
284
285
const input = toolUse.input as AgentInput;
286
invocation.toolSpecificData = new ChatSubagentToolInvocationData(
287
input.description,
288
input.subagent_type,
289
input.prompt);
290
}
291
292
function formatGenericInvocation(invocation: ChatToolInvocationPart, toolUse: Anthropic.Beta.Messages.BetaToolUseBlock): void {
293
invocation.invocationMessage = l10n.t("Used tool: {0}", toolUse.name);
294
}
295
296
function formatUriForMessage(path: string): string {
297
return `[](${URI.file(path).toString()})`;
298
}
299
300
// #endregion
301
302