Skip to content

Commit c50436d

Browse files
Merge branch 'yemohyle/multi_replace_string' into yemohyle/main
2 parents 419cdda + 5be9ac7 commit c50436d

19 files changed

+354
-10
lines changed

IMPLEMENTATION_SUMMARY.md

Whitespace-only changes.

package.json

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -758,6 +758,58 @@
758758
]
759759
}
760760
},
761+
{
762+
"name": "copilot_multiReplaceString",
763+
"toolReferenceName": "multiReplaceString",
764+
"displayName": "%copilot.tools.multiReplaceString.name%",
765+
"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.",
766+
"when": "!config.github.copilot.chat.disableReplaceTool",
767+
"inputSchema": {
768+
"type": "object",
769+
"properties": {
770+
"explanation": {
771+
"type": "string",
772+
"description": "A brief explanation of what the multi-replace operation will accomplish."
773+
},
774+
"replacements": {
775+
"type": "array",
776+
"description": "An array of replacement operations to apply sequentially.",
777+
"items": {
778+
"type": "object",
779+
"properties": {
780+
"explanation": {
781+
"type": "string",
782+
"description": "A brief explanation of this specific replacement operation."
783+
},
784+
"filePath": {
785+
"type": "string",
786+
"description": "An absolute path to the file to edit."
787+
},
788+
"oldString": {
789+
"type": "string",
790+
"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."
791+
},
792+
"newString": {
793+
"type": "string",
794+
"description": "The exact literal text to replace `oldString` with, preferably unescaped. Provide the EXACT text. Ensure the resulting code is correct and idiomatic."
795+
}
796+
},
797+
"required": [
798+
"explanation",
799+
"filePath",
800+
"oldString",
801+
"newString"
802+
]
803+
},
804+
"minItems": 1
805+
}
806+
},
807+
"required": [
808+
"explanation",
809+
"replacements"
810+
]
811+
}
812+
},
761813
{
762814
"name": "copilot_editNotebook",
763815
"toolReferenceName": "editNotebook",

package.nls.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,7 @@
277277
"copilot.tools.createFile.name": "Create File",
278278
"copilot.tools.insertEdit.name": "Edit File",
279279
"copilot.tools.replaceString.name": "Replace String in File",
280+
"copilot.tools.multiReplaceString.name": "Multi-Replace String in Files",
280281
"copilot.tools.editNotebook.name": "Edit Notebook",
281282
"copilot.tools.runNotebookCell.name": "Run Notebook Cell",
282283
"copilot.tools.getNotebookCellOutput.name": "Get Notebook Cell Output",

src/extension/conversation/vscode-node/languageModelAccess.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -139,17 +139,13 @@ export class LanguageModelAccess extends Disposable implements IExtensionContrib
139139
}
140140

141141
const baseCount = await PromptRenderer.create(this._instantiationService, endpoint, LanguageModelAccessPrompt, { noSafety: false, messages: [] }).countTokens();
142-
let multiplierString = endpoint.multiplier !== undefined ? `${endpoint.multiplier}x` : undefined;
143-
if (endpoint.model === AutoChatEndpoint.id) {
144-
multiplierString = 'Variable';
145-
}
146142

147143
const model: vscode.LanguageModelChatInformation = {
148144
id: endpoint.model,
149145
name: endpoint.model === AutoChatEndpoint.id ? 'Auto' : endpoint.name,
150146
family: endpoint.family,
151147
description: modelDescription,
152-
cost: multiplierString,
148+
cost: endpoint.multiplier !== undefined && endpoint.multiplier !== 0 ? `${endpoint.multiplier}x` : endpoint.multiplier === 0 ? localize('languageModel.costIncluded', 'Included') : undefined,
153149
category: modelCategory,
154150
version: endpoint.version,
155151
maxInputTokens: endpoint.modelMaxPromptTokens - baseCount - BaseTokensPerCompletion,

src/extension/intents/node/agentIntent.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ const getTools = (instaService: IInstantiationService, request: vscode.ChatReque
6666

6767
if (modelCanUseReplaceStringExclusively(model)) {
6868
allowTools[ToolName.ReplaceString] = true;
69+
allowTools[ToolName.MultiReplaceString] = true;
6970
allowTools[ToolName.EditFile] = false;
7071
}
7172

src/extension/intents/node/editCodeIntent2.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ const getTools = (instaService: IInstantiationService, request: vscode.ChatReque
4848

4949
if (model.family.startsWith('claude')) {
5050
lookForTools.add(ToolName.ReplaceString);
51+
lookForTools.add(ToolName.MultiReplaceString);
5152
}
5253
lookForTools.add(ToolName.EditNotebook);
5354
if (requestHasNotebookRefs(request, notebookService, { checkPromptAsWell: true })) {

src/extension/intents/node/notebookEditorIntent.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ const getTools = (instaService: IInstantiationService, request: vscode.ChatReque
5050

5151
if (model.family.startsWith('claude')) {
5252
lookForTools.add(ToolName.ReplaceString);
53+
lookForTools.add(ToolName.MultiReplaceString);
5354
}
5455

5556
lookForTools.add(ToolName.EditNotebook);

src/extension/prompts/node/agent/agentInstructions.tsx

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,8 @@ export class DefaultAgentPrompt extends PromptElement<DefaultAgentPromptProps> {
143143
{tools.hasReplaceStringTool ?
144144
<>
145145
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.<br />
146-
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.<br />
146+
{hasMultiReplaceStringTool && <>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.<br /></>}
147+
Use the {ToolName.ReplaceString} tool for single string replacements, paying attention to context to ensure your replacement is unique.<br />
147148
Use the {ToolName.EditFile} tool to insert code into a file ONLY if {ToolName.ReplaceString} has failed.<br />
148149
When editing files, group your changes by file.<br />
149150
{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.<br /></>}
@@ -313,15 +314,16 @@ export class AlternateGPTPrompt extends PromptElement<DefaultAgentPromptProps> {
313314
When editing files, group your changes by file.<br />
314315
{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.<br /></>}
315316
NEVER show the changes to the user, just call the tool, and the edits will be applied and shown to the user.<br />
316-
NEVER print a codeblock that represents a change to a file, use {ToolName.ReplaceString} or {ToolName.EditFile} instead.<br />
317-
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.<br /></> :
317+
NEVER print a codeblock that represents a change to a file, use {ToolName.ReplaceString}, {ToolName.MultiReplaceString}, or {ToolName.EditFile} instead.<br />
318+
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.<br /></> :
318319
<>
319320
Don't try to edit an existing file without reading it first, so you can make changes properly.<br />
321+
{hasMultiReplaceStringTool && <>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.<br /></>}
320322
Use the {ToolName.ReplaceString} tool to edit files. When editing files, group your changes by file.<br />
321323
{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.<br /></>}
322324
NEVER show the changes to the user, just call the tool, and the edits will be applied and shown to the user.<br />
323-
NEVER print a codeblock that represents a change to a file, use {ToolName.ReplaceString} instead.<br />
324-
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.<br />
325+
NEVER print a codeblock that represents a change to a file, use {ToolName.ReplaceString} or {ToolName.MultiReplaceString} instead.<br />
326+
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.<br />
325327
</>}
326328
<GenericEditingTips {...this.props} />
327329
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.<br />

src/extension/prompts/node/panel/toolCalling.tsx

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,33 @@ class ToolResultElement extends PromptElement<ToolResultElementProps, void> {
212212
}
213213
}
214214
this.sendToolCallTelemetry(outcome, validation);
215+
} // Check if next_tool_prediction contains "replace_string_in_file" and prepare reminder text
216+
let reminderText = '';
217+
if (toolResult) {
218+
try {
219+
const toolArgs = JSON.parse(this.props.toolCall.arguments);
220+
this.logService.logger.info(`Tool call arguments parsed: ${JSON.stringify(toolArgs)}`);
221+
222+
if (toolArgs.next_tool_prediction && Array.isArray(toolArgs.next_tool_prediction)) {
223+
this.logService.logger.info(`Found next_tool_prediction: ${JSON.stringify(toolArgs.next_tool_prediction)}`);
224+
const hasSomeTool = toolArgs.next_tool_prediction.some((tool: string) =>
225+
typeof tool === 'string' && tool.includes('replace_string_in_file')
226+
);
227+
228+
if (hasSomeTool) {
229+
this.logService.logger.info('Found "replace_string_in_file" in next_tool_prediction, adding reminder');
230+
//reminderText = '\n\n<reminder>\nIf you need to make multiple edits using replace_string_in_file tool, consider making them in parallel whenever possible.\n</reminder>';
231+
reminderText = '';
232+
} else {
233+
this.logService.logger.info('No "replace_string_in_file" found in next_tool_prediction');
234+
}
235+
} else {
236+
this.logService.logger.info('No next_tool_prediction found or not an array');
237+
}
238+
} catch (error) {
239+
// If parsing fails, continue with original result
240+
this.logService.logger.warn('Failed to parse tool arguments for next_tool_prediction check');
241+
}
215242
}
216243

217244
const toolResultElement = this.props.enableCacheBreakpoints ?
@@ -227,6 +254,7 @@ class ToolResultElement extends PromptElement<ToolResultElementProps, void> {
227254
<meta value={new ToolResultMetadata(this.props.toolCall.id!, toolResult, isCancelled)} />
228255
{...extraMetadata.map(m => <meta value={m} />)}
229256
{toolResultElement}
257+
{reminderText && reminderText}
230258
{this.props.isLast && this.props.enableCacheBreakpoints && <cacheBreakpoint type={CacheType} />}
231259
</ToolMessage>
232260
);

src/extension/tools/common/toolNames.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export const enum ToolName {
2828
EditFile = 'insert_edit_into_file',
2929
CreateFile = 'create_file',
3030
ReplaceString = 'replace_string_in_file',
31+
MultiReplaceString = 'multi_replace_string_in_file',
3132
EditNotebook = 'edit_notebook_file',
3233
RunNotebookCell = 'run_notebook_cell',
3334
GetNotebookSummary = 'copilot_getNotebookSummary',
@@ -79,6 +80,7 @@ export const enum ContributedToolName {
7980
EditFile = 'copilot_insertEdit',
8081
CreateFile = 'copilot_createFile',
8182
ReplaceString = 'copilot_replaceString',
83+
MultiReplaceString = 'copilot_multiReplaceString',
8284
EditNotebook = 'copilot_editNotebook',
8385
RunNotebookCell = 'copilot_runNotebookCell',
8486
GetNotebookSummary = 'copilot_getNotebookSummary',
@@ -123,6 +125,7 @@ const contributedToolNameToToolNames = new Map<ContributedToolName, ToolName>([
123125
[ContributedToolName.FindTestFiles, ToolName.FindTestFiles],
124126
[ContributedToolName.CreateFile, ToolName.CreateFile],
125127
[ContributedToolName.ReplaceString, ToolName.ReplaceString],
128+
[ContributedToolName.MultiReplaceString, ToolName.MultiReplaceString],
126129
[ContributedToolName.EditNotebook, ToolName.EditNotebook],
127130
[ContributedToolName.RunNotebookCell, ToolName.RunNotebookCell],
128131
[ContributedToolName.GetNotebookSummary, ToolName.GetNotebookSummary],

0 commit comments

Comments
 (0)