Path: blob/main/extensions/copilot/src/extension/chatSessions/copilotcli/common/copilotCLITools.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 type { SessionEvent, ToolExecutionCompleteEvent, ToolExecutionStartEvent } from '@github/copilot/sdk';6import * as l10n from '@vscode/l10n';7import type { CancellationToken, ChatParticipantToolToken, ChatPromptReference, ChatSimpleToolResultData, ChatTerminalToolInvocationData, ExtendedChatResponsePart, LanguageModelToolDefinition, LanguageModelToolInformation, LanguageModelToolInvocationOptions, LanguageModelToolResult2 } from 'vscode';8import { ILogger } from '../../../../platform/log/common/logService';9import { IChatEndpoint } from '../../../../platform/networking/common/networking';10import { isLocation } from '../../../../util/common/types';11import { findLast } from '../../../../util/vs/base/common/arraysFind';12import { decodeBase64 } from '../../../../util/vs/base/common/buffer';13import { Emitter } from '../../../../util/vs/base/common/event';14import { ResourceMap } from '../../../../util/vs/base/common/map';15import { constObservable, IObservable } from '../../../../util/vs/base/common/observable';16import { isAbsolutePath, isEqual } from '../../../../util/vs/base/common/resources';17import { URI } from '../../../../util/vs/base/common/uri';18import { ChatMcpToolInvocationData, ChatReferenceBinaryData, ChatRequestTurn2, ChatResponseCodeblockUriPart, ChatResponseMarkdownPart, ChatResponsePullRequestPart, ChatResponseTextEditPart, ChatResponseThinkingProgressPart, ChatResponseTurn2, ChatSubagentToolInvocationData, ChatToolInvocationPart, LanguageModelTextPart, Location, MarkdownString, McpToolInvocationContentData, Range, Uri } from '../../../../vscodeTypes';19import type { MCP } from '../../../common/modelContextProtocol';20import { ToolName } from '../../../tools/common/toolNames';21import { ICopilotTool } from '../../../tools/common/toolsRegistry';22import { IOnWillInvokeToolEvent, IToolsService, IToolValidationResult } from '../../../tools/common/toolsService';23import { formatUriForFileWidget } from '../../../tools/common/toolUtils';24import { StoredModeInstructions } from '../../common/chatSessionMetadataStore';25import { extractChatPromptReferences, getFolderAttachmentPath } from './copilotCLIPrompt';26import { IChatDelegationSummaryService } from './delegationSummaryService';272829interface CreateTool {30toolName: 'create';31arguments: {32path: string;33file_text?: string;34};35}3637interface ViewTool {38toolName: 'view';39arguments: {40path: string;41view_range?: [number, number];42forceReadLargeFiles?: boolean;43};44}4546interface EditTool {47toolName: 'edit' | 'str_replace';48arguments: {49path: string;50old_str?: string;51new_str?: string;52};53}5455interface StrReplaceTool {56toolName: 'str_replace';57arguments: {58path: string;59old_str?: string;60new_str?: string;61};62}6364interface InsertTool {65toolName: 'insert';66arguments: {67path: string;68insert_line?: number;69new_str: string;70};71}7273interface ShellTool {74toolName: 'bash' | 'powershell';75arguments: {76command: string;77description: string;78shellId?: string;79mode?: 'sync' | 'async';80detach?: boolean;81initial_wait?: number;82};83}8485interface WriteShellTool {86toolName: 'write_bash' | 'write_powershell';87arguments: {88shellId: string;89input?: string;90delay: number;91};92}9394interface ReadShellTool {95toolName: 'read_bash' | 'read_powershell';96arguments: {97shellId: string;98delay: number;99};100}101102interface StopShellTool {103toolName: 'stop_bash' | 'stop_powershell';104arguments: {105shellId: string;106};107}108109interface ListShellTool {110toolName: 'list_bash' | 'list_powershell';111arguments: Record<string, never>;112}113114interface GrepTool {115toolName: 'grep' | 'rg';116arguments: {117pattern: string;118path?: string;119output_mode?: 'content' | 'files_with_matches' | 'count';120glob?: string;121type?: string;122'-i'?: boolean;123'-A'?: number;124'-B'?: number;125'-C'?: number;126'-n'?: boolean;127head_limit?: number;128multiline?: boolean;129};130}131132interface GLobTool {133toolName: 'glob';134arguments: {135pattern: string;136path?: string;137};138}139140type ReportIntentTool = {141toolName: 'report_intent';142arguments: {143intent: string;144};145};146type ThinkTool = {147toolName: 'think';148arguments: {149thought: string;150};151};152153type UpdateTodoTool = {154toolName: 'update_todo';155arguments: {156todos: string;157};158};159160type ReportProgressTool = {161toolName: 'report_progress';162arguments: {163commitMessage: string;164prDescription: string;165};166};167168type WebFetchTool = {169toolName: 'web_fetch';170arguments: {171url: string;172max_length?: number;173start_index?: number;174raw?: boolean;175};176};177178type WebSearchTool = {179toolName: 'web_search';180arguments: {181query: string;182};183};184185type SearchCodeSubagentTool = {186toolName: 'search_code_subagent';187arguments: {188query: string;189};190};191192type ReplyToCommentTool = {193toolName: 'reply_to_comment';194arguments: {195reply: string;196comment_id: string;197};198};199200type CodeReviewTool = {201toolName: 'code_review';202arguments: {203prTitle: string;204prDescription: string;205};206};207208type ShowFileTool = {209toolName: 'show_file';210arguments: {211path: string;212view_range?: number[];213diff?: boolean;214};215};216217type FetchCopilotCliDocumentationTool = {218toolName: 'fetch_copilot_cli_documentation';219arguments: Record<string, never>;220};221222type ProposeWorkTool = {223toolName: 'propose_work';224arguments: {225workType: 'code_change' | 'task';226workTitle: string;227workDescription: string;228};229};230231type TaskCompleteTool = {232toolName: 'task_complete';233arguments: {234summary?: string;235};236};237238type AskUserTool = {239toolName: 'ask_user';240arguments:241| {242question: string;243choices?: string[];244allow_freeform?: boolean;245}246| {247message: string;248requestedSchema: {249properties: Record<string, unknown>;250required?: string[];251};252};253};254255type SkillTool = {256toolName: 'skill';257arguments: {258skill: string;259};260};261262type TaskTool = {263toolName: 'task';264arguments: {265description: string;266prompt: string;267agent_type: string;268model?: string;269mode?: 'sync' | 'background';270};271};272273type ListAgentsTool = {274toolName: 'list_agents';275arguments: {276include_completed?: boolean;277};278};279280type ReadAgentTool = {281toolName: 'read_agent';282arguments: {283agent_id: string;284wait?: boolean;285timeout?: number;286};287};288289type ExitPlanModeTool = {290toolName: 'exit_plan_mode';291arguments: {292summary: string;293actions?: string[];294recommendedAction?: string;295};296};297298type SqlTool = {299toolName: 'sql';300arguments: {301description: string;302query: string;303database?: 'session' | 'session_store';304};305};306307type LspTool = {308toolName: 'lsp';309arguments: {310operation: string;311file?: string;312line?: number;313character?: number;314newName?: string;315includeDeclaration?: boolean;316query?: string;317language?: string;318};319};320321type CreatePullRequestTool = {322toolName: 'create_pull_request';323arguments: {324title: string;325description?: string;326draft?: boolean;327};328};329330type DependencyCheckerTool = {331toolName: 'gh-advisory-database';332arguments: {333dependencies: { version: string; name: string; ecosystem: string }[];334};335};336337type StoreMemoryTool = {338toolName: 'store_memory';339arguments: {340subject: string;341fact: string;342citations: string;343reason: string;344category: string;345};346};347348type ParallelValidationTool = {349toolName: 'parallel_validation';350arguments: Record<string, never>;351};352353type ApplyPatchTool = {354toolName: 'apply_patch';355arguments: {356input?: string;357patch?: string;358};359};360361type WriteAgentTool = {362toolName: 'write_agent';363arguments: {364agent_id: string;365message: string;366};367};368369type McpReloadTool = {370toolName: 'mcp_reload';371arguments: Record<string, never>;372};373374type McpValidateTool = {375toolName: 'mcp_validate';376arguments: {377path: string;378};379};380381type ToolSearchTool = {382toolName: 'tool_search_tool_regex';383arguments: {384pattern: string;385limit?: number;386};387};388389type CodeQLCheckerTool = {390toolName: 'codeql_checker';391arguments: Record<string, never>;392};393394395type StringReplaceArgumentTypes = CreateTool | ViewTool | StrReplaceTool | EditTool | InsertTool;396type ToStringReplaceEditorArguments<T extends StringReplaceArgumentTypes> = {397command: T['toolName'];398} & T['arguments'];399type StringReplaceEditorTool = {400toolName: 'str_replace_editor';401arguments: ToStringReplaceEditorArguments<CreateTool> | ToStringReplaceEditorArguments<ViewTool> | ToStringReplaceEditorArguments<EditTool> | ToStringReplaceEditorArguments<StrReplaceTool> |402ToStringReplaceEditorArguments<InsertTool>;403};404export type ToolInfo = StringReplaceEditorTool | EditTool | CreateTool | ViewTool | InsertTool |405ShellTool | WriteShellTool | ReadShellTool | StopShellTool | ListShellTool |406GrepTool | GLobTool |407ReportIntentTool | ThinkTool | ReportProgressTool |408SearchCodeSubagentTool |409ReplyToCommentTool | CodeReviewTool | WebFetchTool | UpdateTodoTool | WebSearchTool |410ShowFileTool | FetchCopilotCliDocumentationTool | ProposeWorkTool | TaskCompleteTool |411AskUserTool | SkillTool | TaskTool | ListAgentsTool | ReadAgentTool | WriteAgentTool |412ExitPlanModeTool | SqlTool | LspTool | CreatePullRequestTool | DependencyCheckerTool | StoreMemoryTool | ParallelValidationTool |413ApplyPatchTool | McpReloadTool | McpValidateTool | ToolSearchTool | CodeQLCheckerTool;414415export type ToolCall = ToolInfo & {416toolCallId: string;417mcpServerName?: string | undefined;418mcpToolName?: string | undefined;419};420export type UnknownToolCall = { toolName: string; arguments: unknown; toolCallId: string };421422function isInstructionAttachmentPath(path: string): boolean {423const normalizedPath = path.replace(/\\/g, '/');424return normalizedPath.endsWith('/.github/copilot-instructions.md')425|| (normalizedPath.includes('/.github/instructions/') && normalizedPath.endsWith('.md'));426}427428export function isCopilotCliEditToolCall(data: { toolName: string; arguments?: unknown }): boolean {429const toolCall = data as ToolCall;430if (toolCall.toolName === 'str_replace_editor') {431return toolCall.arguments.command !== 'view';432}433return toolCall.toolName === 'create' || toolCall.toolName === 'edit';434}435436export function isCopilotCLIToolThatCouldRequirePermissions(event: ToolExecutionStartEvent): boolean {437const toolCall = event.data as unknown as ToolCall;438if (isCopilotCliEditToolCall(toolCall)) {439return true;440}441if (toolCall.mcpServerName) {442return false;443}444if (toolCall.toolName === 'bash' || toolCall.toolName === 'powershell') {445return true;446}447if (toolCall.toolName === 'view') {448return true;449}450return false;451}452453export function getAffectedUrisForEditTool(data: { toolName: string; arguments?: unknown }): URI[] {454const toolCall = data as ToolCall;455// Old versions used str_replace_editor456// This should be removed eventually457// TODO @DonJayamanne verify with SDK & Padawan folk.458if (toolCall.toolName === 'str_replace_editor' && toolCall.arguments.command !== 'view' && typeof toolCall.arguments.path === 'string') {459return [URI.file(toolCall.arguments.path)];460}461462if ((toolCall.toolName === 'create' || toolCall.toolName === 'edit') && typeof toolCall.arguments.path === 'string') {463return [URI.file(toolCall.arguments.path)];464}465466return [];467}468469export function stripReminders(text: string): string {470// Remove any <reminder> ... </reminder> blocks, including newlines471// Also remove <current_datetime> ... </current_datetime> blocks472// Also remove <pr_metadata .../> tags473return text474.replace(/<reminder>[\s\S]*?<\/reminder>\s*/g, '')475.replace(/<attachments>[\s\S]*?<\/attachments>\s*/g, '')476.replace(/<userRequest>[\s\S]*?<\/userRequest>\s*/g, '')477.replace(/<user_query>[\s\S]*?<\/user_query>\s*/g, '')478.replace(/<context>[\s\S]*?<\/context>\s*/g, '')479.replace(/<current_datetime>[\s\S]*?<\/current_datetime>\s*/g, '')480.replace(/<pr_metadata[^>]*\/?>\s*/g, '')481.trim();482}483484/**485* Extract PR metadata from assistant message content486*/487function extractPRMetadata(content: string): { cleanedContent: string; prPart?: ChatResponsePullRequestPart } {488const prMetadataRegex = /<pr_metadata\s+uri="(?<uri>[^"]+)"\s+title="(?<title>[^"]+)"\s+description="(?<description>[^"]+)"\s+author="(?<author>[^"]+)"\s+linkTag="(?<linkTag>[^"]+)"\s*\/?>/;489const match = content.match(prMetadataRegex);490491if (match?.groups) {492const { title, description, author, linkTag } = match.groups;493// Unescape XML entities494const unescapeXml = (text: string) => text495.replace(/'/g, `'`)496.replace(/"/g, '"')497.replace(/>/g, '>')498.replace(/</g, '<')499.replace(/&/g, '&');500501const prPart = new ChatResponsePullRequestPart(502{ command: 'github.copilot.chat.openPullRequestReroute', title: l10n.t('View Pull Request {0}', linkTag), arguments: [Number(linkTag.substring(1))] },503unescapeXml(title),504unescapeXml(description),505unescapeXml(author),506unescapeXml(linkTag)507);508509const cleanedContent = content.replace(match[0], '').trim();510return { cleanedContent, prPart };511}512513return { cleanedContent: content };514}515516export interface RequestIdDetails {517readonly requestId: string;518readonly toolIdEditMap: Record<string, string>;519readonly modeInstructions?: StoredModeInstructions;520}521522/**523* Build chat history from SDK events for VS Code chat session524* Converts SDKEvents into ChatRequestTurn2 and ChatResponseTurn2 objects525*/526export 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)[] {527const turns: (ChatRequestTurn2 | ChatResponseTurn2)[] = [];528let currentResponseParts: ExtendedChatResponsePart[] = [];529const pendingToolInvocations = new Map<string, [ChatToolInvocationPart | ChatResponseMarkdownPart | ChatResponseThinkingProgressPart, toolData: ToolCall, parentToolCallId: string | undefined]>();530531let details: RequestIdDetails | undefined;532let isFirstUserMessage = true;533const currentAssistantMessage: { chunks: string[] } = { chunks: [] };534const processedMessages = new Set<string>();535536function processAssistantMessage(content: string) {537// Extract PR metadata if present538const { cleanedContent, prPart } = extractPRMetadata(content);539// Add PR part first if it exists540if (prPart) {541currentResponseParts.push(prPart);542}543if (cleanedContent) {544currentResponseParts.push(545new ChatResponseMarkdownPart(new MarkdownString(cleanedContent))546);547}548}549550function flushPendingAssistantMessage() {551if (currentAssistantMessage.chunks.length > 0) {552const content = currentAssistantMessage.chunks.join('');553currentAssistantMessage.chunks = [];554processAssistantMessage(content);555}556}557const lastUserMessageId = findLast(events, event => event.type === 'user.message' && !isSyntheticUserMessage(event))?.id;558for (const event of events) {559if (event.type !== 'assistant.message') {560flushPendingAssistantMessage();561}562563switch (event.type) {564case 'user.message': {565if (isSyntheticUserMessage(event)) {566continue;567}568details = getVSCodeRequestId(event.id);569// Flush any pending response parts before adding user message570if (currentResponseParts.length > 0) {571turns.push(new ChatResponseTurn2(currentResponseParts, {}, ''));572currentResponseParts = [];573}574// Filter out vscode instruction files from references when building session history575// TODO@rebornix filter instructions should be rendered as "references" in chat response like normal chat.576const references: ChatPromptReference[] = [];577578try {579references.push(...extractChatPromptReferences(event.data.content || ''));580} catch (ex) {581// ignore errors from parsing references582}583const existingReferences = new ResourceMap<Range | undefined>();584references.forEach(ref => {585if (URI.isUri(ref.value)) {586existingReferences.set(ref.value, undefined);587} else if (isLocation(ref.value)) {588existingReferences.set(ref.value.uri, ref.value.range);589}590});591((event.data.attachments || []))592.filter(attachment => attachment.type === 'selection' || attachment.type === 'github_reference' || attachment.type === 'blob' ? true : !isInstructionAttachmentPath(attachment.path))593.forEach(attachment => {594if (attachment.type === 'github_reference') {595return;596}597if (attachment.type === 'selection') {598const range = attachment.displayName ? getRangeInPrompt(event.data.content || '', attachment.displayName) : undefined;599const uri = Uri.file(attachment.filePath);600if (existingReferences.has(uri) && !existingReferences.get(uri)) {601return; // Skip duplicates602}603references.push({604id: attachment.filePath,605name: attachment.displayName,606value: 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)),607range608});609} else if (attachment.type === 'file' || attachment.type === 'directory') {610const range = attachment.displayName ? getRangeInPrompt(event.data.content || '', attachment.displayName) : undefined;611const attachmentPath = attachment.type === 'directory' ?612getFolderAttachmentPath(attachment.path) :613attachment.path;614const uri = Uri.file(attachmentPath);615if (existingReferences.has(uri)) {616return; // Skip duplicates617}618references.push({619id: attachment.path,620name: attachment.displayName,621value: uri,622range623});624} else if (attachment.type === 'blob') {625const binaryDataSupplier = async () => {626try {627return decodeBase64(attachment.data).buffer;628} catch (error) {629logger.error(error, `Failed to decode blob attachment ${attachment.displayName || ''}`);630throw error;631}632};633references.push({634id: `${attachment.displayName || ''}-${attachment.mimeType}-${attachment.type}`,635name: attachment.displayName || '',636value: new ChatReferenceBinaryData(attachment.mimeType, binaryDataSupplier),637});638}639});640641let prompt = stripReminders(event.data.content || '');642const info = isFirstUserMessage ? delegationSummaryService.extractPrompt(sessionId, prompt) : undefined;643if (info) {644prompt = info.prompt;645references.push(info.reference);646}647isFirstUserMessage = false;648let modeInstructions2 = details?.modeInstructions ? {649uri: details.modeInstructions.uri ? Uri.parse(details.modeInstructions.uri) : undefined,650name: details.modeInstructions.name,651content: details.modeInstructions.content,652metadata: details.modeInstructions.metadata,653isBuiltin: details.modeInstructions.isBuiltin,654} : undefined;655656if (lastUserMessageId && event.id === lastUserMessageId && defaultModeInstructionsForLastRequest && !modeInstructions2) {657modeInstructions2 = modeInstructions2 ?? {658uri: defaultModeInstructionsForLastRequest.uri ? Uri.parse(defaultModeInstructionsForLastRequest.uri) : undefined,659name: defaultModeInstructionsForLastRequest.name,660content: defaultModeInstructionsForLastRequest.content,661metadata: defaultModeInstructionsForLastRequest.metadata,662isBuiltin: defaultModeInstructionsForLastRequest.isBuiltin,663};664}665let commandPrefix = '';666switch (event.data.agentMode) {667case 'autopilot': {668commandPrefix = '/autopilot ';669break;670}671case 'plan': {672commandPrefix = '/plan ';673break;674}675}676677turns.push(new ChatRequestTurn2(`${commandPrefix}${prompt}`, undefined, references, '', [], undefined, details?.requestId ?? event.id, modelId, modeInstructions2));678break;679}680case 'assistant.message_delta': {681if (typeof event.data.deltaContent === 'string') {682// Skip sub-agent markdown — it will be captured in the subagent tool's result683if (!event.data.parentToolCallId) {684processedMessages.add(event.data.messageId);685currentAssistantMessage.chunks.push(event.data.deltaContent);686}687}688break;689}690case 'session.error': {691currentResponseParts.push(new ChatResponseMarkdownPart(`\n\n❌ Error: (${event.data.errorType}) ${event.data.message}`));692break;693}694case 'assistant.message': {695// Skip sub-agent markdown — it will be captured in the subagent tool's result696if (event.data.content && !processedMessages.has(event.data.messageId) && !event.data.parentToolCallId) {697processAssistantMessage(event.data.content);698}699break;700}701case 'tool.execution_start': {702const responsePart = processToolExecutionStart(event, pendingToolInvocations, workingDirectory);703if (responsePart instanceof ChatResponseThinkingProgressPart) {704currentResponseParts.push(responsePart);705}706break;707}708case 'subagent.started': {709enrichToolInvocationWithSubagentMetadata(710event.data.toolCallId,711event.data.agentDisplayName,712event.data.agentDescription,713pendingToolInvocations714);715break;716}717case 'subagent.completed':718case 'subagent.failed': {719// Completion is already handled by tool.execution_complete for the task tool720break;721}722case 'tool.execution_complete': {723const [responsePart, toolCall] = processToolExecutionComplete(event, pendingToolInvocations, logger, workingDirectory) ?? [undefined, undefined];724if (responsePart && toolCall && !(responsePart instanceof ChatResponseThinkingProgressPart)) {725const editId = details?.toolIdEditMap ? details.toolIdEditMap[toolCall.toolCallId] : undefined;726const editedUris = getAffectedUrisForEditTool(toolCall);727if (!(responsePart instanceof ChatResponseMarkdownPart) && isCopilotCliEditToolCall(toolCall) && editId && editedUris.length > 0) {728responsePart.presentation = 'hidden';729currentResponseParts.push(responsePart);730for (const uri of editedUris) {731currentResponseParts.push(new ChatResponseMarkdownPart('\n````\n'));732currentResponseParts.push(new ChatResponseCodeblockUriPart(uri, true, editId));733currentResponseParts.push(new ChatResponseTextEditPart(uri, []));734currentResponseParts.push(new ChatResponseTextEditPart(uri, true));735currentResponseParts.push(new ChatResponseMarkdownPart('\n````\n'));736}737} else {738currentResponseParts.push(responsePart);739}740}741break;742}743}744}745746flushPendingAssistantMessage();747748if (currentResponseParts.length > 0) {749turns.push(new ChatResponseTurn2(currentResponseParts, lastResponseDetails ? { details: lastResponseDetails } : {}, ''));750}751752return turns;753}754755function getRangeInPrompt(prompt: string, referencedName: string): [number, number] | undefined {756referencedName = `#${referencedName}`;757const index = prompt.indexOf(referencedName);758if (index >= 0) {759return [index, index + referencedName.length];760}761return undefined;762}763764/**765* Converts MCP {@link MCP.ContentBlock}[] values produced by MCP tool execution into766* VS Code {@link McpToolInvocationContentData}[] objects for rendering in the chat UI.767*768* MCP ContentBlocks represent heterogeneous pieces of tool output such as text, images,769* audio, embedded resources, or resource links. This helper normalizes those different770* content shapes into a common binary+MIME-type representation that the VS Code chat771* tool invocation renderer understands, so that MCP tool results can be displayed772* consistently alongside other chat responses.773*/774function convertMcpContentToToolInvocationData(result: ToolExecutionCompleteEvent['data']['result'], logger: ILogger): McpToolInvocationContentData[] {775const output: McpToolInvocationContentData[] = [];776const encoder = new TextEncoder();777778if (!Array.isArray(result?.contents) || result.contents.length === 0) {779return output;780}781782for (const block of result.contents) {783try {784switch (block.type) {785case 'text':786// Convert text to UTF-8 bytes with text/plain mime type787output.push(new McpToolInvocationContentData(788encoder.encode(block.text),789'text/plain'790));791break;792793case 'image':794// Decode base64 image data and preserve mime type795output.push(new McpToolInvocationContentData(796decodeBase64(block.data).buffer,797block.mimeType798));799break;800801case 'audio':802// Decode base64 audio data and preserve mime type803output.push(new McpToolInvocationContentData(804decodeBase64(block.data).buffer,805block.mimeType806));807break;808809case 'resource': {810// Handle embedded resource (text or blob)811const resource = block.resource;812if ('text' in resource) {813// TextResourceContents814const mimeType = resource.mimeType || 'text/plain';815output.push(new McpToolInvocationContentData(816encoder.encode(resource.text),817mimeType818));819} else if ('blob' in resource) {820// BlobResourceContents821const mimeType = resource.mimeType || 'application/octet-stream';822output.push(new McpToolInvocationContentData(823decodeBase64(resource.blob).buffer,824mimeType825));826}827break;828}829830case 'resource_link': {831// Format resource link as readable text with name and URI832const displayName = block.title || block.name;833const linkText = displayName ? `Resource: ${displayName}\nURI: ${block.uri}` : block.uri;834output.push(new McpToolInvocationContentData(835encoder.encode(linkText),836'text/plain'837));838break;839}840}841} catch (error) {842// Log conversion errors but continue processing other blocks843logger.error(error, `Failed to convert MCP content block of type ${block.type}:`);844}845}846847return output;848}849850/**851* Enriches an existing pending tool invocation with subagent metadata from a `subagent.started` event.852* The `subagent.started` event carries richer metadata (display name, description) than the `task`853* tool's arguments, so we use it to update the `ChatSubagentToolInvocationData` on the tool invocation.854*/855export function enrichToolInvocationWithSubagentMetadata(856toolCallId: string,857agentDisplayName: string,858agentDescription: string | undefined,859pendingToolInvocations: Map<string, [ChatToolInvocationPart | ChatResponseMarkdownPart | ChatResponseThinkingProgressPart, toolData: ToolCall, parentToolCallId: string | undefined]>860): void {861const invocation = pendingToolInvocations.get(toolCallId);862if (!invocation) {863return;864}865const [part] = invocation;866if (!(part instanceof ChatToolInvocationPart)) {867return;868}869870if (part.toolSpecificData instanceof ChatSubagentToolInvocationData) {871part.toolSpecificData.agentName = agentDisplayName;872if (agentDescription) {873part.toolSpecificData.description = agentDescription;874}875}876}877878export function processToolExecutionStart(event: ToolExecutionStartEvent, pendingToolInvocations: Map<string, [ChatToolInvocationPart | ChatResponseMarkdownPart | ChatResponseThinkingProgressPart, toolData: ToolCall, parentToolCallId: string | undefined]>, workingDirectory?: URI): ChatToolInvocationPart | ChatResponseMarkdownPart | ChatResponseThinkingProgressPart | undefined {879const toolInvocation = createCopilotCLIToolInvocation(event.data as ToolCall, undefined, workingDirectory);880if (toolInvocation) {881if (toolInvocation instanceof ChatToolInvocationPart && event.data.parentToolCallId) {882// Resolve to the root ancestor so all descendants are grouped under the883// top-level subagent container instead of creating intermediate containers.884toolInvocation.subAgentInvocationId = resolveRootSubagentId(event.data.parentToolCallId, pendingToolInvocations);885886// Nested task tools should not create their own subagent container —887// clear ChatSubagentToolInvocationData so the widget treats them as888// regular child tool invocations within the parent container.889if (toolInvocation.toolSpecificData instanceof ChatSubagentToolInvocationData) {890toolInvocation.toolSpecificData = undefined;891}892}893// Store pending invocation to update with result later894pendingToolInvocations.set(event.data.toolCallId, [toolInvocation, event.data as ToolCall, event.data.parentToolCallId]);895}896return toolInvocation;897}898899/**900* Walks the parentToolCallId chain to find the root (top-level) subagent toolCallId.901* This ensures all nested tools are grouped under the outermost subagent container.902*/903function resolveRootSubagentId(904parentToolCallId: string,905pendingToolInvocations: Map<string, [ChatToolInvocationPart | ChatResponseMarkdownPart | ChatResponseThinkingProgressPart, toolData: ToolCall, parentToolCallId: string | undefined]>906): string {907let currentId = parentToolCallId;908const visited = new Set<string>();909while (true) {910if (visited.has(currentId)) {911break; // Prevent infinite loops912}913visited.add(currentId);914const parent = pendingToolInvocations.get(currentId);915if (!parent || !parent[2]) {916break; // No further parent — currentId is the root917}918currentId = parent[2];919}920return currentId;921}922923export 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 {924const invocation = pendingToolInvocations.get(event.data.toolCallId);925pendingToolInvocations.delete(event.data.toolCallId);926927if (invocation && invocation[0] instanceof ChatToolInvocationPart) {928invocation[0].isComplete = true;929invocation[0].isError = !!event.data.error;930invocation[0].invocationMessage = event.data.error?.message || invocation[0].invocationMessage;931if (!event.data.success && (event.data.error?.code === 'rejected' || event.data.error?.code === 'denied')) {932invocation[0].isConfirmed = false;933} else {934invocation[0].isConfirmed = true;935}936const toolCall = invocation[1];937if (Object.hasOwn(ToolFriendlyNameAndHandlers, toolCall.toolName)) {938const [, , postFormatter] = ToolFriendlyNameAndHandlers[toolCall.toolName];939try {940(postFormatter as PostInvocationFormatter)(invocation[0], toolCall, event.data, workingDirectory);941} catch (err) {942logger.error(err, `Failed to format tool invocation completion for tool: ${toolCall.toolName}`);943try {944genericToolInvocationCompleted(invocation[0], toolCall, event.data);945} catch {946// ignore947}948}949} else if (toolCall.mcpServerName && toolCall.mcpToolName) {950// Use tool arguments as input, formatted as JSON951const input = toolCall.arguments ? JSON.stringify(toolCall.arguments, null, 2) : '';952const output = convertMcpContentToToolInvocationData(event.data.result, logger);953if (output.length) {954invocation[0].toolSpecificData = {955input,956output957} satisfies ChatMcpToolInvocationData;958} else {959// If we don't have any structured output, at least include the raw text of the result for visibility in the chat UI.960genericToolInvocationCompleted(invocation[0], toolCall, event.data);961}962} else {963if (!!event.data.error && event.data.error?.message) {964invocation[0] = new ChatToolInvocationPart(invocation[0].toolName, invocation[0].toolCallId, event.data.error.message);965invocation[0].isComplete = true;966invocation[0].isError = true;967invocation[0].invocationMessage = event.data.error?.message || invocation[0].invocationMessage;968invocation[0].pastTenseMessage = `Used tool: ${invocation[0].toolName}`;969} else {970genericToolInvocationCompleted(invocation[0], toolCall, event.data);971}972}973}974975return invocation;976}977978/**979* Creates a formatted tool invocation part for CopilotCLI tools980*/981export function createCopilotCLIToolInvocation(data: {982toolCallId: string; toolName: string; arguments?: unknown; mcpServerName?: string | undefined;983mcpToolName?: string | undefined;984}, editId?: string, workingDirectory?: URI, logger?: ILogger): ChatToolInvocationPart | ChatResponseMarkdownPart | ChatResponseThinkingProgressPart | undefined {985if (!Object.hasOwn(ToolFriendlyNameAndHandlers, data.toolName)) {986const mcpServer = l10n.t('MCP Server');987const toolName = data.mcpServerName && data.mcpToolName ? `${data.mcpServerName}, ${data.mcpToolName} (${mcpServer})` : data.toolName;988const invocation = new ChatToolInvocationPart(toolName ?? 'unknown', data.toolCallId ?? '');989invocation.isConfirmed = false;990invocation.isComplete = false;991invocation.invocationMessage = l10n.t("Using tool: {0}", toolName ?? 'unknown');992invocation.pastTenseMessage = l10n.t("Used tool: {0}", toolName ?? 'unknown');993return invocation;994}995996const toolCall = data as ToolCall;997// Ensures arguments is at least an empty object998toolCall.arguments = toolCall.arguments ?? {};999if (toolCall.toolName === 'report_intent') {1000return undefined; // Ignore these for now1001}1002if (toolCall.toolName === 'think') {1003if (toolCall.arguments && typeof toolCall.arguments.thought === 'string') {1004return new ChatResponseThinkingProgressPart(toolCall.arguments.thought);1005}1006return undefined;1007}1008if (toolCall.toolName === 'show_file') {1009// Currently there's no good way to render this to the user.1010// Its a way to draw users attention to a file/code block.1011// Generally models render the codeblock in the response, but here we have a tool call.1012// Its a WIP, no clear way to render in CLI either, hence decided to hide in VS Code.1013return undefined;1014}1015if (toolCall.toolName === 'task_complete') {1016if (toolCall.arguments.summary) {1017const markdownContent = new MarkdownString();1018markdownContent.appendMarkdown(toolCall.arguments.summary);1019return new ChatResponseMarkdownPart(markdownContent);1020}1021return undefined;1022}10231024const [friendlyToolName, formatter] = ToolFriendlyNameAndHandlers[toolCall.toolName];1025const invocation = new ChatToolInvocationPart(friendlyToolName ?? toolCall.toolName ?? 'unknown', toolCall.toolCallId ?? '');1026invocation.isConfirmed = false;1027invocation.isComplete = false;10281029try {1030(formatter as Formatter)(invocation, toolCall, editId, workingDirectory);1031} catch (err) {1032logger?.error(err, `Failed to format tool invocation for tool: ${toolCall.toolName}`);1033}1034return invocation;1035}10361037type Formatter = (invocation: ChatToolInvocationPart, toolCall: ToolCall, editId?: string, workingDirectory?: URI) => void;1038type PostInvocationFormatter = (invocation: ChatToolInvocationPart, toolCall: ToolCall, result: ToolCallResult, workingDirectory?: URI) => void;1039type ToolCallFor<T extends ToolCall['toolName']> = Extract<ToolCall, { toolName: T }>;1040type ToolCallResult = ToolExecutionCompleteEvent['data'];10411042const 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] } = {1043'str_replace_editor': [l10n.t('Edit File'), formatStrReplaceEditorInvocation, genericToolInvocationCompleted],1044'edit': [l10n.t('Edit File'), formatEditToolInvocation, emptyToolInvocationCompleted],1045'str_replace': [l10n.t('Edit File'), formatEditToolInvocation, emptyToolInvocationCompleted],1046'create': [l10n.t('Create File'), formatCreateToolInvocation, emptyToolInvocationCompleted],1047'insert': [l10n.t('Edit File'), formatInsertToolInvocation, emptyToolInvocationCompleted],1048'view': [l10n.t('Read'), formatViewToolInvocation, emptyToolInvocationCompleted],1049'bash': [l10n.t('Run Shell Command'), formatShellInvocation, formatShellInvocationCompleted],1050'powershell': [l10n.t('Run Shell Command'), formatShellInvocation, formatShellInvocationCompleted],1051'write_bash': [l10n.t('Write to Bash'), emptyInvocation, genericToolInvocationCompleted],1052'write_powershell': [l10n.t('Write to PowerShell'), emptyInvocation, genericToolInvocationCompleted],1053'read_bash': [l10n.t('Read Terminal'), emptyInvocation, genericToolInvocationCompleted],1054'read_powershell': [l10n.t('Read Terminal'), emptyInvocation, genericToolInvocationCompleted],1055'stop_bash': [l10n.t('Stop Terminal Session'), emptyInvocation, genericToolInvocationCompleted],1056'stop_powershell': [l10n.t('Stop Terminal Session'), emptyInvocation, genericToolInvocationCompleted],1057'grep': [l10n.t('Search'), formatSearchToolInvocation, formatSearchToolInvocationCompleted],1058'rg': [l10n.t('Search'), formatSearchToolInvocation, formatSearchToolInvocationCompleted],1059'glob': [l10n.t('Search'), formatSearchToolInvocation, formatSearchToolInvocationCompleted],1060'search_code_subagent': [l10n.t('Search Code'), formatSearchToolInvocation, emptyToolInvocationCompleted],1061'reply_to_comment': [l10n.t('Reply to Comment'), formatReplyToCommentInvocation, genericToolInvocationCompleted],1062'code_review': [l10n.t('Code Review'), formatCodeReviewInvocation, genericToolInvocationCompleted],1063'report_intent': [l10n.t('Report Intent'), emptyInvocation, genericToolInvocationCompleted],1064'think': [l10n.t('Thinking'), emptyInvocation, genericToolInvocationCompleted],1065'report_progress': [l10n.t('Progress update'), formatProgressToolInvocation, genericToolInvocationCompleted],1066'web_fetch': [l10n.t('Fetch Web Content'), emptyInvocation, genericToolInvocationCompleted],1067'web_search': [l10n.t('Web Search'), emptyInvocation, genericToolInvocationCompleted],1068'update_todo': [l10n.t('Update Todo'), formatUpdateTodoInvocation, formatUpdateTodoInvocationCompleted],1069'show_file': [l10n.t('Show File'), formatShowFileInvocation, genericToolInvocationCompleted],1070'fetch_copilot_cli_documentation': [l10n.t('Fetch Documentation'), emptyInvocation, genericToolInvocationCompleted],1071'propose_work': [l10n.t('Propose Work'), formatProposeWorkInvocation, genericToolInvocationCompleted],1072'task_complete': [l10n.t('Task Complete'), formatTaskCompleteInvocation, genericToolInvocationCompleted],1073'ask_user': [l10n.t('Ask User'), formatAskUserInvocation, genericToolInvocationCompleted],1074'skill': [l10n.t('Invoke Skill'), formatSkillInvocation, genericToolInvocationCompleted],1075'task': [l10n.t('Delegate Task'), formatTaskInvocation, formatTaskInvocationCompleted],1076'list_agents': [l10n.t('List Agents'), emptyInvocation, genericToolInvocationCompleted],1077'read_agent': [l10n.t('Read Agent'), formatReadAgentInvocation, genericToolInvocationCompleted],1078'exit_plan_mode': [l10n.t('Exit Plan Mode'), formatExitPlanModeInvocation, genericToolInvocationCompleted],1079'sql': [l10n.t('Execute SQL'), formatSqlInvocation, genericToolInvocationCompleted],1080'lsp': [l10n.t('Language Server'), formatLspInvocation, genericToolInvocationCompleted],1081'create_pull_request': [l10n.t('Create Pull Request'), formatCreatePullRequestInvocation, genericToolInvocationCompleted],1082'gh-advisory-database': [l10n.t('Check Dependencies'), emptyInvocation, genericToolInvocationCompleted],1083'store_memory': [l10n.t('Store Memory'), formatStoreMemoryInvocation, genericToolInvocationCompleted],1084'list_bash': [l10n.t('List Shell Sessions'), emptyInvocation, genericToolInvocationCompleted],1085'list_powershell': [l10n.t('List Shell Sessions'), emptyInvocation, genericToolInvocationCompleted],1086'parallel_validation': [l10n.t('Validate Changes'), emptyInvocation, genericToolInvocationCompleted],1087'apply_patch': [l10n.t('Apply Patch'), formatApplyPatchInvocation, genericToolInvocationCompleted],1088'write_agent': [l10n.t('Write to Agent'), formatWriteAgentInvocation, genericToolInvocationCompleted],1089'mcp_reload': [l10n.t('Reload MCP Config'), emptyInvocation, genericToolInvocationCompleted],1090'mcp_validate': [l10n.t('Validate MCP Config'), formatMcpValidateInvocation, genericToolInvocationCompleted],1091'tool_search_tool_regex': [l10n.t('Search Tools'), formatToolSearchInvocation, genericToolInvocationCompleted],1092'codeql_checker': [l10n.t('CodeQL Security Scan'), emptyInvocation, genericToolInvocationCompleted],1093};109410951096function formatProgressToolInvocation(invocation: ChatToolInvocationPart, toolCall: ReportProgressTool): void {1097const args = toolCall.arguments;1098invocation.invocationMessage = args.prDescription?.trim() || 'Progress Update';1099if (args.commitMessage) {1100invocation.originMessage = `Commit: ${args.commitMessage}`;1101}1102}1103110411051106function formatViewToolInvocation(invocation: ChatToolInvocationPart, toolCall: ViewTool): void {1107const args = toolCall.arguments;11081109if (!args.path) {1110return;1111} else if (args.view_range && args.view_range.length === 2 && args.view_range[1] >= args.view_range[0] && args.view_range[0] >= 0) {1112const [start, end] = args.view_range;1113const location = new Location(Uri.file(args.path), new Range(start === 0 ? start : start - 1, 0, end, 0));1114const display = formatUriForFileWidget(location);1115const localizedMessage = start === end1116? l10n.t("Reading {0}, line {1}", display, start)1117: l10n.t("Reading {0}, lines {1} to {2}", display, start, end);1118const localizedPastTenseMessage = start === end1119? l10n.t("Read {0}, line {1}", display, start)1120: l10n.t("Read {0}, lines {1} to {2}", display, start, end);1121invocation.invocationMessage = new MarkdownString(localizedMessage);1122invocation.pastTenseMessage = new MarkdownString(localizedPastTenseMessage);1123} else {1124const display = formatUriForFileWidget(Uri.file(args.path));1125invocation.invocationMessage = new MarkdownString(l10n.t("Read {0}", display));1126}1127}11281129function formatStrReplaceEditorInvocation(invocation: ChatToolInvocationPart, toolCall: StringReplaceEditorTool, editId?: string): void {1130if (!toolCall.arguments.path) {1131return;1132}1133const args = toolCall.arguments;1134const display = formatUriForFileWidget(Uri.file(args.path));1135switch (args.command) {1136case 'view':1137formatViewToolInvocation(invocation, { toolName: 'view', arguments: args } as ViewTool);1138break;1139case 'edit':1140formatEditToolInvocation(invocation, { toolName: 'edit', arguments: args } as EditTool);1141break;1142case 'insert':1143formatInsertToolInvocation(invocation, { toolName: 'insert', arguments: args } as InsertTool);1144break;1145case 'create':1146formatCreateToolInvocation(invocation, { toolName: 'create', arguments: args } as CreateTool);1147break;1148default:1149invocation.invocationMessage = new MarkdownString(l10n.t("Modified {0}", display));1150}1151}11521153function formatInsertToolInvocation(invocation: ChatToolInvocationPart, toolCall: InsertTool): void {1154const args = toolCall.arguments;1155if (args.path) {1156invocation.invocationMessage = new MarkdownString(l10n.t("Inserted text in {0}", formatUriForFileWidget(Uri.file(args.path))));1157}1158}11591160function formatEditToolInvocation(invocation: ChatToolInvocationPart, toolCall: EditTool, editId?: string): void {1161const args = toolCall.arguments;1162const display = args.path ? formatUriForFileWidget(Uri.file(args.path)) : '';11631164invocation.invocationMessage = display1165? new MarkdownString(l10n.t("Editing {0}", display))1166: new MarkdownString(l10n.t("Editing file"));1167invocation.pastTenseMessage = display1168? new MarkdownString(l10n.t("Edited {0}", display))1169: new MarkdownString(l10n.t("Edited file"));1170}117111721173function formatCreateToolInvocation(invocation: ChatToolInvocationPart, toolCall: CreateTool, editId?: string): void {1174const args = toolCall.arguments;1175const display = args.path ? formatUriForFileWidget(Uri.file(args.path)) : '';11761177if (display) {1178invocation.invocationMessage = new MarkdownString(l10n.t("Creating {0}", display));1179invocation.pastTenseMessage = new MarkdownString(l10n.t("Created {0}", display));1180} else {1181invocation.invocationMessage = new MarkdownString(l10n.t("Creating file"));1182invocation.pastTenseMessage = new MarkdownString(l10n.t("Created file"));1183}1184}11851186/**1187* Extracts a `cd <dir> &&` (or PowerShell equivalent) prefix from a command line,1188* returning the directory and remaining command.1189*/1190export function extractCdPrefix(commandLine: string, isPowershell: boolean): { directory: string; command: string } | undefined {1191const cdPrefixMatch = commandLine.match(1192isPowershell1193? /^(?:cd(?: \/d)?|Set-Location(?: -Path)?) (?<dir>"[^"]*"|[^\s]+) ?(?:&&|;)\s+(?<suffix>.+)$/i1194: /^cd (?<dir>"[^"]*"|[^\s]+) &&\s+(?<suffix>.+)$/1195);1196const cdDir = cdPrefixMatch?.groups?.dir;1197const cdSuffix = cdPrefixMatch?.groups?.suffix;1198if (cdDir && cdSuffix) {1199let cdDirPath = cdDir;1200if (cdDirPath.startsWith('"') && cdDirPath.endsWith('"')) {1201cdDirPath = cdDirPath.slice(1, -1);1202}1203return { directory: cdDirPath, command: cdSuffix };1204}1205return undefined;1206}12071208/**1209* Returns presentationOverrides only when the cd prefix directory matches the working directory.1210*/1211export function getCdPresentationOverrides(commandLine: string, isPowershell: boolean, workingDirectory?: URI): { commandLine: string } | undefined {1212const cdPrefix = extractCdPrefix(commandLine, isPowershell);1213if (!cdPrefix || !workingDirectory) {1214return undefined;1215}1216const cdUri = URI.file(cdPrefix.directory);1217if (isEqual(cdUri, workingDirectory)) {1218return { commandLine: cdPrefix.command };1219}1220return undefined;1221}12221223function formatShellInvocation(invocation: ChatToolInvocationPart, toolCall: ShellTool, _editId?: string, workingDirectory?: URI): void {1224const args = toolCall.arguments;1225const command = args.command ?? '';1226const isPowershell = toolCall.toolName === 'powershell';1227const presentationOverrides = getCdPresentationOverrides(command, isPowershell, workingDirectory);1228invocation.invocationMessage = args.description ? new MarkdownString(args.description) : '';1229invocation.toolSpecificData = {1230commandLine: {1231original: presentationOverrides?.commandLine ?? command1232},1233language: isPowershell ? 'powershell' : 'bash',1234presentationOverrides1235} as ChatTerminalToolInvocationData;1236}1237function formatShellInvocationCompleted(invocation: ChatToolInvocationPart, toolCall: ShellTool, result: ToolCallResult, workingDirectory?: URI): void {1238const resultContent = result.result?.content || '';1239// Exit code will be at the end of the result in the last line in the form of `<exited with exit code ${output.exitCode}>`,1240const exitCodeStr = resultContent ? /<exited with exit code (\d+)>$/.exec(resultContent)?.[1] : undefined;1241const exitCode = exitCodeStr ? parseInt(exitCodeStr, 10) : undefined;1242// Lets remove the last line containing the exit code from the output.1243const text = (exitCode !== undefined ? resultContent.replace(/<exited with exit code \d+>$/, '').trimEnd() : resultContent).replace(/\n/g, '\r\n');1244const isPowershell = toolCall.toolName === 'powershell';1245const presentationOverrides = getCdPresentationOverrides(toolCall.arguments.command, isPowershell, workingDirectory);1246const toolSpecificData: ChatTerminalToolInvocationData = {1247commandLine: {1248original: presentationOverrides?.commandLine ?? toolCall.arguments.command1249},1250language: isPowershell ? 'powershell' : 'bash',1251presentationOverrides,1252state: {1253exitCode1254},1255output: {1256text1257}1258};1259invocation.toolSpecificData = toolSpecificData;1260}1261function formatSearchToolInvocation(invocation: ChatToolInvocationPart, toolCall: SearchCodeSubagentTool | GLobTool | GrepTool): void {1262if (toolCall.toolName === 'glob') {1263const searchInPath = toolCall.arguments.path ? ` in \`${toolCall.arguments.path}\`` : '';1264invocation.invocationMessage = `Search for files matching \`${toolCall.arguments.pattern}\`${searchInPath}`;1265invocation.pastTenseMessage = `Searched for files matching \`${toolCall.arguments.pattern}\`${searchInPath}`;1266} else if (toolCall.toolName === 'grep' || toolCall.toolName === 'rg') {1267const searchInPath = toolCall.arguments.path ? ` in \`${toolCall.arguments.path}\`` : '';1268invocation.invocationMessage = `Search for files matching \`${toolCall.arguments.pattern}\`${searchInPath}`;1269invocation.pastTenseMessage = `Searched for files matching \`${toolCall.arguments.pattern}\`${searchInPath}`;1270} else if (toolCall.toolName === 'search_code_subagent') {1271invocation.invocationMessage = `Criteria: ${toolCall.arguments.query}`;1272invocation.pastTenseMessage = `Searched code for: ${toolCall.arguments.query}`;1273}1274}12751276function formatSearchToolInvocationCompleted(invocation: ChatToolInvocationPart, toolCall: SearchCodeSubagentTool | GLobTool | GrepTool, result: ToolCallResult, workingDirectory?: URI): void {1277if (toolCall.toolName === 'glob' || toolCall.toolName === 'grep' || toolCall.toolName === 'rg') {1278const 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());12791280let searchPath = toolCall.arguments.path ? Uri.file(toolCall.arguments.path) : workingDirectory;1281if (toolCall.arguments.path && workingDirectory && searchPath && !isAbsolutePath(searchPath)) {1282searchPath = Uri.joinPath(workingDirectory, toolCall.arguments.path);1283}1284const searchInPath = toolCall.arguments.path ? ` in \`${toolCall.arguments.path}\`` : '';1285let files: string[] = [];1286if (Array.isArray(result.result?.contents) && result.result.contents.length > 0 && result.result.contents[0].type === 'terminal' && typeof result.result.contents[0].text === 'string') {1287const matches = result.result.contents[0].text.trim();1288const noMatches = matches.length === 0;1289files = !noMatches && result.success ? matches.split('\n') : [];1290} else {1291const noMatches = messagesIndicatingNoMatches.some(msg => (result.result?.content || '').toLowerCase().includes(msg));1292files = !noMatches && result.success && typeof result.result?.content === 'string' ? result.result.content.split('\n') : [];1293}12941295const successMessage = files.length ? `, ${files.length} result${files.length > 1 ? 's' : ''}` : '.';1296invocation.pastTenseMessage = `Searched for files matching \`${toolCall.arguments.pattern}\`${searchInPath}${successMessage}`;1297invocation.toolSpecificData = {1298values: files.map(file => {1299if (!file.startsWith('./') || !searchPath) {1300return Uri.file(file);1301}1302return Uri.joinPath(searchPath, file.substring(2));1303})1304};1305}1306}13071308function formatCodeReviewInvocation(invocation: ChatToolInvocationPart, toolCall: CodeReviewTool): void {1309invocation.invocationMessage = toolCall.arguments.prTitle;1310invocation.originMessage = toolCall.arguments.prDescription;1311}13121313function formatReplyToCommentInvocation(invocation: ChatToolInvocationPart, toolCall: ReplyToCommentTool): void {1314invocation.invocationMessage = `Replying to comment_id ${toolCall.arguments.comment_id}`;1315invocation.pastTenseMessage = `Replied to comment_id ${toolCall.arguments.comment_id}`;1316invocation.originMessage = toolCall.arguments.reply;1317}13181319function formatShowFileInvocation(invocation: ChatToolInvocationPart, toolCall: ShowFileTool): void {1320const args = toolCall.arguments;1321if (!args.path) {1322return;1323}1324const display = formatUriForFileWidget(Uri.file(args.path));1325if (args.diff) {1326invocation.invocationMessage = new MarkdownString(l10n.t("Showing diff of {0}", display));1327invocation.pastTenseMessage = new MarkdownString(l10n.t("Showed diff of {0}", display));1328} else if (args.view_range && args.view_range.length >= 2) {1329const [start, end] = args.view_range;1330invocation.invocationMessage = new MarkdownString(l10n.t("Showing {0}, lines {1} to {2}", display, start, end));1331invocation.pastTenseMessage = new MarkdownString(l10n.t("Showed {0}, lines {1} to {2}", display, start, end));1332} else if (args.view_range && args.view_range.length === 1) {1333const [line] = args.view_range;1334invocation.invocationMessage = new MarkdownString(l10n.t("Showing {0}, line {1}", display, line));1335invocation.pastTenseMessage = new MarkdownString(l10n.t("Showed {0}, line {1}", display, line));1336} else {1337invocation.invocationMessage = new MarkdownString(l10n.t("Showing {0}", display));1338invocation.pastTenseMessage = new MarkdownString(l10n.t("Showed {0}", display));1339}1340}13411342function formatProposeWorkInvocation(invocation: ChatToolInvocationPart, toolCall: ProposeWorkTool): void {1343invocation.invocationMessage = toolCall.arguments.workTitle || 'Proposing work';1344invocation.pastTenseMessage = toolCall.arguments.workTitle || 'Proposed work';1345}13461347function formatTaskCompleteInvocation(invocation: ChatToolInvocationPart, toolCall: TaskCompleteTool): void {1348invocation.invocationMessage = toolCall.arguments.summary || l10n.t('Marking task as complete');1349invocation.pastTenseMessage = toolCall.arguments.summary || l10n.t('Task completed');1350}13511352function formatAskUserInvocation(invocation: ChatToolInvocationPart, toolCall: AskUserTool): void {1353if ('question' in toolCall.arguments) {1354invocation.invocationMessage = toolCall.arguments.question || l10n.t('Asking user a question');1355invocation.pastTenseMessage = toolCall.arguments.question || l10n.t('Asked user a question');1356return;1357}13581359invocation.invocationMessage = toolCall.arguments.message || l10n.t('Asking user for input');1360invocation.pastTenseMessage = toolCall.arguments.message || l10n.t('Asked user for input');1361}13621363function formatSkillInvocation(invocation: ChatToolInvocationPart, toolCall: SkillTool): void {1364invocation.invocationMessage = l10n.t("Invoking skill: {0}", toolCall.arguments.skill);1365invocation.pastTenseMessage = l10n.t("Invoked skill: {0}", toolCall.arguments.skill);1366}13671368function formatTaskInvocation(invocation: ChatToolInvocationPart, toolCall: TaskTool): void {1369invocation.invocationMessage = toolCall.arguments.description || l10n.t('Delegating task');1370invocation.pastTenseMessage = toolCall.arguments.description || l10n.t('Delegated task');1371invocation.toolSpecificData = new ChatSubagentToolInvocationData(1372toolCall.arguments.description,1373toolCall.arguments.agent_type,1374toolCall.arguments.prompt);1375}13761377function formatTaskInvocationCompleted(invocation: ChatToolInvocationPart, _toolCall: TaskTool, result: ToolCallResult): void {1378if (invocation.toolSpecificData instanceof ChatSubagentToolInvocationData && result.success && result.result?.content) {1379const content = typeof result.result.content === 'string' ? result.result.content : JSON.stringify(result.result.content, null, 2);1380invocation.toolSpecificData.result = content;1381}1382}13831384function formatReadAgentInvocation(invocation: ChatToolInvocationPart, toolCall: ReadAgentTool): void {1385invocation.invocationMessage = l10n.t("Reading agent {0}", toolCall.arguments.agent_id);1386invocation.pastTenseMessage = l10n.t("Read agent {0}", toolCall.arguments.agent_id);1387}13881389function formatExitPlanModeInvocation(invocation: ChatToolInvocationPart, toolCall: ExitPlanModeTool): void {1390invocation.invocationMessage = toolCall.arguments.summary ? l10n.t('Presenting plan') : l10n.t('Exiting plan mode');1391invocation.pastTenseMessage = l10n.t('Exited plan mode');1392}13931394function formatSqlInvocation(invocation: ChatToolInvocationPart, toolCall: SqlTool): void {1395invocation.invocationMessage = toolCall.arguments.description || l10n.t('Executing SQL query');1396invocation.pastTenseMessage = toolCall.arguments.description || l10n.t('Executed SQL query');1397}13981399function formatLspInvocation(invocation: ChatToolInvocationPart, toolCall: LspTool): void {1400const op = toolCall.arguments.operation;1401const file = toolCall.arguments.file;1402if (file) {1403const display = formatUriForFileWidget(Uri.file(file));1404invocation.invocationMessage = new MarkdownString(l10n.t("LSP {0} on {1}", op, display));1405} else {1406invocation.invocationMessage = l10n.t("LSP {0}", op);1407}1408}14091410function formatCreatePullRequestInvocation(invocation: ChatToolInvocationPart, toolCall: CreatePullRequestTool): void {1411invocation.invocationMessage = toolCall.arguments.title || l10n.t('Creating pull request');1412invocation.pastTenseMessage = toolCall.arguments.title || l10n.t('Created pull request');1413if (toolCall.arguments.description) {1414invocation.originMessage = toolCall.arguments.description;1415}1416}14171418function formatStoreMemoryInvocation(invocation: ChatToolInvocationPart, toolCall: StoreMemoryTool): void {1419invocation.invocationMessage = l10n.t("Storing memory: {0}", toolCall.arguments.subject);1420invocation.pastTenseMessage = l10n.t("Stored memory: {0}", toolCall.arguments.subject);1421}14221423function formatApplyPatchInvocation(invocation: ChatToolInvocationPart, _toolCall: ApplyPatchTool): void {1424invocation.invocationMessage = l10n.t('Applying patch to files');1425invocation.pastTenseMessage = l10n.t('Applied patch to files');1426}14271428function formatWriteAgentInvocation(invocation: ChatToolInvocationPart, toolCall: WriteAgentTool): void {1429invocation.invocationMessage = l10n.t("Writing to agent {0}", toolCall.arguments.agent_id);1430invocation.pastTenseMessage = l10n.t("Wrote to agent {0}", toolCall.arguments.agent_id);1431}14321433function formatMcpValidateInvocation(invocation: ChatToolInvocationPart, toolCall: McpValidateTool): void {1434const display = toolCall.arguments.path ? formatUriForFileWidget(Uri.file(toolCall.arguments.path)) : '';1435invocation.invocationMessage = display1436? new MarkdownString(l10n.t("Validating MCP config {0}", display))1437: l10n.t('Validating MCP config');1438invocation.pastTenseMessage = display1439? new MarkdownString(l10n.t("Validated MCP config {0}", display))1440: l10n.t('Validated MCP config');1441}14421443function formatToolSearchInvocation(invocation: ChatToolInvocationPart, toolCall: ToolSearchTool): void {1444invocation.invocationMessage = l10n.t("Searching tools matching: {0}", toolCall.arguments.pattern);1445invocation.pastTenseMessage = l10n.t("Searched tools matching: {0}", toolCall.arguments.pattern);1446}144714481449export function parseTodoMarkdown(markdown: string): { title: string; todoList: Array<{ id: number; title: string; status: 'not-started' | 'in-progress' | 'completed' }> } {1450const lines = markdown.split('\n');1451const todoList: Array<{ id: number; title: string; status: 'not-started' | 'in-progress' | 'completed' }> = [];1452let title = 'Updated todo list';1453let inCodeBlock = false;1454let currentItem: { title: string; status: 'not-started' | 'in-progress' | 'completed' } | null = null;14551456for (const line of lines) {1457// Track code fences1458if (line.trim().startsWith('```') || line.trim().startsWith('~~~')) {1459inCodeBlock = !inCodeBlock;1460continue;1461}14621463// Skip lines inside code blocks1464if (inCodeBlock) {1465continue;1466}14671468// Extract title from first non-empty line1469if (title === 'Updated todo list' && line.trim()) {1470const trimmed = line.trim();1471// Check if it's not a list item1472if (!trimmed.match(/^[-*+]\s+\[.\]/) && !trimmed.match(/^\d+[.)]\s+\[.\]/)) {1473// Strip leading # for headings1474title = trimmed.replace(/^#+\s*/, '');1475}1476}14771478// Parse checklist items (unordered and ordered lists)1479const unorderedMatch = line.match(/^\s*[-*+]\s+\[(.?)\]\s*(.*)$/);1480const orderedMatch = line.match(/^\s*\d+[.)]\s+\[(.?)\]\s*(.*)$/);1481const match = unorderedMatch || orderedMatch;14821483if (match) {1484// Save previous item if exists1485if (currentItem && currentItem.title.trim()) {1486todoList.push({1487id: todoList.length + 1,1488title: currentItem.title.trim(),1489status: currentItem.status1490});1491}14921493const checkboxChar = match[1];1494const itemTitle = match[2];14951496// Map checkbox character to status1497let status: 'not-started' | 'in-progress' | 'completed';1498if (checkboxChar === 'x' || checkboxChar === 'X') {1499status = 'completed';1500} else if (checkboxChar === '>' || checkboxChar === '~') {1501status = 'in-progress';1502} else {1503status = 'not-started';1504}15051506currentItem = { title: itemTitle, status };1507} else if (currentItem && line.trim() && (line.startsWith(' ') || line.startsWith('\t'))) {1508// Continuation line - append to current item1509currentItem.title += ' ' + line.trim();1510}1511}15121513// Add the last item1514if (currentItem && currentItem.title.trim()) {1515todoList.push({1516id: todoList.length + 1,1517title: currentItem.title.trim(),1518status: currentItem.status1519});1520}15211522return { title, todoList };1523}15241525function formatUpdateTodoInvocation(invocation: ChatToolInvocationPart, toolCall: UpdateTodoTool): void {1526const args = toolCall.arguments;1527const parsed = args.todos ? parseTodoMarkdown(args.todos) : { title: '', todoList: [] };1528if (!args.todos || !parsed) {1529invocation.invocationMessage = 'Updating todo list';1530invocation.pastTenseMessage = 'Updated todo list';1531return;1532}15331534invocation.invocationMessage = parsed.title;1535invocation.toolSpecificData = {1536output: '',1537input: [`# ${parsed.title}`, ...parsed.todoList.map(item => `- [${item.status === 'completed' ? 'x' : item.status === 'in-progress' ? '>' : ' '}] ${item.title}`)].join('\n')1538};1539}15401541function formatUpdateTodoInvocationCompleted(invocation: ChatToolInvocationPart, toolCall: UpdateTodoTool, result: ToolCallResult): void {1542const input = (invocation.toolSpecificData ? (invocation.toolSpecificData as ChatSimpleToolResultData).input : '') || '';1543invocation.toolSpecificData = {1544output: typeof result.result?.content === 'string' ? result.result.content : JSON.stringify(result.result?.content || '', null, 2),1545input1546};1547}154815491550/**1551* Check whether a SQL query writes to the `todos` or `todo_deps` table.1552* Pure reads (SELECT) are ignored to avoid unnecessary widget refreshes.1553*/1554export function isTodoRelatedSqlQuery(query: string): boolean {1555const normalized = query.replace(/\s+/g, ' ').toLowerCase();1556const targetsTodoTable = /\btodos\b/.test(normalized) || /\btodo_deps\b/.test(normalized);1557if (!targetsTodoTable) {1558return false;1559}1560return /\b(insert|update|delete|create|drop|alter)\b/.test(normalized);1561}15621563interface SqlTodoItem {1564readonly id: string;1565readonly title: string;1566readonly description: string;1567readonly status: 'pending' | 'in_progress' | 'done' | 'blocked';1568}15691570function mapSqlStatusToTodoStatus(status: string): 'not-started' | 'in-progress' | 'completed' {1571switch (status) {1572case 'done':1573return 'completed';1574case 'in_progress':1575return 'in-progress';1576case 'pending':1577case 'blocked':1578default:1579return 'not-started';1580}1581}15821583/**1584* Update the todo list widget from SQL todo items queried from the session database.1585*/1586export async function updateTodoListFromSqlItems(1587items: readonly SqlTodoItem[],1588toolsService: IToolsService,1589toolInvocationToken: ChatParticipantToolToken,1590token: CancellationToken1591): Promise<void> {1592await toolsService.invokeTool(ToolName.CoreManageTodoList, {1593input: {1594operation: 'write',1595todoList: items.map((item, i) => ({1596id: i,1597title: item.title,1598description: item.description || '',1599status: mapSqlStatusToTodoStatus(item.status)1600} satisfies IManageTodoListToolInputParams['todoList'][number])),1601} satisfies IManageTodoListToolInputParams,1602toolInvocationToken,1603}, token);1604}16051606export async function clearTodoList(toolsService: IToolsService,1607toolInvocationToken: ChatParticipantToolToken,1608token: CancellationToken): Promise<void> {1609await toolsService.invokeTool(ToolName.CoreManageTodoList, {1610input: {1611operation: 'write',1612todoList: []1613} satisfies IManageTodoListToolInputParams,1614toolInvocationToken,1615}, token);1616}16171618interface IManageTodoListToolInputParams {1619readonly operation?: 'write' | 'read'; // Optional in write-only mode1620readonly todoList: readonly {1621readonly id: number;1622readonly title: string;1623readonly description: string;1624readonly status: 'not-started' | 'in-progress' | 'completed';1625}[];1626}16271628/**1629* No-op formatter for tool invocations that do not require custom formatting.1630* The `toolCall` parameter is unused and present for interface consistency.1631*/1632function emptyInvocation(_invocation: ChatToolInvocationPart, _toolCall: UnknownToolCall): void {1633// No custom formatting needed1634}16351636/**1637* No-op post-invocation formatter for tools whose completion requires no custom display.1638*/1639function emptyToolInvocationCompleted(_invocation: ChatToolInvocationPart, _toolCall: UnknownToolCall, _result: ToolCallResult): void {1640// No custom post-invocation formatting needed1641}164216431644function genericToolInvocationCompleted(invocation: ChatToolInvocationPart, toolCall: UnknownToolCall, result: ToolCallResult): void {1645if (result.success && result.result?.content) {1646invocation.toolSpecificData = {1647output: typeof result.result.content === 'string' ? result.result.content : JSON.stringify(result.result.content, null, 2),1648input: toolCall.arguments ? JSON.stringify(toolCall.arguments, null, 2) : ''1649};1650}16511652}165316541655/**1656* Mock tools service that can be configured for different test scenarios1657*/1658export class FakeToolsService implements IToolsService {1659readonly _serviceBrand: undefined;16601661private readonly _onWillInvokeTool = new Emitter<IOnWillInvokeToolEvent>();1662readonly onWillInvokeTool = this._onWillInvokeTool.event;16631664readonly tools: ReadonlyArray<LanguageModelToolInformation> = [];1665readonly copilotTools = new Map<ToolName, ICopilotTool<unknown>>();16661667private _confirmationResult: 'yes' | 'no' = 'yes';1668private _invokeToolCalls: Array<{ name: string; input: unknown }> = [];16691670setConfirmationResult(result: 'yes' | 'no'): void {1671this._confirmationResult = result;1672}16731674get invokeToolCalls(): ReadonlyArray<{ name: string; input: unknown }> {1675return this._invokeToolCalls;1676}16771678clearCalls(): void {1679this._invokeToolCalls = [];1680}16811682invokeToolWithEndpoint(name: string, options: LanguageModelToolInvocationOptions<unknown>, endpoint: IChatEndpoint | undefined, token: CancellationToken): Thenable<LanguageModelToolResult2> {1683return this.invokeTool(name, options);1684}16851686modelSpecificTools: IObservable<{ definition: LanguageModelToolDefinition; tool: ICopilotTool<unknown> }[]> = constObservable([]);16871688async invokeTool(1689name: string,1690options: LanguageModelToolInvocationOptions<unknown>1691): Promise<LanguageModelToolResult2> {1692this._invokeToolCalls.push({ name, input: options.input });16931694if (name === ToolName.CoreConfirmationTool || name === ToolName.CoreTerminalConfirmationTool) {1695return {1696content: [new LanguageModelTextPart(this._confirmationResult)]1697};1698}16991700if (name === 'vscode_reviewPlan') {1701if (this._confirmationResult === 'no') {1702return { content: [new LanguageModelTextPart(JSON.stringify({ rejected: true }))] };1703}1704const input = options.input as { actions?: Array<{ label: string }> } | undefined;1705const firstAction = input?.actions?.[0]?.label;1706return { content: [new LanguageModelTextPart(JSON.stringify({ action: firstAction, rejected: false }))] };1707}17081709return { content: [] };1710}17111712getCopilotTool(): ICopilotTool<unknown> | undefined {1713return undefined;1714}17151716getTool(): LanguageModelToolInformation | undefined {1717return undefined;1718}17191720getToolByToolReferenceName(): LanguageModelToolInformation | undefined {1721return undefined;1722}17231724validateToolInput(): IToolValidationResult {1725return { inputObj: {} };1726}17271728validateToolName(): string | undefined {1729return undefined;1730}17311732getEnabledTools(): LanguageModelToolInformation[] {1733return [];1734}1735}173617371738/**1739* CLI sends 'synthetic' user messages for cases such as Skill invocations.1740* 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.1741*/1742export function isSyntheticUserMessage(event: Extract<SessionEvent, { type: 'user.message' }>): boolean {1743return event.type === 'user.message' && !!event.data.source && (event.data.source ?? '').toLowerCase() !== 'user';1744}174517461747