Skip to content

Commit e682c03

Browse files
feat: add search_and_replace tool for batch text replacements (#9549)
Co-authored-by: daniel-lxs <[email protected]>
1 parent 9b5f639 commit e682c03

File tree

11 files changed

+477
-7
lines changed

11 files changed

+477
-7
lines changed

packages/types/src/tool.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export const toolNames = [
2020
"write_to_file",
2121
"apply_diff",
2222
"insert_content",
23+
"search_and_replace",
2324
"search_files",
2425
"list_files",
2526
"list_code_definition_names",

src/core/assistant-message/NativeToolCallParser.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -635,6 +635,15 @@ export class NativeToolCallParser {
635635
}
636636
break
637637

638+
case "search_and_replace":
639+
if (args.path !== undefined && args.operations !== undefined && Array.isArray(args.operations)) {
640+
nativeArgs = {
641+
path: args.path,
642+
operations: args.operations,
643+
} as NativeArgsFor<TName>
644+
}
645+
break
646+
638647
case "ask_followup_question":
639648
if (args.question !== undefined && args.follow_up !== undefined) {
640649
nativeArgs = {

src/core/assistant-message/presentAssistantMessage.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { shouldUseSingleFileRead, TOOL_PROTOCOL } from "@roo-code/types"
1717
import { writeToFileTool } from "../tools/WriteToFileTool"
1818
import { applyDiffTool } from "../tools/MultiApplyDiffTool"
1919
import { insertContentTool } from "../tools/InsertContentTool"
20+
import { searchAndReplaceTool } from "../tools/SearchAndReplaceTool"
2021
import { listCodeDefinitionNamesTool } from "../tools/ListCodeDefinitionNamesTool"
2122
import { searchFilesTool } from "../tools/SearchFilesTool"
2223
import { browserActionTool } from "../tools/BrowserActionTool"
@@ -379,6 +380,8 @@ export async function presentAssistantMessage(cline: Task) {
379380
}]`
380381
case "insert_content":
381382
return `[${block.name} for '${block.params.path}']`
383+
case "search_and_replace":
384+
return `[${block.name} for '${block.params.path}']`
382385
case "list_files":
383386
return `[${block.name} for '${block.params.path}']`
384387
case "list_code_definition_names":
@@ -677,7 +680,14 @@ export async function presentAssistantMessage(cline: Task) {
677680
}
678681

679682
// Validate tool use before execution.
680-
const { mode, customModes } = (await cline.providerRef.deref()?.getState()) ?? {}
683+
const {
684+
mode,
685+
customModes,
686+
experiments: stateExperiments,
687+
apiConfiguration,
688+
} = (await cline.providerRef.deref()?.getState()) ?? {}
689+
const modelInfo = cline.api.getModel()
690+
const includedTools = modelInfo?.info?.includedTools
681691

682692
try {
683693
validateToolUse(
@@ -686,6 +696,8 @@ export async function presentAssistantMessage(cline: Task) {
686696
customModes ?? [],
687697
{ apply_diff: cline.diffEnabled },
688698
block.params,
699+
stateExperiments,
700+
includedTools,
689701
)
690702
} catch (error) {
691703
cline.consecutiveMistakeCount++
@@ -806,6 +818,16 @@ export async function presentAssistantMessage(cline: Task) {
806818
toolProtocol,
807819
})
808820
break
821+
case "search_and_replace":
822+
await checkpointSaveAndMark(cline)
823+
await searchAndReplaceTool.handle(cline, block as ToolUse<"search_and_replace">, {
824+
askApproval,
825+
handleError,
826+
pushToolResult,
827+
removeClosingTag,
828+
toolProtocol,
829+
})
830+
break
809831
case "read_file":
810832
// Check if this model should use the simplified single-file read tool
811833
// Only use simplified tool for XML protocol - native protocol works with standard tool

src/core/prompts/tools/native-tools/apply_diff.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ const DIFF_PARAMETER_DESCRIPTION = `A string containing one or more search/repla
1111
[new content to replace with]
1212
>>>>>>> REPLACE`
1313

14-
export const apply_diff_single_file = {
14+
export const apply_diff = {
1515
type: "function",
1616
function: {
1717
name: "apply_diff",

src/core/prompts/tools/native-tools/index.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type OpenAI from "openai"
22
import accessMcpResource from "./access_mcp_resource"
3+
import { apply_diff } from "./apply_diff"
34
import askFollowupQuestion from "./ask_followup_question"
45
import attemptCompletion from "./attempt_completion"
56
import browserAction from "./browser_action"
@@ -13,11 +14,11 @@ import listFiles from "./list_files"
1314
import newTask from "./new_task"
1415
import { createReadFileTool } from "./read_file"
1516
import runSlashCommand from "./run_slash_command"
17+
import searchAndReplace from "./search_and_replace"
1618
import searchFiles from "./search_files"
1719
import switchMode from "./switch_mode"
1820
import updateTodoList from "./update_todo_list"
1921
import writeToFile from "./write_to_file"
20-
import { apply_diff_single_file } from "./apply_diff"
2122

2223
export { getMcpServerTools } from "./mcp_server"
2324
export { convertOpenAIToolToAnthropic, convertOpenAIToolsToAnthropic } from "./converters"
@@ -31,7 +32,7 @@ export { convertOpenAIToolToAnthropic, convertOpenAIToolsToAnthropic } from "./c
3132
export function getNativeTools(partialReadsEnabled: boolean = true): OpenAI.Chat.ChatCompletionTool[] {
3233
return [
3334
accessMcpResource,
34-
apply_diff_single_file,
35+
apply_diff,
3536
askFollowupQuestion,
3637
attemptCompletion,
3738
browserAction,
@@ -45,6 +46,7 @@ export function getNativeTools(partialReadsEnabled: boolean = true): OpenAI.Chat
4546
newTask,
4647
createReadFileTool(partialReadsEnabled),
4748
runSlashCommand,
49+
searchAndReplace,
4850
searchFiles,
4951
switchMode,
5052
updateTodoList,
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import type OpenAI from "openai"
2+
3+
const SEARCH_AND_REPLACE_DESCRIPTION = `Apply precise, targeted modifications to an existing file using search and replace operations. This tool is for surgical edits only; provide an array of operations where each operation specifies the exact text to search for and what to replace it with. The search text must exactly match the existing content, including whitespace and indentation.`
4+
5+
const search_and_replace = {
6+
type: "function",
7+
function: {
8+
name: "search_and_replace",
9+
description: SEARCH_AND_REPLACE_DESCRIPTION,
10+
parameters: {
11+
type: "object",
12+
properties: {
13+
path: {
14+
type: "string",
15+
description: "The path of the file to modify, relative to the current workspace directory.",
16+
},
17+
operations: {
18+
type: "array",
19+
description: "Array of search and replace operations to perform on the file.",
20+
items: {
21+
type: "object",
22+
properties: {
23+
search: {
24+
type: "string",
25+
description:
26+
"The exact text to find in the file. Must match exactly, including whitespace.",
27+
},
28+
replace: {
29+
type: "string",
30+
description: "The text to replace the search text with.",
31+
},
32+
},
33+
required: ["search", "replace"],
34+
},
35+
minItems: 1,
36+
},
37+
},
38+
required: ["path", "operations"],
39+
additionalProperties: false,
40+
},
41+
},
42+
} satisfies OpenAI.Chat.ChatCompletionTool
43+
44+
export default search_and_replace

0 commit comments

Comments
 (0)