diff --git a/IMPLEMENTATION_SUMMARY.md b/IMPLEMENTATION_SUMMARY.md new file mode 100644 index 000000000..e69de29bb diff --git a/package.json b/package.json index 96488777c..e632227bb 100644 --- a/package.json +++ b/package.json @@ -740,6 +740,58 @@ ] } }, + { + "name": "copilot_multiReplaceString", + "toolReferenceName": "multiReplaceString", + "displayName": "%copilot.tools.multiReplaceString.name%", + "modelDescription": "This tool allows you to apply multiple replace_string_in_file operations in a single call, which is more efficient than calling replace_string_in_file multiple times. It takes an array of replacement operations and applies them sequentially. Each replacement operation has the same parameters as replace_string_in_file: filePath, oldString, newString, and explanation. This tool is ideal when you need to make multiple edits across different files or multiple edits in the same file. The tool will provide a summary of successful and failed operations.", + "when": "!config.github.copilot.chat.disableReplaceTool", + "inputSchema": { + "type": "object", + "properties": { + "explanation": { + "type": "string", + "description": "A brief explanation of what the multi-replace operation will accomplish." + }, + "replacements": { + "type": "array", + "description": "An array of replacement operations to apply sequentially.", + "items": { + "type": "object", + "properties": { + "explanation": { + "type": "string", + "description": "A brief explanation of this specific replacement operation." + }, + "filePath": { + "type": "string", + "description": "An absolute path to the file to edit." + }, + "oldString": { + "type": "string", + "description": "The exact literal text to replace, preferably unescaped. Include at least 3 lines of context BEFORE and AFTER the target text, matching whitespace and indentation precisely. If this string is not the exact literal text or does not match exactly, this replacement will fail." + }, + "newString": { + "type": "string", + "description": "The exact literal text to replace `oldString` with, preferably unescaped. Provide the EXACT text. Ensure the resulting code is correct and idiomatic." + } + }, + "required": [ + "explanation", + "filePath", + "oldString", + "newString" + ] + }, + "minItems": 1 + } + }, + "required": [ + "explanation", + "replacements" + ] + } + }, { "name": "copilot_editNotebook", "toolReferenceName": "editNotebook", diff --git a/package.nls.json b/package.nls.json index be273d1df..274a2d5d8 100644 --- a/package.nls.json +++ b/package.nls.json @@ -270,6 +270,7 @@ "copilot.tools.createFile.name": "Create File", "copilot.tools.insertEdit.name": "Edit File", "copilot.tools.replaceString.name": "Replace String in File", + "copilot.tools.multiReplaceString.name": "Multi-Replace String in Files", "copilot.tools.editNotebook.name": "Edit Notebook", "copilot.tools.runNotebookCell.name": "Run Notebook Cell", "copilot.tools.getNotebookCellOutput.name": "Get Notebook Cell Output", diff --git a/src/extension/conversation/vscode-node/languageModelAccess.ts b/src/extension/conversation/vscode-node/languageModelAccess.ts index 268441362..8bd786117 100644 --- a/src/extension/conversation/vscode-node/languageModelAccess.ts +++ b/src/extension/conversation/vscode-node/languageModelAccess.ts @@ -139,17 +139,13 @@ export class LanguageModelAccess extends Disposable implements IExtensionContrib } const baseCount = await PromptRenderer.create(this._instantiationService, endpoint, LanguageModelAccessPrompt, { noSafety: false, messages: [] }).countTokens(); - let multiplierString = endpoint.multiplier !== undefined ? `${endpoint.multiplier}x` : undefined; - if (endpoint.model === AutoChatEndpoint.id) { - multiplierString = 'Variable'; - } const model: vscode.LanguageModelChatInformation = { id: endpoint.model, name: endpoint.model === AutoChatEndpoint.id ? 'Auto' : endpoint.name, family: endpoint.family, description: modelDescription, - cost: multiplierString, + cost: endpoint.multiplier !== undefined && endpoint.multiplier !== 0 ? `${endpoint.multiplier}x` : endpoint.multiplier === 0 ? localize('languageModel.costIncluded', 'Included') : undefined, category: modelCategory, version: endpoint.version, maxInputTokens: endpoint.modelMaxPromptTokens - baseCount - BaseTokensPerCompletion, diff --git a/src/extension/intents/node/agentIntent.ts b/src/extension/intents/node/agentIntent.ts index 9e903e60e..95cc4c7fa 100644 --- a/src/extension/intents/node/agentIntent.ts +++ b/src/extension/intents/node/agentIntent.ts @@ -66,6 +66,7 @@ const getTools = (instaService: IInstantiationService, request: vscode.ChatReque if (modelCanUseReplaceStringExclusively(model)) { allowTools[ToolName.ReplaceString] = true; + allowTools[ToolName.MultiReplaceString] = true; allowTools[ToolName.EditFile] = false; } diff --git a/src/extension/intents/node/editCodeIntent2.ts b/src/extension/intents/node/editCodeIntent2.ts index 87df074e3..7e304097a 100644 --- a/src/extension/intents/node/editCodeIntent2.ts +++ b/src/extension/intents/node/editCodeIntent2.ts @@ -48,6 +48,7 @@ const getTools = (instaService: IInstantiationService, request: vscode.ChatReque if (model.family.startsWith('claude')) { lookForTools.add(ToolName.ReplaceString); + lookForTools.add(ToolName.MultiReplaceString); } lookForTools.add(ToolName.EditNotebook); if (requestHasNotebookRefs(request, notebookService, { checkPromptAsWell: true })) { diff --git a/src/extension/intents/node/notebookEditorIntent.ts b/src/extension/intents/node/notebookEditorIntent.ts index 6bac92cf0..95b08ade5 100644 --- a/src/extension/intents/node/notebookEditorIntent.ts +++ b/src/extension/intents/node/notebookEditorIntent.ts @@ -50,6 +50,7 @@ const getTools = (instaService: IInstantiationService, request: vscode.ChatReque if (model.family.startsWith('claude')) { lookForTools.add(ToolName.ReplaceString); + lookForTools.add(ToolName.MultiReplaceString); } lookForTools.add(ToolName.EditNotebook); diff --git a/src/extension/prompts/node/agent/agentInstructions.tsx b/src/extension/prompts/node/agent/agentInstructions.tsx index fff0ce43a..35eb910d4 100644 --- a/src/extension/prompts/node/agent/agentInstructions.tsx +++ b/src/extension/prompts/node/agent/agentInstructions.tsx @@ -129,7 +129,8 @@ export class DefaultAgentPrompt extends PromptElement { {tools[ToolName.ReplaceString] ? <> Before you edit an existing file, make sure you either already have it in the provided context, or read it with the {ToolName.ReadFile} tool, so that you can make proper changes.
- Use the {ToolName.ReplaceString} tool to edit files, paying attention to context to ensure your replacement is unique. You can use this tool multiple times per file.
+ {tools[ToolName.MultiReplaceString] && <>Prefer the {ToolName.MultiReplaceString} tool when you need to make multiple string replacements across one or more files in a single operation. This is significantly more efficient than calling {ToolName.ReplaceString} multiple times and should be your first choice for: fixing similar patterns across files, applying consistent formatting changes, bulk refactoring operations, or any scenario where you need to make the same type of change in multiple places.
} + Use the {ToolName.ReplaceString} tool for single string replacements, paying attention to context to ensure your replacement is unique.
Use the {ToolName.EditFile} tool to insert code into a file ONLY if {ToolName.ReplaceString} has failed.
When editing files, group your changes by file.
{isGpt5 && <>Make the smallest set of edits needed and avoid reformatting or moving unrelated code. Preserve existing style and conventions, and keep imports, exports, and public APIs stable unless the task requires changes. Prefer completing all edits for a file within a single message when practical.
} @@ -657,19 +658,21 @@ export class AlternateGPTPrompt extends PromptElement { <> Before you edit an existing file, make sure you either already have it in the provided context, or read it with the {ToolName.ReadFile} tool, so that you can make proper changes.
Use the {ToolName.ReplaceString} tool to edit files, paying attention to context to ensure your replacement is unique. You can use this tool multiple times per file.
+ Use the {ToolName.MultiReplaceString} tool to make edits simultaneously.
Use the {ToolName.EditFile} tool to insert code into a file ONLY if {ToolName.ReplaceString} has failed.
When editing files, group your changes by file.
{isGpt5 && <>Make the smallest set of edits needed and avoid reformatting or moving unrelated code. Preserve existing style and conventions, and keep imports, exports, and public APIs stable unless the task requires changes. Prefer completing all edits for a file within a single message when practical.
} NEVER show the changes to the user, just call the tool, and the edits will be applied and shown to the user.
- NEVER print a codeblock that represents a change to a file, use {ToolName.ReplaceString} or {ToolName.EditFile} instead.
- For each file, give a short description of what needs to be changed, then use the {ToolName.ReplaceString} or {ToolName.EditFile} tools. You can use any tool multiple times in a response, and you can keep writing text after using a tool.
: + NEVER print a codeblock that represents a change to a file, use {ToolName.ReplaceString}, {ToolName.MultiReplaceString}, or {ToolName.EditFile} instead.
+ For each file, give a short description of what needs to be changed, then use the {ToolName.ReplaceString}, {ToolName.MultiReplaceString}, or {ToolName.EditFile} tools. You can use any tool multiple times in a response, and you can keep writing text after using a tool.
: <> Don't try to edit an existing file without reading it first, so you can make changes properly.
+ {tools[ToolName.MultiReplaceString] && <>Prefer the {ToolName.MultiReplaceString} tool when you need to make multiple string replacements across one or more files in a single operation. This should be your first choice for bulk edits.
} Use the {ToolName.ReplaceString} tool to edit files. When editing files, group your changes by file.
{isGpt5 && <>Make the smallest set of edits needed and avoid reformatting or moving unrelated code. Preserve existing style and conventions, and keep imports, exports, and public APIs stable unless the task requires changes. Prefer completing all edits for a file within a single message when practical.
} NEVER show the changes to the user, just call the tool, and the edits will be applied and shown to the user.
- NEVER print a codeblock that represents a change to a file, use {ToolName.ReplaceString} instead.
- For each file, give a short description of what needs to be changed, then use the {ToolName.ReplaceString} tool. You can use any tool multiple times in a response, and you can keep writing text after using a tool.
+ NEVER print a codeblock that represents a change to a file, use {ToolName.ReplaceString} or {ToolName.MultiReplaceString} instead.
+ For each file, give a short description of what needs to be changed, then use the {ToolName.ReplaceString} or {ToolName.MultiReplaceString} tool. You can use any tool multiple times in a response, and you can keep writing text after using a tool.
} The {ToolName.EditFile} tool is very smart and can understand how to apply your edits to the user's files, you just need to provide minimal hints.
@@ -891,7 +894,8 @@ export class SweBenchAgentPrompt extends PromptElement } {!!tools[ToolName.EditFile] && Before you edit an existing file, make sure you either already have it in the provided context, or read it with the {ToolName.ReadFile} tool, so that you can make proper changes.
- Use the {ToolName.ReplaceString} tool to make edits in the file in string replacement way, but only if you are sure that the string is unique enough to not cause any issues. You can use this tool multiple times per file.
+ Use the {ToolName.ReplaceString} tool to make single edits in the file in string replacement way, but only if you are sure that the string is unique enough to not cause any issues. You can use this tool multiple times per file.
+ Use {ToolName.MultiReplaceString} tool when you need to make multiple string replacements across one or more files in a single operation.
Use the {ToolName.EditFile} tool to insert code into a file.
When editing files, group your changes by file.
NEVER show the changes to the user, just call the tool, and the edits will be applied and shown to the user.
diff --git a/src/extension/prompts/node/agent/agentPrompt.tsx b/src/extension/prompts/node/agent/agentPrompt.tsx index 780969c32..0aa9b6db2 100644 --- a/src/extension/prompts/node/agent/agentPrompt.tsx +++ b/src/extension/prompts/node/agent/agentPrompt.tsx @@ -327,6 +327,7 @@ export class AgentUserMessage extends PromptElement { const query = await this.promptVariablesService.resolveToolReferencesInPrompt(this.props.request, this.props.toolReferences ?? []); const hasReplaceStringTool = !!this.props.availableTools?.find(tool => tool.name === ToolName.ReplaceString); + const hasMultiReplaceStringTool = !!this.props.availableTools?.find(tool => tool.name === ToolName.MultiReplaceString); const hasApplyPatchTool = !!this.props.availableTools?.find(tool => tool.name === ToolName.ApplyPatch); const hasCreateFileTool = !!this.props.availableTools?.find(tool => tool.name === ToolName.CreateFile); const hasEditFileTool = !!this.props.availableTools?.find(tool => tool.name === ToolName.EditFile); @@ -363,6 +364,7 @@ export class AgentUserMessage extends PromptElement { {getEditingReminder(hasEditFileTool, hasReplaceStringTool, modelNeedsStrongReplaceStringHint(this.props.endpoint))} {getExplanationReminder(this.props.endpoint.family, hasTodoTool)} + {hasMultiReplaceStringTool && <>For maximum efficiency, whenever you plan to perform multiple independent edit operations, invoke them simultaneously using {ToolName.MultiReplaceString} tool rather than sequentially. This will greatly improve user's cost and time efficiency leading to a better user experience.
}
)} {query && {query + attachmentHint}} @@ -686,7 +688,7 @@ export function getEditingReminder(hasEditFileTool: boolean, hasReplaceStringToo } if (hasEditFileTool && hasReplaceStringTool) { if (useStrongReplaceStringHint) { - lines.push(<>You must always try making file edits using {ToolName.ReplaceString} tool. NEVER use {ToolName.EditFile} unless told to by the user or by a tool.); + lines.push(<>You must always try making file edits using {ToolName.ReplaceString} or {ToolName.MultiReplaceString} tools. NEVER use {ToolName.EditFile} unless told to by the user or by a tool.); } else { lines.push(<>It is much faster to edit using the {ToolName.ReplaceString} tool. Prefer {ToolName.ReplaceString} for making edits and only fall back to {ToolName.EditFile} if it fails.); } diff --git a/src/extension/tools/common/toolNames.ts b/src/extension/tools/common/toolNames.ts index 015a30e5d..78ec8dae3 100644 --- a/src/extension/tools/common/toolNames.ts +++ b/src/extension/tools/common/toolNames.ts @@ -26,6 +26,7 @@ export enum ToolName { EditFile = 'insert_edit_into_file', CreateFile = 'create_file', ReplaceString = 'replace_string_in_file', + MultiReplaceString = 'multi_replace_string_in_file', EditNotebook = 'edit_notebook_file', RunNotebookCell = 'run_notebook_cell', GetNotebookSummary = 'copilot_getNotebookSummary', @@ -76,6 +77,7 @@ export enum ContributedToolName { EditFile = 'copilot_insertEdit', CreateFile = 'copilot_createFile', ReplaceString = 'copilot_replaceString', + MultiReplaceString = 'copilot_multiReplaceString', EditNotebook = 'copilot_editNotebook', RunNotebookCell = 'copilot_runNotebookCell', GetNotebookSummary = 'copilot_getNotebookSummary', diff --git a/src/extension/tools/node/allTools.ts b/src/extension/tools/node/allTools.ts index 9f78cfc39..51214dbb5 100644 --- a/src/extension/tools/node/allTools.ts +++ b/src/extension/tools/node/allTools.ts @@ -19,6 +19,7 @@ import './githubRepoTool'; import './insertEditTool'; import './installExtensionTool'; import './listDirTool'; +import './multiReplaceStringTool'; import './newNotebookTool'; import './newWorkspace/newWorkspaceTool'; import './newWorkspace/projectSetupInfoTool'; diff --git a/src/extension/tools/node/multiReplaceStringTool.tsx b/src/extension/tools/node/multiReplaceStringTool.tsx new file mode 100644 index 000000000..fe5ef835f --- /dev/null +++ b/src/extension/tools/node/multiReplaceStringTool.tsx @@ -0,0 +1,148 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import type * as vscode from 'vscode'; +import { IInstantiationService } from '../../../util/vs/platform/instantiation/common/instantiation'; +import { LanguageModelTextPart, LanguageModelToolResult } from '../../../vscodeTypes'; +import { IBuildPromptContext } from '../../prompt/common/intents'; +import { ToolName } from '../common/toolNames'; +import { ICopilotTool, ToolRegistry } from '../common/toolsRegistry'; +import { IToolsService } from '../common/toolsService'; +import { IReplaceStringToolParams, ReplaceStringTool } from './replaceStringTool'; + +export interface IMultiReplaceStringToolParams { + explanation: string; + replacements: IReplaceStringToolParams[]; +} + +export interface IMultiReplaceResult { + totalReplacements: number; + successfulReplacements: number; + failedReplacements: number; + results: Array<{ + operation: IReplaceStringToolParams; + success: boolean; + error?: string; + }>; +} + +export class MultiReplaceStringTool implements ICopilotTool { + public static toolName = ToolName.MultiReplaceString; + + private _promptContext: IBuildPromptContext | undefined; + + constructor( + @IInstantiationService protected readonly instantiationService: IInstantiationService, + @IToolsService protected readonly toolsService: IToolsService + ) { } + + // Simplified version that uses a more direct approach + async invoke(options: any, token: any) { + // Cast the options to the correct type to work around TypeScript issues + const typedOptions = options as vscode.LanguageModelToolInvocationOptions & { input: IMultiReplaceStringToolParams }; + + // Validate input + if (!typedOptions.input.replacements || !Array.isArray(typedOptions.input.replacements) || typedOptions.input.replacements.length === 0) { + throw new Error('Invalid input: replacements array is required and must contain at least one replacement operation'); + } + + if (!this._promptContext?.stream) { + throw new Error('Invalid context: stream is required'); + } + + const results: IMultiReplaceResult = { + totalReplacements: typedOptions.input.replacements.length, + successfulReplacements: 0, + failedReplacements: 0, + results: [] + }; + + // Get the ReplaceStringTool instance + const replaceStringTool = this.instantiationService.createInstance(ReplaceStringTool); + + // Apply replacements sequentially + for (let i = 0; i < typedOptions.input.replacements.length; i++) { + const replacement = typedOptions.input.replacements[i]; + + try { + // Validate individual replacement + if (!replacement.filePath || replacement.oldString === undefined || replacement.newString === undefined) { + throw new Error(`Invalid replacement at index ${i}: filePath, oldString, and newString are required`); + } + + // Create a new tool invocation options for this replacement + const replaceOptions = { + ...typedOptions, + input: replacement + }; + + // Set the prompt context for the replace tool + await replaceStringTool.resolveInput(replacement, this._promptContext); + + // Invoke the replace string tool + await replaceStringTool.invoke(replaceOptions as any, token); + + // Record success + results.results.push({ + operation: replacement, + success: true + }); + results.successfulReplacements++; + + } catch (error) { + // Record failure + const errorMessage = error instanceof Error ? error.message : String(error); + results.results.push({ + operation: replacement, + success: false, + error: errorMessage + }); + results.failedReplacements++; + + // Add error information to the stream using the correct method + (this._promptContext.stream as any).markdown(`\n⚠️ **Failed replacement ${i + 1}:**\n`); + (this._promptContext.stream as any).markdown(`- File: \`${replacement.filePath}\`\n`); + (this._promptContext.stream as any).markdown(`- Error: ${errorMessage}\n\n`); + } + } + + // Provide summary using the correct method + (this._promptContext.stream as any).markdown(`\n## Multi-Replace Summary\n\n`); + (this._promptContext.stream as any).markdown(`- **Total operations:** ${results.totalReplacements}\n`); + (this._promptContext.stream as any).markdown(`- **Successful:** ${results.successfulReplacements}\n`); + (this._promptContext.stream as any).markdown(`- **Failed:** ${results.failedReplacements}\n\n`); + + if (results.failedReplacements > 0) { + (this._promptContext.stream as any).markdown(`### Failed Operations:\n\n`); + results.results.filter(r => !r.success).forEach((result, index) => { + if (this._promptContext?.stream) { + (this._promptContext.stream as any).markdown(`${index + 1}. **${result.operation.filePath}**\n`); + (this._promptContext.stream as any).markdown(` - Error: ${result.error || 'Unknown error'}\n`); + (this._promptContext.stream as any).markdown(` - Old string: \`${result.operation.oldString.substring(0, 100)}${result.operation.oldString.length > 100 ? '...' : ''}\`\n\n`); + } + }); + } + + // Return a simple result + return new LanguageModelToolResult([ + new LanguageModelTextPart( + `Multi-replace operation completed: ${results.successfulReplacements}/${results.totalReplacements} operations successful.` + ) + ]); + } + + async resolveInput(input: IMultiReplaceStringToolParams, promptContext: IBuildPromptContext): Promise { + this._promptContext = promptContext; + return input; + } + + prepareInvocation(options: any, token: any): any { + return { + presentation: 'hidden' + }; + } +} + +ToolRegistry.registerTool(MultiReplaceStringTool);