Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/extension/chatSessions/copilotcli/common/copilotCLITools.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 type { SessionEvent, ToolExecutionCompleteEvent, ToolExecutionStartEvent } from '@github/copilot/sdk';
7
import * as l10n from '@vscode/l10n';
8
import type { CancellationToken, ChatParticipantToolToken, ChatPromptReference, ChatSimpleToolResultData, ChatTerminalToolInvocationData, ExtendedChatResponsePart, LanguageModelToolDefinition, LanguageModelToolInformation, LanguageModelToolInvocationOptions, LanguageModelToolResult2 } from 'vscode';
9
import { ILogger } from '../../../../platform/log/common/logService';
10
import { IChatEndpoint } from '../../../../platform/networking/common/networking';
11
import { isLocation } from '../../../../util/common/types';
12
import { findLast } from '../../../../util/vs/base/common/arraysFind';
13
import { decodeBase64 } from '../../../../util/vs/base/common/buffer';
14
import { Emitter } from '../../../../util/vs/base/common/event';
15
import { ResourceMap } from '../../../../util/vs/base/common/map';
16
import { constObservable, IObservable } from '../../../../util/vs/base/common/observable';
17
import { isAbsolutePath, isEqual } from '../../../../util/vs/base/common/resources';
18
import { URI } from '../../../../util/vs/base/common/uri';
19
import { ChatMcpToolInvocationData, ChatReferenceBinaryData, ChatRequestTurn2, ChatResponseCodeblockUriPart, ChatResponseMarkdownPart, ChatResponsePullRequestPart, ChatResponseTextEditPart, ChatResponseThinkingProgressPart, ChatResponseTurn2, ChatSubagentToolInvocationData, ChatToolInvocationPart, LanguageModelTextPart, Location, MarkdownString, McpToolInvocationContentData, Range, Uri } from '../../../../vscodeTypes';
20
import type { MCP } from '../../../common/modelContextProtocol';
21
import { ToolName } from '../../../tools/common/toolNames';
22
import { ICopilotTool } from '../../../tools/common/toolsRegistry';
23
import { IOnWillInvokeToolEvent, IToolsService, IToolValidationResult } from '../../../tools/common/toolsService';
24
import { formatUriForFileWidget } from '../../../tools/common/toolUtils';
25
import { StoredModeInstructions } from '../../common/chatSessionMetadataStore';
26
import { extractChatPromptReferences, getFolderAttachmentPath } from './copilotCLIPrompt';
27
import { IChatDelegationSummaryService } from './delegationSummaryService';
28
29
30
interface CreateTool {
31
toolName: 'create';
32
arguments: {
33
path: string;
34
file_text?: string;
35
};
36
}
37
38
interface ViewTool {
39
toolName: 'view';
40
arguments: {
41
path: string;
42
view_range?: [number, number];
43
forceReadLargeFiles?: boolean;
44
};
45
}
46
47
interface EditTool {
48
toolName: 'edit' | 'str_replace';
49
arguments: {
50
path: string;
51
old_str?: string;
52
new_str?: string;
53
};
54
}
55
56
interface StrReplaceTool {
57
toolName: 'str_replace';
58
arguments: {
59
path: string;
60
old_str?: string;
61
new_str?: string;
62
};
63
}
64
65
interface InsertTool {
66
toolName: 'insert';
67
arguments: {
68
path: string;
69
insert_line?: number;
70
new_str: string;
71
};
72
}
73
74
interface ShellTool {
75
toolName: 'bash' | 'powershell';
76
arguments: {
77
command: string;
78
description: string;
79
shellId?: string;
80
mode?: 'sync' | 'async';
81
detach?: boolean;
82
initial_wait?: number;
83
};
84
}
85
86
interface WriteShellTool {
87
toolName: 'write_bash' | 'write_powershell';
88
arguments: {
89
shellId: string;
90
input?: string;
91
delay: number;
92
};
93
}
94
95
interface ReadShellTool {
96
toolName: 'read_bash' | 'read_powershell';
97
arguments: {
98
shellId: string;
99
delay: number;
100
};
101
}
102
103
interface StopShellTool {
104
toolName: 'stop_bash' | 'stop_powershell';
105
arguments: {
106
shellId: string;
107
};
108
}
109
110
interface ListShellTool {
111
toolName: 'list_bash' | 'list_powershell';
112
arguments: Record<string, never>;
113
}
114
115
interface GrepTool {
116
toolName: 'grep' | 'rg';
117
arguments: {
118
pattern: string;
119
path?: string;
120
output_mode?: 'content' | 'files_with_matches' | 'count';
121
glob?: string;
122
type?: string;
123
'-i'?: boolean;
124
'-A'?: number;
125
'-B'?: number;
126
'-C'?: number;
127
'-n'?: boolean;
128
head_limit?: number;
129
multiline?: boolean;
130
};
131
}
132
133
interface GLobTool {
134
toolName: 'glob';
135
arguments: {
136
pattern: string;
137
path?: string;
138
};
139
}
140
141
type ReportIntentTool = {
142
toolName: 'report_intent';
143
arguments: {
144
intent: string;
145
};
146
};
147
type ThinkTool = {
148
toolName: 'think';
149
arguments: {
150
thought: string;
151
};
152
};
153
154
type UpdateTodoTool = {
155
toolName: 'update_todo';
156
arguments: {
157
todos: string;
158
};
159
};
160
161
type ReportProgressTool = {
162
toolName: 'report_progress';
163
arguments: {
164
commitMessage: string;
165
prDescription: string;
166
};
167
};
168
169
type WebFetchTool = {
170
toolName: 'web_fetch';
171
arguments: {
172
url: string;
173
max_length?: number;
174
start_index?: number;
175
raw?: boolean;
176
};
177
};
178
179
type WebSearchTool = {
180
toolName: 'web_search';
181
arguments: {
182
query: string;
183
};
184
};
185
186
type SearchCodeSubagentTool = {
187
toolName: 'search_code_subagent';
188
arguments: {
189
query: string;
190
};
191
};
192
193
type ReplyToCommentTool = {
194
toolName: 'reply_to_comment';
195
arguments: {
196
reply: string;
197
comment_id: string;
198
};
199
};
200
201
type CodeReviewTool = {
202
toolName: 'code_review';
203
arguments: {
204
prTitle: string;
205
prDescription: string;
206
};
207
};
208
209
type ShowFileTool = {
210
toolName: 'show_file';
211
arguments: {
212
path: string;
213
view_range?: number[];
214
diff?: boolean;
215
};
216
};
217
218
type FetchCopilotCliDocumentationTool = {
219
toolName: 'fetch_copilot_cli_documentation';
220
arguments: Record<string, never>;
221
};
222
223
type ProposeWorkTool = {
224
toolName: 'propose_work';
225
arguments: {
226
workType: 'code_change' | 'task';
227
workTitle: string;
228
workDescription: string;
229
};
230
};
231
232
type TaskCompleteTool = {
233
toolName: 'task_complete';
234
arguments: {
235
summary?: string;
236
};
237
};
238
239
type AskUserTool = {
240
toolName: 'ask_user';
241
arguments:
242
| {
243
question: string;
244
choices?: string[];
245
allow_freeform?: boolean;
246
}
247
| {
248
message: string;
249
requestedSchema: {
250
properties: Record<string, unknown>;
251
required?: string[];
252
};
253
};
254
};
255
256
type SkillTool = {
257
toolName: 'skill';
258
arguments: {
259
skill: string;
260
};
261
};
262
263
type TaskTool = {
264
toolName: 'task';
265
arguments: {
266
description: string;
267
prompt: string;
268
agent_type: string;
269
model?: string;
270
mode?: 'sync' | 'background';
271
};
272
};
273
274
type ListAgentsTool = {
275
toolName: 'list_agents';
276
arguments: {
277
include_completed?: boolean;
278
};
279
};
280
281
type ReadAgentTool = {
282
toolName: 'read_agent';
283
arguments: {
284
agent_id: string;
285
wait?: boolean;
286
timeout?: number;
287
};
288
};
289
290
type ExitPlanModeTool = {
291
toolName: 'exit_plan_mode';
292
arguments: {
293
summary: string;
294
actions?: string[];
295
recommendedAction?: string;
296
};
297
};
298
299
type SqlTool = {
300
toolName: 'sql';
301
arguments: {
302
description: string;
303
query: string;
304
database?: 'session' | 'session_store';
305
};
306
};
307
308
type LspTool = {
309
toolName: 'lsp';
310
arguments: {
311
operation: string;
312
file?: string;
313
line?: number;
314
character?: number;
315
newName?: string;
316
includeDeclaration?: boolean;
317
query?: string;
318
language?: string;
319
};
320
};
321
322
type CreatePullRequestTool = {
323
toolName: 'create_pull_request';
324
arguments: {
325
title: string;
326
description?: string;
327
draft?: boolean;
328
};
329
};
330
331
type DependencyCheckerTool = {
332
toolName: 'gh-advisory-database';
333
arguments: {
334
dependencies: { version: string; name: string; ecosystem: string }[];
335
};
336
};
337
338
type StoreMemoryTool = {
339
toolName: 'store_memory';
340
arguments: {
341
subject: string;
342
fact: string;
343
citations: string;
344
reason: string;
345
category: string;
346
};
347
};
348
349
type ParallelValidationTool = {
350
toolName: 'parallel_validation';
351
arguments: Record<string, never>;
352
};
353
354
type ApplyPatchTool = {
355
toolName: 'apply_patch';
356
arguments: {
357
input?: string;
358
patch?: string;
359
};
360
};
361
362
type WriteAgentTool = {
363
toolName: 'write_agent';
364
arguments: {
365
agent_id: string;
366
message: string;
367
};
368
};
369
370
type McpReloadTool = {
371
toolName: 'mcp_reload';
372
arguments: Record<string, never>;
373
};
374
375
type McpValidateTool = {
376
toolName: 'mcp_validate';
377
arguments: {
378
path: string;
379
};
380
};
381
382
type ToolSearchTool = {
383
toolName: 'tool_search_tool_regex';
384
arguments: {
385
pattern: string;
386
limit?: number;
387
};
388
};
389
390
type CodeQLCheckerTool = {
391
toolName: 'codeql_checker';
392
arguments: Record<string, never>;
393
};
394
395
396
type StringReplaceArgumentTypes = CreateTool | ViewTool | StrReplaceTool | EditTool | InsertTool;
397
type ToStringReplaceEditorArguments<T extends StringReplaceArgumentTypes> = {
398
command: T['toolName'];
399
} & T['arguments'];
400
type StringReplaceEditorTool = {
401
toolName: 'str_replace_editor';
402
arguments: ToStringReplaceEditorArguments<CreateTool> | ToStringReplaceEditorArguments<ViewTool> | ToStringReplaceEditorArguments<EditTool> | ToStringReplaceEditorArguments<StrReplaceTool> |
403
ToStringReplaceEditorArguments<InsertTool>;
404
};
405
export type ToolInfo = StringReplaceEditorTool | EditTool | CreateTool | ViewTool | InsertTool |
406
ShellTool | WriteShellTool | ReadShellTool | StopShellTool | ListShellTool |
407
GrepTool | GLobTool |
408
ReportIntentTool | ThinkTool | ReportProgressTool |
409
SearchCodeSubagentTool |
410
ReplyToCommentTool | CodeReviewTool | WebFetchTool | UpdateTodoTool | WebSearchTool |
411
ShowFileTool | FetchCopilotCliDocumentationTool | ProposeWorkTool | TaskCompleteTool |
412
AskUserTool | SkillTool | TaskTool | ListAgentsTool | ReadAgentTool | WriteAgentTool |
413
ExitPlanModeTool | SqlTool | LspTool | CreatePullRequestTool | DependencyCheckerTool | StoreMemoryTool | ParallelValidationTool |
414
ApplyPatchTool | McpReloadTool | McpValidateTool | ToolSearchTool | CodeQLCheckerTool;
415
416
export type ToolCall = ToolInfo & {
417
toolCallId: string;
418
mcpServerName?: string | undefined;
419
mcpToolName?: string | undefined;
420
};
421
export type UnknownToolCall = { toolName: string; arguments: unknown; toolCallId: string };
422
423
function isInstructionAttachmentPath(path: string): boolean {
424
const normalizedPath = path.replace(/\\/g, '/');
425
return normalizedPath.endsWith('/.github/copilot-instructions.md')
426
|| (normalizedPath.includes('/.github/instructions/') && normalizedPath.endsWith('.md'));
427
}
428
429
export function isCopilotCliEditToolCall(data: { toolName: string; arguments?: unknown }): boolean {
430
const toolCall = data as ToolCall;
431
if (toolCall.toolName === 'str_replace_editor') {
432
return toolCall.arguments.command !== 'view';
433
}
434
return toolCall.toolName === 'create' || toolCall.toolName === 'edit';
435
}
436
437
export function isCopilotCLIToolThatCouldRequirePermissions(event: ToolExecutionStartEvent): boolean {
438
const toolCall = event.data as unknown as ToolCall;
439
if (isCopilotCliEditToolCall(toolCall)) {
440
return true;
441
}
442
if (toolCall.mcpServerName) {
443
return false;
444
}
445
if (toolCall.toolName === 'bash' || toolCall.toolName === 'powershell') {
446
return true;
447
}
448
if (toolCall.toolName === 'view') {
449
return true;
450
}
451
return false;
452
}
453
454
export function getAffectedUrisForEditTool(data: { toolName: string; arguments?: unknown }): URI[] {
455
const toolCall = data as ToolCall;
456
// Old versions used str_replace_editor
457
// This should be removed eventually
458
// TODO @DonJayamanne verify with SDK & Padawan folk.
459
if (toolCall.toolName === 'str_replace_editor' && toolCall.arguments.command !== 'view' && typeof toolCall.arguments.path === 'string') {
460
return [URI.file(toolCall.arguments.path)];
461
}
462
463
if ((toolCall.toolName === 'create' || toolCall.toolName === 'edit') && typeof toolCall.arguments.path === 'string') {
464
return [URI.file(toolCall.arguments.path)];
465
}
466
467
return [];
468
}
469
470
export function stripReminders(text: string): string {
471
// Remove any <reminder> ... </reminder> blocks, including newlines
472
// Also remove <current_datetime> ... </current_datetime> blocks
473
// Also remove <pr_metadata .../> tags
474
return text
475
.replace(/<reminder>[\s\S]*?<\/reminder>\s*/g, '')
476
.replace(/<attachments>[\s\S]*?<\/attachments>\s*/g, '')
477
.replace(/<userRequest>[\s\S]*?<\/userRequest>\s*/g, '')
478
.replace(/<user_query>[\s\S]*?<\/user_query>\s*/g, '')
479
.replace(/<context>[\s\S]*?<\/context>\s*/g, '')
480
.replace(/<current_datetime>[\s\S]*?<\/current_datetime>\s*/g, '')
481
.replace(/<pr_metadata[^>]*\/?>\s*/g, '')
482
.trim();
483
}
484
485
/**
486
* Extract PR metadata from assistant message content
487
*/
488
function extractPRMetadata(content: string): { cleanedContent: string; prPart?: ChatResponsePullRequestPart } {
489
const prMetadataRegex = /<pr_metadata\s+uri="(?<uri>[^"]+)"\s+title="(?<title>[^"]+)"\s+description="(?<description>[^"]+)"\s+author="(?<author>[^"]+)"\s+linkTag="(?<linkTag>[^"]+)"\s*\/?>/;
490
const match = content.match(prMetadataRegex);
491
492
if (match?.groups) {
493
const { title, description, author, linkTag } = match.groups;
494
// Unescape XML entities
495
const unescapeXml = (text: string) => text
496
.replace(/&apos;/g, `'`)
497
.replace(/&quot;/g, '"')
498
.replace(/&gt;/g, '>')
499
.replace(/&lt;/g, '<')
500
.replace(/&amp;/g, '&');
501
502
const prPart = new ChatResponsePullRequestPart(
503
{ command: 'github.copilot.chat.openPullRequestReroute', title: l10n.t('View Pull Request {0}', linkTag), arguments: [Number(linkTag.substring(1))] },
504
unescapeXml(title),
505
unescapeXml(description),
506
unescapeXml(author),
507
unescapeXml(linkTag)
508
);
509
510
const cleanedContent = content.replace(match[0], '').trim();
511
return { cleanedContent, prPart };
512
}
513
514
return { cleanedContent: content };
515
}
516
517
export interface RequestIdDetails {
518
readonly requestId: string;
519
readonly toolIdEditMap: Record<string, string>;
520
readonly modeInstructions?: StoredModeInstructions;
521
}
522
523
/**
524
* Build chat history from SDK events for VS Code chat session
525
* Converts SDKEvents into ChatRequestTurn2 and ChatResponseTurn2 objects
526
*/
527
export function buildChatHistoryFromEvents(sessionId: string, modelId: string | undefined, events: readonly SessionEvent[], getVSCodeRequestId: (sdkRequestId: string) => RequestIdDetails | undefined, delegationSummaryService: IChatDelegationSummaryService, logger: ILogger, workingDirectory?: URI, defaultModeInstructionsForLastRequest?: StoredModeInstructions, lastResponseDetails?: string): (ChatRequestTurn2 | ChatResponseTurn2)[] {
528
const turns: (ChatRequestTurn2 | ChatResponseTurn2)[] = [];
529
let currentResponseParts: ExtendedChatResponsePart[] = [];
530
const pendingToolInvocations = new Map<string, [ChatToolInvocationPart | ChatResponseMarkdownPart | ChatResponseThinkingProgressPart, toolData: ToolCall, parentToolCallId: string | undefined]>();
531
532
let details: RequestIdDetails | undefined;
533
let isFirstUserMessage = true;
534
const currentAssistantMessage: { chunks: string[] } = { chunks: [] };
535
const processedMessages = new Set<string>();
536
537
function processAssistantMessage(content: string) {
538
// Extract PR metadata if present
539
const { cleanedContent, prPart } = extractPRMetadata(content);
540
// Add PR part first if it exists
541
if (prPart) {
542
currentResponseParts.push(prPart);
543
}
544
if (cleanedContent) {
545
currentResponseParts.push(
546
new ChatResponseMarkdownPart(new MarkdownString(cleanedContent))
547
);
548
}
549
}
550
551
function flushPendingAssistantMessage() {
552
if (currentAssistantMessage.chunks.length > 0) {
553
const content = currentAssistantMessage.chunks.join('');
554
currentAssistantMessage.chunks = [];
555
processAssistantMessage(content);
556
}
557
}
558
const lastUserMessageId = findLast(events, event => event.type === 'user.message' && !isSyntheticUserMessage(event))?.id;
559
for (const event of events) {
560
if (event.type !== 'assistant.message') {
561
flushPendingAssistantMessage();
562
}
563
564
switch (event.type) {
565
case 'user.message': {
566
if (isSyntheticUserMessage(event)) {
567
continue;
568
}
569
details = getVSCodeRequestId(event.id);
570
// Flush any pending response parts before adding user message
571
if (currentResponseParts.length > 0) {
572
turns.push(new ChatResponseTurn2(currentResponseParts, {}, ''));
573
currentResponseParts = [];
574
}
575
// Filter out vscode instruction files from references when building session history
576
// TODO@rebornix filter instructions should be rendered as "references" in chat response like normal chat.
577
const references: ChatPromptReference[] = [];
578
579
try {
580
references.push(...extractChatPromptReferences(event.data.content || ''));
581
} catch (ex) {
582
// ignore errors from parsing references
583
}
584
const existingReferences = new ResourceMap<Range | undefined>();
585
references.forEach(ref => {
586
if (URI.isUri(ref.value)) {
587
existingReferences.set(ref.value, undefined);
588
} else if (isLocation(ref.value)) {
589
existingReferences.set(ref.value.uri, ref.value.range);
590
}
591
});
592
((event.data.attachments || []))
593
.filter(attachment => attachment.type === 'selection' || attachment.type === 'github_reference' || attachment.type === 'blob' ? true : !isInstructionAttachmentPath(attachment.path))
594
.forEach(attachment => {
595
if (attachment.type === 'github_reference') {
596
return;
597
}
598
if (attachment.type === 'selection') {
599
const range = attachment.displayName ? getRangeInPrompt(event.data.content || '', attachment.displayName) : undefined;
600
const uri = Uri.file(attachment.filePath);
601
if (existingReferences.has(uri) && !existingReferences.get(uri)) {
602
return; // Skip duplicates
603
}
604
references.push({
605
id: attachment.filePath,
606
name: attachment.displayName,
607
value: new Location(uri, new Range(attachment.selection.start.line - 1, attachment.selection.start.character - 1, attachment.selection.end.line - 1, attachment.selection.end.character - 1)),
608
range
609
});
610
} else if (attachment.type === 'file' || attachment.type === 'directory') {
611
const range = attachment.displayName ? getRangeInPrompt(event.data.content || '', attachment.displayName) : undefined;
612
const attachmentPath = attachment.type === 'directory' ?
613
getFolderAttachmentPath(attachment.path) :
614
attachment.path;
615
const uri = Uri.file(attachmentPath);
616
if (existingReferences.has(uri)) {
617
return; // Skip duplicates
618
}
619
references.push({
620
id: attachment.path,
621
name: attachment.displayName,
622
value: uri,
623
range
624
});
625
} else if (attachment.type === 'blob') {
626
const binaryDataSupplier = async () => {
627
try {
628
return decodeBase64(attachment.data).buffer;
629
} catch (error) {
630
logger.error(error, `Failed to decode blob attachment ${attachment.displayName || ''}`);
631
throw error;
632
}
633
};
634
references.push({
635
id: `${attachment.displayName || ''}-${attachment.mimeType}-${attachment.type}`,
636
name: attachment.displayName || '',
637
value: new ChatReferenceBinaryData(attachment.mimeType, binaryDataSupplier),
638
});
639
}
640
});
641
642
let prompt = stripReminders(event.data.content || '');
643
const info = isFirstUserMessage ? delegationSummaryService.extractPrompt(sessionId, prompt) : undefined;
644
if (info) {
645
prompt = info.prompt;
646
references.push(info.reference);
647
}
648
isFirstUserMessage = false;
649
let modeInstructions2 = details?.modeInstructions ? {
650
uri: details.modeInstructions.uri ? Uri.parse(details.modeInstructions.uri) : undefined,
651
name: details.modeInstructions.name,
652
content: details.modeInstructions.content,
653
metadata: details.modeInstructions.metadata,
654
isBuiltin: details.modeInstructions.isBuiltin,
655
} : undefined;
656
657
if (lastUserMessageId && event.id === lastUserMessageId && defaultModeInstructionsForLastRequest && !modeInstructions2) {
658
modeInstructions2 = modeInstructions2 ?? {
659
uri: defaultModeInstructionsForLastRequest.uri ? Uri.parse(defaultModeInstructionsForLastRequest.uri) : undefined,
660
name: defaultModeInstructionsForLastRequest.name,
661
content: defaultModeInstructionsForLastRequest.content,
662
metadata: defaultModeInstructionsForLastRequest.metadata,
663
isBuiltin: defaultModeInstructionsForLastRequest.isBuiltin,
664
};
665
}
666
let commandPrefix = '';
667
switch (event.data.agentMode) {
668
case 'autopilot': {
669
commandPrefix = '/autopilot ';
670
break;
671
}
672
case 'plan': {
673
commandPrefix = '/plan ';
674
break;
675
}
676
}
677
678
turns.push(new ChatRequestTurn2(`${commandPrefix}${prompt}`, undefined, references, '', [], undefined, details?.requestId ?? event.id, modelId, modeInstructions2));
679
break;
680
}
681
case 'assistant.message_delta': {
682
if (typeof event.data.deltaContent === 'string') {
683
// Skip sub-agent markdown — it will be captured in the subagent tool's result
684
if (!event.data.parentToolCallId) {
685
processedMessages.add(event.data.messageId);
686
currentAssistantMessage.chunks.push(event.data.deltaContent);
687
}
688
}
689
break;
690
}
691
case 'session.error': {
692
currentResponseParts.push(new ChatResponseMarkdownPart(`\n\n❌ Error: (${event.data.errorType}) ${event.data.message}`));
693
break;
694
}
695
case 'assistant.message': {
696
// Skip sub-agent markdown — it will be captured in the subagent tool's result
697
if (event.data.content && !processedMessages.has(event.data.messageId) && !event.data.parentToolCallId) {
698
processAssistantMessage(event.data.content);
699
}
700
break;
701
}
702
case 'tool.execution_start': {
703
const responsePart = processToolExecutionStart(event, pendingToolInvocations, workingDirectory);
704
if (responsePart instanceof ChatResponseThinkingProgressPart) {
705
currentResponseParts.push(responsePart);
706
}
707
break;
708
}
709
case 'subagent.started': {
710
enrichToolInvocationWithSubagentMetadata(
711
event.data.toolCallId,
712
event.data.agentDisplayName,
713
event.data.agentDescription,
714
pendingToolInvocations
715
);
716
break;
717
}
718
case 'subagent.completed':
719
case 'subagent.failed': {
720
// Completion is already handled by tool.execution_complete for the task tool
721
break;
722
}
723
case 'tool.execution_complete': {
724
const [responsePart, toolCall] = processToolExecutionComplete(event, pendingToolInvocations, logger, workingDirectory) ?? [undefined, undefined];
725
if (responsePart && toolCall && !(responsePart instanceof ChatResponseThinkingProgressPart)) {
726
const editId = details?.toolIdEditMap ? details.toolIdEditMap[toolCall.toolCallId] : undefined;
727
const editedUris = getAffectedUrisForEditTool(toolCall);
728
if (!(responsePart instanceof ChatResponseMarkdownPart) && isCopilotCliEditToolCall(toolCall) && editId && editedUris.length > 0) {
729
responsePart.presentation = 'hidden';
730
currentResponseParts.push(responsePart);
731
for (const uri of editedUris) {
732
currentResponseParts.push(new ChatResponseMarkdownPart('\n````\n'));
733
currentResponseParts.push(new ChatResponseCodeblockUriPart(uri, true, editId));
734
currentResponseParts.push(new ChatResponseTextEditPart(uri, []));
735
currentResponseParts.push(new ChatResponseTextEditPart(uri, true));
736
currentResponseParts.push(new ChatResponseMarkdownPart('\n````\n'));
737
}
738
} else {
739
currentResponseParts.push(responsePart);
740
}
741
}
742
break;
743
}
744
}
745
}
746
747
flushPendingAssistantMessage();
748
749
if (currentResponseParts.length > 0) {
750
turns.push(new ChatResponseTurn2(currentResponseParts, lastResponseDetails ? { details: lastResponseDetails } : {}, ''));
751
}
752
753
return turns;
754
}
755
756
function getRangeInPrompt(prompt: string, referencedName: string): [number, number] | undefined {
757
referencedName = `#${referencedName}`;
758
const index = prompt.indexOf(referencedName);
759
if (index >= 0) {
760
return [index, index + referencedName.length];
761
}
762
return undefined;
763
}
764
765
/**
766
* Converts MCP {@link MCP.ContentBlock}[] values produced by MCP tool execution into
767
* VS Code {@link McpToolInvocationContentData}[] objects for rendering in the chat UI.
768
*
769
* MCP ContentBlocks represent heterogeneous pieces of tool output such as text, images,
770
* audio, embedded resources, or resource links. This helper normalizes those different
771
* content shapes into a common binary+MIME-type representation that the VS Code chat
772
* tool invocation renderer understands, so that MCP tool results can be displayed
773
* consistently alongside other chat responses.
774
*/
775
function convertMcpContentToToolInvocationData(result: ToolExecutionCompleteEvent['data']['result'], logger: ILogger): McpToolInvocationContentData[] {
776
const output: McpToolInvocationContentData[] = [];
777
const encoder = new TextEncoder();
778
779
if (!Array.isArray(result?.contents) || result.contents.length === 0) {
780
return output;
781
}
782
783
for (const block of result.contents) {
784
try {
785
switch (block.type) {
786
case 'text':
787
// Convert text to UTF-8 bytes with text/plain mime type
788
output.push(new McpToolInvocationContentData(
789
encoder.encode(block.text),
790
'text/plain'
791
));
792
break;
793
794
case 'image':
795
// Decode base64 image data and preserve mime type
796
output.push(new McpToolInvocationContentData(
797
decodeBase64(block.data).buffer,
798
block.mimeType
799
));
800
break;
801
802
case 'audio':
803
// Decode base64 audio data and preserve mime type
804
output.push(new McpToolInvocationContentData(
805
decodeBase64(block.data).buffer,
806
block.mimeType
807
));
808
break;
809
810
case 'resource': {
811
// Handle embedded resource (text or blob)
812
const resource = block.resource;
813
if ('text' in resource) {
814
// TextResourceContents
815
const mimeType = resource.mimeType || 'text/plain';
816
output.push(new McpToolInvocationContentData(
817
encoder.encode(resource.text),
818
mimeType
819
));
820
} else if ('blob' in resource) {
821
// BlobResourceContents
822
const mimeType = resource.mimeType || 'application/octet-stream';
823
output.push(new McpToolInvocationContentData(
824
decodeBase64(resource.blob).buffer,
825
mimeType
826
));
827
}
828
break;
829
}
830
831
case 'resource_link': {
832
// Format resource link as readable text with name and URI
833
const displayName = block.title || block.name;
834
const linkText = displayName ? `Resource: ${displayName}\nURI: ${block.uri}` : block.uri;
835
output.push(new McpToolInvocationContentData(
836
encoder.encode(linkText),
837
'text/plain'
838
));
839
break;
840
}
841
}
842
} catch (error) {
843
// Log conversion errors but continue processing other blocks
844
logger.error(error, `Failed to convert MCP content block of type ${block.type}:`);
845
}
846
}
847
848
return output;
849
}
850
851
/**
852
* Enriches an existing pending tool invocation with subagent metadata from a `subagent.started` event.
853
* The `subagent.started` event carries richer metadata (display name, description) than the `task`
854
* tool's arguments, so we use it to update the `ChatSubagentToolInvocationData` on the tool invocation.
855
*/
856
export function enrichToolInvocationWithSubagentMetadata(
857
toolCallId: string,
858
agentDisplayName: string,
859
agentDescription: string | undefined,
860
pendingToolInvocations: Map<string, [ChatToolInvocationPart | ChatResponseMarkdownPart | ChatResponseThinkingProgressPart, toolData: ToolCall, parentToolCallId: string | undefined]>
861
): void {
862
const invocation = pendingToolInvocations.get(toolCallId);
863
if (!invocation) {
864
return;
865
}
866
const [part] = invocation;
867
if (!(part instanceof ChatToolInvocationPart)) {
868
return;
869
}
870
871
if (part.toolSpecificData instanceof ChatSubagentToolInvocationData) {
872
part.toolSpecificData.agentName = agentDisplayName;
873
if (agentDescription) {
874
part.toolSpecificData.description = agentDescription;
875
}
876
}
877
}
878
879
export function processToolExecutionStart(event: ToolExecutionStartEvent, pendingToolInvocations: Map<string, [ChatToolInvocationPart | ChatResponseMarkdownPart | ChatResponseThinkingProgressPart, toolData: ToolCall, parentToolCallId: string | undefined]>, workingDirectory?: URI): ChatToolInvocationPart | ChatResponseMarkdownPart | ChatResponseThinkingProgressPart | undefined {
880
const toolInvocation = createCopilotCLIToolInvocation(event.data as ToolCall, undefined, workingDirectory);
881
if (toolInvocation) {
882
if (toolInvocation instanceof ChatToolInvocationPart && event.data.parentToolCallId) {
883
// Resolve to the root ancestor so all descendants are grouped under the
884
// top-level subagent container instead of creating intermediate containers.
885
toolInvocation.subAgentInvocationId = resolveRootSubagentId(event.data.parentToolCallId, pendingToolInvocations);
886
887
// Nested task tools should not create their own subagent container —
888
// clear ChatSubagentToolInvocationData so the widget treats them as
889
// regular child tool invocations within the parent container.
890
if (toolInvocation.toolSpecificData instanceof ChatSubagentToolInvocationData) {
891
toolInvocation.toolSpecificData = undefined;
892
}
893
}
894
// Store pending invocation to update with result later
895
pendingToolInvocations.set(event.data.toolCallId, [toolInvocation, event.data as ToolCall, event.data.parentToolCallId]);
896
}
897
return toolInvocation;
898
}
899
900
/**
901
* Walks the parentToolCallId chain to find the root (top-level) subagent toolCallId.
902
* This ensures all nested tools are grouped under the outermost subagent container.
903
*/
904
function resolveRootSubagentId(
905
parentToolCallId: string,
906
pendingToolInvocations: Map<string, [ChatToolInvocationPart | ChatResponseMarkdownPart | ChatResponseThinkingProgressPart, toolData: ToolCall, parentToolCallId: string | undefined]>
907
): string {
908
let currentId = parentToolCallId;
909
const visited = new Set<string>();
910
while (true) {
911
if (visited.has(currentId)) {
912
break; // Prevent infinite loops
913
}
914
visited.add(currentId);
915
const parent = pendingToolInvocations.get(currentId);
916
if (!parent || !parent[2]) {
917
break; // No further parent — currentId is the root
918
}
919
currentId = parent[2];
920
}
921
return currentId;
922
}
923
924
export function processToolExecutionComplete(event: ToolExecutionCompleteEvent, pendingToolInvocations: Map<string, [ChatToolInvocationPart | ChatResponseMarkdownPart | ChatResponseThinkingProgressPart, toolData: ToolCall, parentToolCallId: string | undefined]>, logger: ILogger, workingDirectory?: URI): [ChatToolInvocationPart | ChatResponseMarkdownPart | ChatResponseThinkingProgressPart, toolData: ToolCall, parentToolCallId: string | undefined] | undefined {
925
const invocation = pendingToolInvocations.get(event.data.toolCallId);
926
pendingToolInvocations.delete(event.data.toolCallId);
927
928
if (invocation && invocation[0] instanceof ChatToolInvocationPart) {
929
invocation[0].isComplete = true;
930
invocation[0].isError = !!event.data.error;
931
invocation[0].invocationMessage = event.data.error?.message || invocation[0].invocationMessage;
932
if (!event.data.success && (event.data.error?.code === 'rejected' || event.data.error?.code === 'denied')) {
933
invocation[0].isConfirmed = false;
934
} else {
935
invocation[0].isConfirmed = true;
936
}
937
const toolCall = invocation[1];
938
if (Object.hasOwn(ToolFriendlyNameAndHandlers, toolCall.toolName)) {
939
const [, , postFormatter] = ToolFriendlyNameAndHandlers[toolCall.toolName];
940
try {
941
(postFormatter as PostInvocationFormatter)(invocation[0], toolCall, event.data, workingDirectory);
942
} catch (err) {
943
logger.error(err, `Failed to format tool invocation completion for tool: ${toolCall.toolName}`);
944
try {
945
genericToolInvocationCompleted(invocation[0], toolCall, event.data);
946
} catch {
947
// ignore
948
}
949
}
950
} else if (toolCall.mcpServerName && toolCall.mcpToolName) {
951
// Use tool arguments as input, formatted as JSON
952
const input = toolCall.arguments ? JSON.stringify(toolCall.arguments, null, 2) : '';
953
const output = convertMcpContentToToolInvocationData(event.data.result, logger);
954
if (output.length) {
955
invocation[0].toolSpecificData = {
956
input,
957
output
958
} satisfies ChatMcpToolInvocationData;
959
} else {
960
// If we don't have any structured output, at least include the raw text of the result for visibility in the chat UI.
961
genericToolInvocationCompleted(invocation[0], toolCall, event.data);
962
}
963
} else {
964
if (!!event.data.error && event.data.error?.message) {
965
invocation[0] = new ChatToolInvocationPart(invocation[0].toolName, invocation[0].toolCallId, event.data.error.message);
966
invocation[0].isComplete = true;
967
invocation[0].isError = true;
968
invocation[0].invocationMessage = event.data.error?.message || invocation[0].invocationMessage;
969
invocation[0].pastTenseMessage = `Used tool: ${invocation[0].toolName}`;
970
} else {
971
genericToolInvocationCompleted(invocation[0], toolCall, event.data);
972
}
973
}
974
}
975
976
return invocation;
977
}
978
979
/**
980
* Creates a formatted tool invocation part for CopilotCLI tools
981
*/
982
export function createCopilotCLIToolInvocation(data: {
983
toolCallId: string; toolName: string; arguments?: unknown; mcpServerName?: string | undefined;
984
mcpToolName?: string | undefined;
985
}, editId?: string, workingDirectory?: URI, logger?: ILogger): ChatToolInvocationPart | ChatResponseMarkdownPart | ChatResponseThinkingProgressPart | undefined {
986
if (!Object.hasOwn(ToolFriendlyNameAndHandlers, data.toolName)) {
987
const mcpServer = l10n.t('MCP Server');
988
const toolName = data.mcpServerName && data.mcpToolName ? `${data.mcpServerName}, ${data.mcpToolName} (${mcpServer})` : data.toolName;
989
const invocation = new ChatToolInvocationPart(toolName ?? 'unknown', data.toolCallId ?? '');
990
invocation.isConfirmed = false;
991
invocation.isComplete = false;
992
invocation.invocationMessage = l10n.t("Using tool: {0}", toolName ?? 'unknown');
993
invocation.pastTenseMessage = l10n.t("Used tool: {0}", toolName ?? 'unknown');
994
return invocation;
995
}
996
997
const toolCall = data as ToolCall;
998
// Ensures arguments is at least an empty object
999
toolCall.arguments = toolCall.arguments ?? {};
1000
if (toolCall.toolName === 'report_intent') {
1001
return undefined; // Ignore these for now
1002
}
1003
if (toolCall.toolName === 'think') {
1004
if (toolCall.arguments && typeof toolCall.arguments.thought === 'string') {
1005
return new ChatResponseThinkingProgressPart(toolCall.arguments.thought);
1006
}
1007
return undefined;
1008
}
1009
if (toolCall.toolName === 'show_file') {
1010
// Currently there's no good way to render this to the user.
1011
// Its a way to draw users attention to a file/code block.
1012
// Generally models render the codeblock in the response, but here we have a tool call.
1013
// Its a WIP, no clear way to render in CLI either, hence decided to hide in VS Code.
1014
return undefined;
1015
}
1016
if (toolCall.toolName === 'task_complete') {
1017
if (toolCall.arguments.summary) {
1018
const markdownContent = new MarkdownString();
1019
markdownContent.appendMarkdown(toolCall.arguments.summary);
1020
return new ChatResponseMarkdownPart(markdownContent);
1021
}
1022
return undefined;
1023
}
1024
1025
const [friendlyToolName, formatter] = ToolFriendlyNameAndHandlers[toolCall.toolName];
1026
const invocation = new ChatToolInvocationPart(friendlyToolName ?? toolCall.toolName ?? 'unknown', toolCall.toolCallId ?? '');
1027
invocation.isConfirmed = false;
1028
invocation.isComplete = false;
1029
1030
try {
1031
(formatter as Formatter)(invocation, toolCall, editId, workingDirectory);
1032
} catch (err) {
1033
logger?.error(err, `Failed to format tool invocation for tool: ${toolCall.toolName}`);
1034
}
1035
return invocation;
1036
}
1037
1038
type Formatter = (invocation: ChatToolInvocationPart, toolCall: ToolCall, editId?: string, workingDirectory?: URI) => void;
1039
type PostInvocationFormatter = (invocation: ChatToolInvocationPart, toolCall: ToolCall, result: ToolCallResult, workingDirectory?: URI) => void;
1040
type ToolCallFor<T extends ToolCall['toolName']> = Extract<ToolCall, { toolName: T }>;
1041
type ToolCallResult = ToolExecutionCompleteEvent['data'];
1042
1043
const ToolFriendlyNameAndHandlers: { [K in ToolCall['toolName']]: [title: string, pre: (invocation: ChatToolInvocationPart, toolCall: ToolCallFor<K>, editId?: string, workingDirectory?: URI) => void, post: (invocation: ChatToolInvocationPart, toolCall: ToolCallFor<K>, result: ToolCallResult, workingDirectory?: URI) => void] } = {
1044
'str_replace_editor': [l10n.t('Edit File'), formatStrReplaceEditorInvocation, genericToolInvocationCompleted],
1045
'edit': [l10n.t('Edit File'), formatEditToolInvocation, emptyToolInvocationCompleted],
1046
'str_replace': [l10n.t('Edit File'), formatEditToolInvocation, emptyToolInvocationCompleted],
1047
'create': [l10n.t('Create File'), formatCreateToolInvocation, emptyToolInvocationCompleted],
1048
'insert': [l10n.t('Edit File'), formatInsertToolInvocation, emptyToolInvocationCompleted],
1049
'view': [l10n.t('Read'), formatViewToolInvocation, emptyToolInvocationCompleted],
1050
'bash': [l10n.t('Run Shell Command'), formatShellInvocation, formatShellInvocationCompleted],
1051
'powershell': [l10n.t('Run Shell Command'), formatShellInvocation, formatShellInvocationCompleted],
1052
'write_bash': [l10n.t('Write to Bash'), emptyInvocation, genericToolInvocationCompleted],
1053
'write_powershell': [l10n.t('Write to PowerShell'), emptyInvocation, genericToolInvocationCompleted],
1054
'read_bash': [l10n.t('Read Terminal'), emptyInvocation, genericToolInvocationCompleted],
1055
'read_powershell': [l10n.t('Read Terminal'), emptyInvocation, genericToolInvocationCompleted],
1056
'stop_bash': [l10n.t('Stop Terminal Session'), emptyInvocation, genericToolInvocationCompleted],
1057
'stop_powershell': [l10n.t('Stop Terminal Session'), emptyInvocation, genericToolInvocationCompleted],
1058
'grep': [l10n.t('Search'), formatSearchToolInvocation, formatSearchToolInvocationCompleted],
1059
'rg': [l10n.t('Search'), formatSearchToolInvocation, formatSearchToolInvocationCompleted],
1060
'glob': [l10n.t('Search'), formatSearchToolInvocation, formatSearchToolInvocationCompleted],
1061
'search_code_subagent': [l10n.t('Search Code'), formatSearchToolInvocation, emptyToolInvocationCompleted],
1062
'reply_to_comment': [l10n.t('Reply to Comment'), formatReplyToCommentInvocation, genericToolInvocationCompleted],
1063
'code_review': [l10n.t('Code Review'), formatCodeReviewInvocation, genericToolInvocationCompleted],
1064
'report_intent': [l10n.t('Report Intent'), emptyInvocation, genericToolInvocationCompleted],
1065
'think': [l10n.t('Thinking'), emptyInvocation, genericToolInvocationCompleted],
1066
'report_progress': [l10n.t('Progress update'), formatProgressToolInvocation, genericToolInvocationCompleted],
1067
'web_fetch': [l10n.t('Fetch Web Content'), emptyInvocation, genericToolInvocationCompleted],
1068
'web_search': [l10n.t('Web Search'), emptyInvocation, genericToolInvocationCompleted],
1069
'update_todo': [l10n.t('Update Todo'), formatUpdateTodoInvocation, formatUpdateTodoInvocationCompleted],
1070
'show_file': [l10n.t('Show File'), formatShowFileInvocation, genericToolInvocationCompleted],
1071
'fetch_copilot_cli_documentation': [l10n.t('Fetch Documentation'), emptyInvocation, genericToolInvocationCompleted],
1072
'propose_work': [l10n.t('Propose Work'), formatProposeWorkInvocation, genericToolInvocationCompleted],
1073
'task_complete': [l10n.t('Task Complete'), formatTaskCompleteInvocation, genericToolInvocationCompleted],
1074
'ask_user': [l10n.t('Ask User'), formatAskUserInvocation, genericToolInvocationCompleted],
1075
'skill': [l10n.t('Invoke Skill'), formatSkillInvocation, genericToolInvocationCompleted],
1076
'task': [l10n.t('Delegate Task'), formatTaskInvocation, formatTaskInvocationCompleted],
1077
'list_agents': [l10n.t('List Agents'), emptyInvocation, genericToolInvocationCompleted],
1078
'read_agent': [l10n.t('Read Agent'), formatReadAgentInvocation, genericToolInvocationCompleted],
1079
'exit_plan_mode': [l10n.t('Exit Plan Mode'), formatExitPlanModeInvocation, genericToolInvocationCompleted],
1080
'sql': [l10n.t('Execute SQL'), formatSqlInvocation, genericToolInvocationCompleted],
1081
'lsp': [l10n.t('Language Server'), formatLspInvocation, genericToolInvocationCompleted],
1082
'create_pull_request': [l10n.t('Create Pull Request'), formatCreatePullRequestInvocation, genericToolInvocationCompleted],
1083
'gh-advisory-database': [l10n.t('Check Dependencies'), emptyInvocation, genericToolInvocationCompleted],
1084
'store_memory': [l10n.t('Store Memory'), formatStoreMemoryInvocation, genericToolInvocationCompleted],
1085
'list_bash': [l10n.t('List Shell Sessions'), emptyInvocation, genericToolInvocationCompleted],
1086
'list_powershell': [l10n.t('List Shell Sessions'), emptyInvocation, genericToolInvocationCompleted],
1087
'parallel_validation': [l10n.t('Validate Changes'), emptyInvocation, genericToolInvocationCompleted],
1088
'apply_patch': [l10n.t('Apply Patch'), formatApplyPatchInvocation, genericToolInvocationCompleted],
1089
'write_agent': [l10n.t('Write to Agent'), formatWriteAgentInvocation, genericToolInvocationCompleted],
1090
'mcp_reload': [l10n.t('Reload MCP Config'), emptyInvocation, genericToolInvocationCompleted],
1091
'mcp_validate': [l10n.t('Validate MCP Config'), formatMcpValidateInvocation, genericToolInvocationCompleted],
1092
'tool_search_tool_regex': [l10n.t('Search Tools'), formatToolSearchInvocation, genericToolInvocationCompleted],
1093
'codeql_checker': [l10n.t('CodeQL Security Scan'), emptyInvocation, genericToolInvocationCompleted],
1094
};
1095
1096
1097
function formatProgressToolInvocation(invocation: ChatToolInvocationPart, toolCall: ReportProgressTool): void {
1098
const args = toolCall.arguments;
1099
invocation.invocationMessage = args.prDescription?.trim() || 'Progress Update';
1100
if (args.commitMessage) {
1101
invocation.originMessage = `Commit: ${args.commitMessage}`;
1102
}
1103
}
1104
1105
1106
1107
function formatViewToolInvocation(invocation: ChatToolInvocationPart, toolCall: ViewTool): void {
1108
const args = toolCall.arguments;
1109
1110
if (!args.path) {
1111
return;
1112
} else if (args.view_range && args.view_range.length === 2 && args.view_range[1] >= args.view_range[0] && args.view_range[0] >= 0) {
1113
const [start, end] = args.view_range;
1114
const location = new Location(Uri.file(args.path), new Range(start === 0 ? start : start - 1, 0, end, 0));
1115
const display = formatUriForFileWidget(location);
1116
const localizedMessage = start === end
1117
? l10n.t("Reading {0}, line {1}", display, start)
1118
: l10n.t("Reading {0}, lines {1} to {2}", display, start, end);
1119
const localizedPastTenseMessage = start === end
1120
? l10n.t("Read {0}, line {1}", display, start)
1121
: l10n.t("Read {0}, lines {1} to {2}", display, start, end);
1122
invocation.invocationMessage = new MarkdownString(localizedMessage);
1123
invocation.pastTenseMessage = new MarkdownString(localizedPastTenseMessage);
1124
} else {
1125
const display = formatUriForFileWidget(Uri.file(args.path));
1126
invocation.invocationMessage = new MarkdownString(l10n.t("Read {0}", display));
1127
}
1128
}
1129
1130
function formatStrReplaceEditorInvocation(invocation: ChatToolInvocationPart, toolCall: StringReplaceEditorTool, editId?: string): void {
1131
if (!toolCall.arguments.path) {
1132
return;
1133
}
1134
const args = toolCall.arguments;
1135
const display = formatUriForFileWidget(Uri.file(args.path));
1136
switch (args.command) {
1137
case 'view':
1138
formatViewToolInvocation(invocation, { toolName: 'view', arguments: args } as ViewTool);
1139
break;
1140
case 'edit':
1141
formatEditToolInvocation(invocation, { toolName: 'edit', arguments: args } as EditTool);
1142
break;
1143
case 'insert':
1144
formatInsertToolInvocation(invocation, { toolName: 'insert', arguments: args } as InsertTool);
1145
break;
1146
case 'create':
1147
formatCreateToolInvocation(invocation, { toolName: 'create', arguments: args } as CreateTool);
1148
break;
1149
default:
1150
invocation.invocationMessage = new MarkdownString(l10n.t("Modified {0}", display));
1151
}
1152
}
1153
1154
function formatInsertToolInvocation(invocation: ChatToolInvocationPart, toolCall: InsertTool): void {
1155
const args = toolCall.arguments;
1156
if (args.path) {
1157
invocation.invocationMessage = new MarkdownString(l10n.t("Inserted text in {0}", formatUriForFileWidget(Uri.file(args.path))));
1158
}
1159
}
1160
1161
function formatEditToolInvocation(invocation: ChatToolInvocationPart, toolCall: EditTool, editId?: string): void {
1162
const args = toolCall.arguments;
1163
const display = args.path ? formatUriForFileWidget(Uri.file(args.path)) : '';
1164
1165
invocation.invocationMessage = display
1166
? new MarkdownString(l10n.t("Editing {0}", display))
1167
: new MarkdownString(l10n.t("Editing file"));
1168
invocation.pastTenseMessage = display
1169
? new MarkdownString(l10n.t("Edited {0}", display))
1170
: new MarkdownString(l10n.t("Edited file"));
1171
}
1172
1173
1174
function formatCreateToolInvocation(invocation: ChatToolInvocationPart, toolCall: CreateTool, editId?: string): void {
1175
const args = toolCall.arguments;
1176
const display = args.path ? formatUriForFileWidget(Uri.file(args.path)) : '';
1177
1178
if (display) {
1179
invocation.invocationMessage = new MarkdownString(l10n.t("Creating {0}", display));
1180
invocation.pastTenseMessage = new MarkdownString(l10n.t("Created {0}", display));
1181
} else {
1182
invocation.invocationMessage = new MarkdownString(l10n.t("Creating file"));
1183
invocation.pastTenseMessage = new MarkdownString(l10n.t("Created file"));
1184
}
1185
}
1186
1187
/**
1188
* Extracts a `cd <dir> &&` (or PowerShell equivalent) prefix from a command line,
1189
* returning the directory and remaining command.
1190
*/
1191
export function extractCdPrefix(commandLine: string, isPowershell: boolean): { directory: string; command: string } | undefined {
1192
const cdPrefixMatch = commandLine.match(
1193
isPowershell
1194
? /^(?:cd(?: \/d)?|Set-Location(?: -Path)?) (?<dir>"[^"]*"|[^\s]+) ?(?:&&|;)\s+(?<suffix>.+)$/i
1195
: /^cd (?<dir>"[^"]*"|[^\s]+) &&\s+(?<suffix>.+)$/
1196
);
1197
const cdDir = cdPrefixMatch?.groups?.dir;
1198
const cdSuffix = cdPrefixMatch?.groups?.suffix;
1199
if (cdDir && cdSuffix) {
1200
let cdDirPath = cdDir;
1201
if (cdDirPath.startsWith('"') && cdDirPath.endsWith('"')) {
1202
cdDirPath = cdDirPath.slice(1, -1);
1203
}
1204
return { directory: cdDirPath, command: cdSuffix };
1205
}
1206
return undefined;
1207
}
1208
1209
/**
1210
* Returns presentationOverrides only when the cd prefix directory matches the working directory.
1211
*/
1212
export function getCdPresentationOverrides(commandLine: string, isPowershell: boolean, workingDirectory?: URI): { commandLine: string } | undefined {
1213
const cdPrefix = extractCdPrefix(commandLine, isPowershell);
1214
if (!cdPrefix || !workingDirectory) {
1215
return undefined;
1216
}
1217
const cdUri = URI.file(cdPrefix.directory);
1218
if (isEqual(cdUri, workingDirectory)) {
1219
return { commandLine: cdPrefix.command };
1220
}
1221
return undefined;
1222
}
1223
1224
function formatShellInvocation(invocation: ChatToolInvocationPart, toolCall: ShellTool, _editId?: string, workingDirectory?: URI): void {
1225
const args = toolCall.arguments;
1226
const command = args.command ?? '';
1227
const isPowershell = toolCall.toolName === 'powershell';
1228
const presentationOverrides = getCdPresentationOverrides(command, isPowershell, workingDirectory);
1229
invocation.invocationMessage = args.description ? new MarkdownString(args.description) : '';
1230
invocation.toolSpecificData = {
1231
commandLine: {
1232
original: presentationOverrides?.commandLine ?? command
1233
},
1234
language: isPowershell ? 'powershell' : 'bash',
1235
presentationOverrides
1236
} as ChatTerminalToolInvocationData;
1237
}
1238
function formatShellInvocationCompleted(invocation: ChatToolInvocationPart, toolCall: ShellTool, result: ToolCallResult, workingDirectory?: URI): void {
1239
const resultContent = result.result?.content || '';
1240
// Exit code will be at the end of the result in the last line in the form of `<exited with exit code ${output.exitCode}>`,
1241
const exitCodeStr = resultContent ? /<exited with exit code (\d+)>$/.exec(resultContent)?.[1] : undefined;
1242
const exitCode = exitCodeStr ? parseInt(exitCodeStr, 10) : undefined;
1243
// Lets remove the last line containing the exit code from the output.
1244
const text = (exitCode !== undefined ? resultContent.replace(/<exited with exit code \d+>$/, '').trimEnd() : resultContent).replace(/\n/g, '\r\n');
1245
const isPowershell = toolCall.toolName === 'powershell';
1246
const presentationOverrides = getCdPresentationOverrides(toolCall.arguments.command, isPowershell, workingDirectory);
1247
const toolSpecificData: ChatTerminalToolInvocationData = {
1248
commandLine: {
1249
original: presentationOverrides?.commandLine ?? toolCall.arguments.command
1250
},
1251
language: isPowershell ? 'powershell' : 'bash',
1252
presentationOverrides,
1253
state: {
1254
exitCode
1255
},
1256
output: {
1257
text
1258
}
1259
};
1260
invocation.toolSpecificData = toolSpecificData;
1261
}
1262
function formatSearchToolInvocation(invocation: ChatToolInvocationPart, toolCall: SearchCodeSubagentTool | GLobTool | GrepTool): void {
1263
if (toolCall.toolName === 'glob') {
1264
const searchInPath = toolCall.arguments.path ? ` in \`${toolCall.arguments.path}\`` : '';
1265
invocation.invocationMessage = `Search for files matching \`${toolCall.arguments.pattern}\`${searchInPath}`;
1266
invocation.pastTenseMessage = `Searched for files matching \`${toolCall.arguments.pattern}\`${searchInPath}`;
1267
} else if (toolCall.toolName === 'grep' || toolCall.toolName === 'rg') {
1268
const searchInPath = toolCall.arguments.path ? ` in \`${toolCall.arguments.path}\`` : '';
1269
invocation.invocationMessage = `Search for files matching \`${toolCall.arguments.pattern}\`${searchInPath}`;
1270
invocation.pastTenseMessage = `Searched for files matching \`${toolCall.arguments.pattern}\`${searchInPath}`;
1271
} else if (toolCall.toolName === 'search_code_subagent') {
1272
invocation.invocationMessage = `Criteria: ${toolCall.arguments.query}`;
1273
invocation.pastTenseMessage = `Searched code for: ${toolCall.arguments.query}`;
1274
}
1275
}
1276
1277
function formatSearchToolInvocationCompleted(invocation: ChatToolInvocationPart, toolCall: SearchCodeSubagentTool | GLobTool | GrepTool, result: ToolCallResult, workingDirectory?: URI): void {
1278
if (toolCall.toolName === 'glob' || toolCall.toolName === 'grep' || toolCall.toolName === 'rg') {
1279
const messagesIndicatingNoMatches = ['Pattern matched but no output generated', 'Pattern matched but no files found', 'No matches found', 'no files matched the pattern'].map(msg => msg.toLowerCase());
1280
1281
let searchPath = toolCall.arguments.path ? Uri.file(toolCall.arguments.path) : workingDirectory;
1282
if (toolCall.arguments.path && workingDirectory && searchPath && !isAbsolutePath(searchPath)) {
1283
searchPath = Uri.joinPath(workingDirectory, toolCall.arguments.path);
1284
}
1285
const searchInPath = toolCall.arguments.path ? ` in \`${toolCall.arguments.path}\`` : '';
1286
let files: string[] = [];
1287
if (Array.isArray(result.result?.contents) && result.result.contents.length > 0 && result.result.contents[0].type === 'terminal' && typeof result.result.contents[0].text === 'string') {
1288
const matches = result.result.contents[0].text.trim();
1289
const noMatches = matches.length === 0;
1290
files = !noMatches && result.success ? matches.split('\n') : [];
1291
} else {
1292
const noMatches = messagesIndicatingNoMatches.some(msg => (result.result?.content || '').toLowerCase().includes(msg));
1293
files = !noMatches && result.success && typeof result.result?.content === 'string' ? result.result.content.split('\n') : [];
1294
}
1295
1296
const successMessage = files.length ? `, ${files.length} result${files.length > 1 ? 's' : ''}` : '.';
1297
invocation.pastTenseMessage = `Searched for files matching \`${toolCall.arguments.pattern}\`${searchInPath}${successMessage}`;
1298
invocation.toolSpecificData = {
1299
values: files.map(file => {
1300
if (!file.startsWith('./') || !searchPath) {
1301
return Uri.file(file);
1302
}
1303
return Uri.joinPath(searchPath, file.substring(2));
1304
})
1305
};
1306
}
1307
}
1308
1309
function formatCodeReviewInvocation(invocation: ChatToolInvocationPart, toolCall: CodeReviewTool): void {
1310
invocation.invocationMessage = toolCall.arguments.prTitle;
1311
invocation.originMessage = toolCall.arguments.prDescription;
1312
}
1313
1314
function formatReplyToCommentInvocation(invocation: ChatToolInvocationPart, toolCall: ReplyToCommentTool): void {
1315
invocation.invocationMessage = `Replying to comment_id ${toolCall.arguments.comment_id}`;
1316
invocation.pastTenseMessage = `Replied to comment_id ${toolCall.arguments.comment_id}`;
1317
invocation.originMessage = toolCall.arguments.reply;
1318
}
1319
1320
function formatShowFileInvocation(invocation: ChatToolInvocationPart, toolCall: ShowFileTool): void {
1321
const args = toolCall.arguments;
1322
if (!args.path) {
1323
return;
1324
}
1325
const display = formatUriForFileWidget(Uri.file(args.path));
1326
if (args.diff) {
1327
invocation.invocationMessage = new MarkdownString(l10n.t("Showing diff of {0}", display));
1328
invocation.pastTenseMessage = new MarkdownString(l10n.t("Showed diff of {0}", display));
1329
} else if (args.view_range && args.view_range.length >= 2) {
1330
const [start, end] = args.view_range;
1331
invocation.invocationMessage = new MarkdownString(l10n.t("Showing {0}, lines {1} to {2}", display, start, end));
1332
invocation.pastTenseMessage = new MarkdownString(l10n.t("Showed {0}, lines {1} to {2}", display, start, end));
1333
} else if (args.view_range && args.view_range.length === 1) {
1334
const [line] = args.view_range;
1335
invocation.invocationMessage = new MarkdownString(l10n.t("Showing {0}, line {1}", display, line));
1336
invocation.pastTenseMessage = new MarkdownString(l10n.t("Showed {0}, line {1}", display, line));
1337
} else {
1338
invocation.invocationMessage = new MarkdownString(l10n.t("Showing {0}", display));
1339
invocation.pastTenseMessage = new MarkdownString(l10n.t("Showed {0}", display));
1340
}
1341
}
1342
1343
function formatProposeWorkInvocation(invocation: ChatToolInvocationPart, toolCall: ProposeWorkTool): void {
1344
invocation.invocationMessage = toolCall.arguments.workTitle || 'Proposing work';
1345
invocation.pastTenseMessage = toolCall.arguments.workTitle || 'Proposed work';
1346
}
1347
1348
function formatTaskCompleteInvocation(invocation: ChatToolInvocationPart, toolCall: TaskCompleteTool): void {
1349
invocation.invocationMessage = toolCall.arguments.summary || l10n.t('Marking task as complete');
1350
invocation.pastTenseMessage = toolCall.arguments.summary || l10n.t('Task completed');
1351
}
1352
1353
function formatAskUserInvocation(invocation: ChatToolInvocationPart, toolCall: AskUserTool): void {
1354
if ('question' in toolCall.arguments) {
1355
invocation.invocationMessage = toolCall.arguments.question || l10n.t('Asking user a question');
1356
invocation.pastTenseMessage = toolCall.arguments.question || l10n.t('Asked user a question');
1357
return;
1358
}
1359
1360
invocation.invocationMessage = toolCall.arguments.message || l10n.t('Asking user for input');
1361
invocation.pastTenseMessage = toolCall.arguments.message || l10n.t('Asked user for input');
1362
}
1363
1364
function formatSkillInvocation(invocation: ChatToolInvocationPart, toolCall: SkillTool): void {
1365
invocation.invocationMessage = l10n.t("Invoking skill: {0}", toolCall.arguments.skill);
1366
invocation.pastTenseMessage = l10n.t("Invoked skill: {0}", toolCall.arguments.skill);
1367
}
1368
1369
function formatTaskInvocation(invocation: ChatToolInvocationPart, toolCall: TaskTool): void {
1370
invocation.invocationMessage = toolCall.arguments.description || l10n.t('Delegating task');
1371
invocation.pastTenseMessage = toolCall.arguments.description || l10n.t('Delegated task');
1372
invocation.toolSpecificData = new ChatSubagentToolInvocationData(
1373
toolCall.arguments.description,
1374
toolCall.arguments.agent_type,
1375
toolCall.arguments.prompt);
1376
}
1377
1378
function formatTaskInvocationCompleted(invocation: ChatToolInvocationPart, _toolCall: TaskTool, result: ToolCallResult): void {
1379
if (invocation.toolSpecificData instanceof ChatSubagentToolInvocationData && result.success && result.result?.content) {
1380
const content = typeof result.result.content === 'string' ? result.result.content : JSON.stringify(result.result.content, null, 2);
1381
invocation.toolSpecificData.result = content;
1382
}
1383
}
1384
1385
function formatReadAgentInvocation(invocation: ChatToolInvocationPart, toolCall: ReadAgentTool): void {
1386
invocation.invocationMessage = l10n.t("Reading agent {0}", toolCall.arguments.agent_id);
1387
invocation.pastTenseMessage = l10n.t("Read agent {0}", toolCall.arguments.agent_id);
1388
}
1389
1390
function formatExitPlanModeInvocation(invocation: ChatToolInvocationPart, toolCall: ExitPlanModeTool): void {
1391
invocation.invocationMessage = toolCall.arguments.summary ? l10n.t('Presenting plan') : l10n.t('Exiting plan mode');
1392
invocation.pastTenseMessage = l10n.t('Exited plan mode');
1393
}
1394
1395
function formatSqlInvocation(invocation: ChatToolInvocationPart, toolCall: SqlTool): void {
1396
invocation.invocationMessage = toolCall.arguments.description || l10n.t('Executing SQL query');
1397
invocation.pastTenseMessage = toolCall.arguments.description || l10n.t('Executed SQL query');
1398
}
1399
1400
function formatLspInvocation(invocation: ChatToolInvocationPart, toolCall: LspTool): void {
1401
const op = toolCall.arguments.operation;
1402
const file = toolCall.arguments.file;
1403
if (file) {
1404
const display = formatUriForFileWidget(Uri.file(file));
1405
invocation.invocationMessage = new MarkdownString(l10n.t("LSP {0} on {1}", op, display));
1406
} else {
1407
invocation.invocationMessage = l10n.t("LSP {0}", op);
1408
}
1409
}
1410
1411
function formatCreatePullRequestInvocation(invocation: ChatToolInvocationPart, toolCall: CreatePullRequestTool): void {
1412
invocation.invocationMessage = toolCall.arguments.title || l10n.t('Creating pull request');
1413
invocation.pastTenseMessage = toolCall.arguments.title || l10n.t('Created pull request');
1414
if (toolCall.arguments.description) {
1415
invocation.originMessage = toolCall.arguments.description;
1416
}
1417
}
1418
1419
function formatStoreMemoryInvocation(invocation: ChatToolInvocationPart, toolCall: StoreMemoryTool): void {
1420
invocation.invocationMessage = l10n.t("Storing memory: {0}", toolCall.arguments.subject);
1421
invocation.pastTenseMessage = l10n.t("Stored memory: {0}", toolCall.arguments.subject);
1422
}
1423
1424
function formatApplyPatchInvocation(invocation: ChatToolInvocationPart, _toolCall: ApplyPatchTool): void {
1425
invocation.invocationMessage = l10n.t('Applying patch to files');
1426
invocation.pastTenseMessage = l10n.t('Applied patch to files');
1427
}
1428
1429
function formatWriteAgentInvocation(invocation: ChatToolInvocationPart, toolCall: WriteAgentTool): void {
1430
invocation.invocationMessage = l10n.t("Writing to agent {0}", toolCall.arguments.agent_id);
1431
invocation.pastTenseMessage = l10n.t("Wrote to agent {0}", toolCall.arguments.agent_id);
1432
}
1433
1434
function formatMcpValidateInvocation(invocation: ChatToolInvocationPart, toolCall: McpValidateTool): void {
1435
const display = toolCall.arguments.path ? formatUriForFileWidget(Uri.file(toolCall.arguments.path)) : '';
1436
invocation.invocationMessage = display
1437
? new MarkdownString(l10n.t("Validating MCP config {0}", display))
1438
: l10n.t('Validating MCP config');
1439
invocation.pastTenseMessage = display
1440
? new MarkdownString(l10n.t("Validated MCP config {0}", display))
1441
: l10n.t('Validated MCP config');
1442
}
1443
1444
function formatToolSearchInvocation(invocation: ChatToolInvocationPart, toolCall: ToolSearchTool): void {
1445
invocation.invocationMessage = l10n.t("Searching tools matching: {0}", toolCall.arguments.pattern);
1446
invocation.pastTenseMessage = l10n.t("Searched tools matching: {0}", toolCall.arguments.pattern);
1447
}
1448
1449
1450
export function parseTodoMarkdown(markdown: string): { title: string; todoList: Array<{ id: number; title: string; status: 'not-started' | 'in-progress' | 'completed' }> } {
1451
const lines = markdown.split('\n');
1452
const todoList: Array<{ id: number; title: string; status: 'not-started' | 'in-progress' | 'completed' }> = [];
1453
let title = 'Updated todo list';
1454
let inCodeBlock = false;
1455
let currentItem: { title: string; status: 'not-started' | 'in-progress' | 'completed' } | null = null;
1456
1457
for (const line of lines) {
1458
// Track code fences
1459
if (line.trim().startsWith('```') || line.trim().startsWith('~~~')) {
1460
inCodeBlock = !inCodeBlock;
1461
continue;
1462
}
1463
1464
// Skip lines inside code blocks
1465
if (inCodeBlock) {
1466
continue;
1467
}
1468
1469
// Extract title from first non-empty line
1470
if (title === 'Updated todo list' && line.trim()) {
1471
const trimmed = line.trim();
1472
// Check if it's not a list item
1473
if (!trimmed.match(/^[-*+]\s+\[.\]/) && !trimmed.match(/^\d+[.)]\s+\[.\]/)) {
1474
// Strip leading # for headings
1475
title = trimmed.replace(/^#+\s*/, '');
1476
}
1477
}
1478
1479
// Parse checklist items (unordered and ordered lists)
1480
const unorderedMatch = line.match(/^\s*[-*+]\s+\[(.?)\]\s*(.*)$/);
1481
const orderedMatch = line.match(/^\s*\d+[.)]\s+\[(.?)\]\s*(.*)$/);
1482
const match = unorderedMatch || orderedMatch;
1483
1484
if (match) {
1485
// Save previous item if exists
1486
if (currentItem && currentItem.title.trim()) {
1487
todoList.push({
1488
id: todoList.length + 1,
1489
title: currentItem.title.trim(),
1490
status: currentItem.status
1491
});
1492
}
1493
1494
const checkboxChar = match[1];
1495
const itemTitle = match[2];
1496
1497
// Map checkbox character to status
1498
let status: 'not-started' | 'in-progress' | 'completed';
1499
if (checkboxChar === 'x' || checkboxChar === 'X') {
1500
status = 'completed';
1501
} else if (checkboxChar === '>' || checkboxChar === '~') {
1502
status = 'in-progress';
1503
} else {
1504
status = 'not-started';
1505
}
1506
1507
currentItem = { title: itemTitle, status };
1508
} else if (currentItem && line.trim() && (line.startsWith(' ') || line.startsWith('\t'))) {
1509
// Continuation line - append to current item
1510
currentItem.title += ' ' + line.trim();
1511
}
1512
}
1513
1514
// Add the last item
1515
if (currentItem && currentItem.title.trim()) {
1516
todoList.push({
1517
id: todoList.length + 1,
1518
title: currentItem.title.trim(),
1519
status: currentItem.status
1520
});
1521
}
1522
1523
return { title, todoList };
1524
}
1525
1526
function formatUpdateTodoInvocation(invocation: ChatToolInvocationPart, toolCall: UpdateTodoTool): void {
1527
const args = toolCall.arguments;
1528
const parsed = args.todos ? parseTodoMarkdown(args.todos) : { title: '', todoList: [] };
1529
if (!args.todos || !parsed) {
1530
invocation.invocationMessage = 'Updating todo list';
1531
invocation.pastTenseMessage = 'Updated todo list';
1532
return;
1533
}
1534
1535
invocation.invocationMessage = parsed.title;
1536
invocation.toolSpecificData = {
1537
output: '',
1538
input: [`# ${parsed.title}`, ...parsed.todoList.map(item => `- [${item.status === 'completed' ? 'x' : item.status === 'in-progress' ? '>' : ' '}] ${item.title}`)].join('\n')
1539
};
1540
}
1541
1542
function formatUpdateTodoInvocationCompleted(invocation: ChatToolInvocationPart, toolCall: UpdateTodoTool, result: ToolCallResult): void {
1543
const input = (invocation.toolSpecificData ? (invocation.toolSpecificData as ChatSimpleToolResultData).input : '') || '';
1544
invocation.toolSpecificData = {
1545
output: typeof result.result?.content === 'string' ? result.result.content : JSON.stringify(result.result?.content || '', null, 2),
1546
input
1547
};
1548
}
1549
1550
1551
/**
1552
* Check whether a SQL query writes to the `todos` or `todo_deps` table.
1553
* Pure reads (SELECT) are ignored to avoid unnecessary widget refreshes.
1554
*/
1555
export function isTodoRelatedSqlQuery(query: string): boolean {
1556
const normalized = query.replace(/\s+/g, ' ').toLowerCase();
1557
const targetsTodoTable = /\btodos\b/.test(normalized) || /\btodo_deps\b/.test(normalized);
1558
if (!targetsTodoTable) {
1559
return false;
1560
}
1561
return /\b(insert|update|delete|create|drop|alter)\b/.test(normalized);
1562
}
1563
1564
interface SqlTodoItem {
1565
readonly id: string;
1566
readonly title: string;
1567
readonly description: string;
1568
readonly status: 'pending' | 'in_progress' | 'done' | 'blocked';
1569
}
1570
1571
function mapSqlStatusToTodoStatus(status: string): 'not-started' | 'in-progress' | 'completed' {
1572
switch (status) {
1573
case 'done':
1574
return 'completed';
1575
case 'in_progress':
1576
return 'in-progress';
1577
case 'pending':
1578
case 'blocked':
1579
default:
1580
return 'not-started';
1581
}
1582
}
1583
1584
/**
1585
* Update the todo list widget from SQL todo items queried from the session database.
1586
*/
1587
export async function updateTodoListFromSqlItems(
1588
items: readonly SqlTodoItem[],
1589
toolsService: IToolsService,
1590
toolInvocationToken: ChatParticipantToolToken,
1591
token: CancellationToken
1592
): Promise<void> {
1593
await toolsService.invokeTool(ToolName.CoreManageTodoList, {
1594
input: {
1595
operation: 'write',
1596
todoList: items.map((item, i) => ({
1597
id: i,
1598
title: item.title,
1599
description: item.description || '',
1600
status: mapSqlStatusToTodoStatus(item.status)
1601
} satisfies IManageTodoListToolInputParams['todoList'][number])),
1602
} satisfies IManageTodoListToolInputParams,
1603
toolInvocationToken,
1604
}, token);
1605
}
1606
1607
export async function clearTodoList(toolsService: IToolsService,
1608
toolInvocationToken: ChatParticipantToolToken,
1609
token: CancellationToken): Promise<void> {
1610
await toolsService.invokeTool(ToolName.CoreManageTodoList, {
1611
input: {
1612
operation: 'write',
1613
todoList: []
1614
} satisfies IManageTodoListToolInputParams,
1615
toolInvocationToken,
1616
}, token);
1617
}
1618
1619
interface IManageTodoListToolInputParams {
1620
readonly operation?: 'write' | 'read'; // Optional in write-only mode
1621
readonly todoList: readonly {
1622
readonly id: number;
1623
readonly title: string;
1624
readonly description: string;
1625
readonly status: 'not-started' | 'in-progress' | 'completed';
1626
}[];
1627
}
1628
1629
/**
1630
* No-op formatter for tool invocations that do not require custom formatting.
1631
* The `toolCall` parameter is unused and present for interface consistency.
1632
*/
1633
function emptyInvocation(_invocation: ChatToolInvocationPart, _toolCall: UnknownToolCall): void {
1634
// No custom formatting needed
1635
}
1636
1637
/**
1638
* No-op post-invocation formatter for tools whose completion requires no custom display.
1639
*/
1640
function emptyToolInvocationCompleted(_invocation: ChatToolInvocationPart, _toolCall: UnknownToolCall, _result: ToolCallResult): void {
1641
// No custom post-invocation formatting needed
1642
}
1643
1644
1645
function genericToolInvocationCompleted(invocation: ChatToolInvocationPart, toolCall: UnknownToolCall, result: ToolCallResult): void {
1646
if (result.success && result.result?.content) {
1647
invocation.toolSpecificData = {
1648
output: typeof result.result.content === 'string' ? result.result.content : JSON.stringify(result.result.content, null, 2),
1649
input: toolCall.arguments ? JSON.stringify(toolCall.arguments, null, 2) : ''
1650
};
1651
}
1652
1653
}
1654
1655
1656
/**
1657
* Mock tools service that can be configured for different test scenarios
1658
*/
1659
export class FakeToolsService implements IToolsService {
1660
readonly _serviceBrand: undefined;
1661
1662
private readonly _onWillInvokeTool = new Emitter<IOnWillInvokeToolEvent>();
1663
readonly onWillInvokeTool = this._onWillInvokeTool.event;
1664
1665
readonly tools: ReadonlyArray<LanguageModelToolInformation> = [];
1666
readonly copilotTools = new Map<ToolName, ICopilotTool<unknown>>();
1667
1668
private _confirmationResult: 'yes' | 'no' = 'yes';
1669
private _invokeToolCalls: Array<{ name: string; input: unknown }> = [];
1670
1671
setConfirmationResult(result: 'yes' | 'no'): void {
1672
this._confirmationResult = result;
1673
}
1674
1675
get invokeToolCalls(): ReadonlyArray<{ name: string; input: unknown }> {
1676
return this._invokeToolCalls;
1677
}
1678
1679
clearCalls(): void {
1680
this._invokeToolCalls = [];
1681
}
1682
1683
invokeToolWithEndpoint(name: string, options: LanguageModelToolInvocationOptions<unknown>, endpoint: IChatEndpoint | undefined, token: CancellationToken): Thenable<LanguageModelToolResult2> {
1684
return this.invokeTool(name, options);
1685
}
1686
1687
modelSpecificTools: IObservable<{ definition: LanguageModelToolDefinition; tool: ICopilotTool<unknown> }[]> = constObservable([]);
1688
1689
async invokeTool(
1690
name: string,
1691
options: LanguageModelToolInvocationOptions<unknown>
1692
): Promise<LanguageModelToolResult2> {
1693
this._invokeToolCalls.push({ name, input: options.input });
1694
1695
if (name === ToolName.CoreConfirmationTool || name === ToolName.CoreTerminalConfirmationTool) {
1696
return {
1697
content: [new LanguageModelTextPart(this._confirmationResult)]
1698
};
1699
}
1700
1701
if (name === 'vscode_reviewPlan') {
1702
if (this._confirmationResult === 'no') {
1703
return { content: [new LanguageModelTextPart(JSON.stringify({ rejected: true }))] };
1704
}
1705
const input = options.input as { actions?: Array<{ label: string }> } | undefined;
1706
const firstAction = input?.actions?.[0]?.label;
1707
return { content: [new LanguageModelTextPart(JSON.stringify({ action: firstAction, rejected: false }))] };
1708
}
1709
1710
return { content: [] };
1711
}
1712
1713
getCopilotTool(): ICopilotTool<unknown> | undefined {
1714
return undefined;
1715
}
1716
1717
getTool(): LanguageModelToolInformation | undefined {
1718
return undefined;
1719
}
1720
1721
getToolByToolReferenceName(): LanguageModelToolInformation | undefined {
1722
return undefined;
1723
}
1724
1725
validateToolInput(): IToolValidationResult {
1726
return { inputObj: {} };
1727
}
1728
1729
validateToolName(): string | undefined {
1730
return undefined;
1731
}
1732
1733
getEnabledTools(): LanguageModelToolInformation[] {
1734
return [];
1735
}
1736
}
1737
1738
1739
/**
1740
* CLI sends 'synthetic' user messages for cases such as Skill invocations.
1741
* We need to ensure these user.messages are not treated as regular user messages in the UI, which could cause confusion as they may not be directly from the user.
1742
*/
1743
export function isSyntheticUserMessage(event: Extract<SessionEvent, { type: 'user.message' }>): boolean {
1744
return event.type === 'user.message' && !!event.data.source && (event.data.source ?? '').toLowerCase() !== 'user';
1745
}
1746
1747