mirror of
https://github.com/pacnpal/Roo-Code.git
synced 2025-12-20 12:21:13 -05:00
Checkpoint on insert and search/replace tools
This commit is contained in:
@@ -60,6 +60,7 @@ import { BrowserSession } from "../services/browser/BrowserSession"
|
||||
import { OpenRouterHandler } from "../api/providers/openrouter"
|
||||
import { McpHub } from "../services/mcp/McpHub"
|
||||
import crypto from "crypto"
|
||||
import { insertGroups } from "./diff/insert-groups"
|
||||
|
||||
const cwd =
|
||||
vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0) ?? path.join(os.homedir(), "Desktop") // may or may not exist but fs checking existence would immediately ask for permission which would be bad UX, need to come up with a better solution
|
||||
@@ -1008,6 +1009,10 @@ export class Cline {
|
||||
return `[${block.name} for '${block.params.regex}'${
|
||||
block.params.file_pattern ? ` in '${block.params.file_pattern}'` : ""
|
||||
}]`
|
||||
case "insert_code_block":
|
||||
return `[${block.name} for '${block.params.path}']`
|
||||
case "search_and_replace":
|
||||
return `[${block.name} for '${block.params.path}']`
|
||||
case "list_files":
|
||||
return `[${block.name} for '${block.params.path}']`
|
||||
case "list_code_definition_names":
|
||||
@@ -1479,6 +1484,323 @@ export class Cline {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
case "insert_code_block": {
|
||||
const relPath: string | undefined = block.params.path
|
||||
const operations: string | undefined = block.params.operations
|
||||
|
||||
const sharedMessageProps: ClineSayTool = {
|
||||
tool: "appliedDiff",
|
||||
path: getReadablePath(cwd, removeClosingTag("path", relPath)),
|
||||
}
|
||||
|
||||
try {
|
||||
if (block.partial) {
|
||||
const partialMessage = JSON.stringify(sharedMessageProps)
|
||||
await this.ask("tool", partialMessage, block.partial).catch(() => {})
|
||||
break
|
||||
}
|
||||
|
||||
// Validate required parameters
|
||||
if (!relPath) {
|
||||
this.consecutiveMistakeCount++
|
||||
pushToolResult(await this.sayAndCreateMissingParamError("insert_code_block", "path"))
|
||||
break
|
||||
}
|
||||
|
||||
if (!operations) {
|
||||
this.consecutiveMistakeCount++
|
||||
pushToolResult(
|
||||
await this.sayAndCreateMissingParamError("insert_code_block", "operations"),
|
||||
)
|
||||
break
|
||||
}
|
||||
|
||||
const absolutePath = path.resolve(cwd, relPath)
|
||||
const fileExists = await fileExistsAtPath(absolutePath)
|
||||
|
||||
if (!fileExists) {
|
||||
this.consecutiveMistakeCount++
|
||||
const formattedError = `File does not exist at path: ${absolutePath}\n\n<error_details>\nThe specified file could not be found. Please verify the file path and try again.\n</error_details>`
|
||||
await this.say("error", formattedError)
|
||||
pushToolResult(formattedError)
|
||||
break
|
||||
}
|
||||
|
||||
let parsedOperations: Array<{
|
||||
start_line: number
|
||||
content: string
|
||||
}>
|
||||
|
||||
try {
|
||||
parsedOperations = JSON.parse(operations)
|
||||
if (!Array.isArray(parsedOperations)) {
|
||||
throw new Error("Operations must be an array")
|
||||
}
|
||||
} catch (error) {
|
||||
this.consecutiveMistakeCount++
|
||||
await this.say("error", `Failed to parse operations JSON: ${error.message}`)
|
||||
pushToolResult(formatResponse.toolError("Invalid operations JSON format"))
|
||||
break
|
||||
}
|
||||
|
||||
this.consecutiveMistakeCount = 0
|
||||
|
||||
// Read the file
|
||||
const fileContent = await fs.readFile(absolutePath, "utf8")
|
||||
this.diffViewProvider.editType = "modify"
|
||||
this.diffViewProvider.originalContent = fileContent
|
||||
const lines = fileContent.split("\n")
|
||||
|
||||
const updatedContent = insertGroups(
|
||||
lines,
|
||||
parsedOperations.map((elem) => {
|
||||
return {
|
||||
index: elem.start_line - 1,
|
||||
elements: elem.content.split("\n"),
|
||||
}
|
||||
}),
|
||||
).join("\n")
|
||||
|
||||
// Show changes in diff view
|
||||
if (!this.diffViewProvider.isEditing) {
|
||||
await this.ask("tool", JSON.stringify(sharedMessageProps), true).catch(() => {})
|
||||
// First open with original content
|
||||
await this.diffViewProvider.open(relPath)
|
||||
await this.diffViewProvider.update(fileContent, false)
|
||||
this.diffViewProvider.scrollToFirstDiff()
|
||||
await delay(200)
|
||||
}
|
||||
|
||||
const diff = formatResponse.createPrettyPatch(
|
||||
relPath,
|
||||
this.diffViewProvider.originalContent,
|
||||
updatedContent,
|
||||
)
|
||||
|
||||
if (!diff) {
|
||||
pushToolResult(`No changes needed for '${relPath}'`)
|
||||
break
|
||||
}
|
||||
|
||||
await this.diffViewProvider.update(updatedContent, true)
|
||||
|
||||
const completeMessage = JSON.stringify({
|
||||
...sharedMessageProps,
|
||||
diff,
|
||||
} satisfies ClineSayTool)
|
||||
|
||||
const didApprove = await this.ask("tool", completeMessage, false).then(
|
||||
(response) => response.response === "yesButtonClicked",
|
||||
)
|
||||
|
||||
if (!didApprove) {
|
||||
await this.diffViewProvider.revertChanges()
|
||||
pushToolResult("Changes were rejected by the user.")
|
||||
break
|
||||
}
|
||||
|
||||
const { newProblemsMessage, userEdits, finalContent } =
|
||||
await this.diffViewProvider.saveChanges()
|
||||
this.didEditFile = true
|
||||
|
||||
if (!userEdits) {
|
||||
pushToolResult(
|
||||
`The code block was successfully inserted in ${relPath.toPosix()}.${newProblemsMessage}`,
|
||||
)
|
||||
await this.diffViewProvider.reset()
|
||||
break
|
||||
}
|
||||
|
||||
const userFeedbackDiff = JSON.stringify({
|
||||
tool: "appliedDiff",
|
||||
path: getReadablePath(cwd, relPath),
|
||||
diff: userEdits,
|
||||
} satisfies ClineSayTool)
|
||||
|
||||
console.debug("[DEBUG] User made edits, sending feedback diff:", userFeedbackDiff)
|
||||
await this.say("user_feedback_diff", userFeedbackDiff)
|
||||
pushToolResult(
|
||||
`The user made the following updates to your content:\n\n${userEdits}\n\n` +
|
||||
`The updated content, which includes both your original modifications and the user's edits, has been successfully saved to ${relPath.toPosix()}. Here is the full, updated content of the file:\n\n` +
|
||||
`<final_file_content path="${relPath.toPosix()}">\n${finalContent}\n</final_file_content>\n\n` +
|
||||
`Please note:\n` +
|
||||
`1. You do not need to re-write the file with these changes, as they have already been applied.\n` +
|
||||
`2. Proceed with the task using this updated file content as the new baseline.\n` +
|
||||
`3. If the user's edits have addressed part of the task or changed the requirements, adjust your approach accordingly.` +
|
||||
`${newProblemsMessage}`,
|
||||
)
|
||||
await this.diffViewProvider.reset()
|
||||
} catch (error) {
|
||||
handleError("insert block", error)
|
||||
await this.diffViewProvider.reset()
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case "search_and_replace": {
|
||||
const relPath: string | undefined = block.params.path
|
||||
const operations: string | undefined = block.params.operations
|
||||
|
||||
const sharedMessageProps: ClineSayTool = {
|
||||
tool: "appliedDiff",
|
||||
path: getReadablePath(cwd, removeClosingTag("path", relPath)),
|
||||
}
|
||||
|
||||
try {
|
||||
if (block.partial) {
|
||||
const partialMessage = JSON.stringify({
|
||||
path: removeClosingTag("path", relPath),
|
||||
operations: removeClosingTag("operations", operations),
|
||||
})
|
||||
await this.ask("tool", partialMessage, block.partial).catch(() => {})
|
||||
break
|
||||
} else {
|
||||
if (!relPath) {
|
||||
this.consecutiveMistakeCount++
|
||||
pushToolResult(
|
||||
await this.sayAndCreateMissingParamError("search_and_replace", "path"),
|
||||
)
|
||||
break
|
||||
}
|
||||
if (!operations) {
|
||||
this.consecutiveMistakeCount++
|
||||
pushToolResult(
|
||||
await this.sayAndCreateMissingParamError("search_and_replace", "operations"),
|
||||
)
|
||||
break
|
||||
}
|
||||
|
||||
const absolutePath = path.resolve(cwd, relPath)
|
||||
const fileExists = await fileExistsAtPath(absolutePath)
|
||||
|
||||
if (!fileExists) {
|
||||
this.consecutiveMistakeCount++
|
||||
const formattedError = `File does not exist at path: ${absolutePath}\n\n<error_details>\nThe specified file could not be found. Please verify the file path and try again.\n</error_details>`
|
||||
await this.say("error", formattedError)
|
||||
pushToolResult(formattedError)
|
||||
break
|
||||
}
|
||||
|
||||
let parsedOperations: Array<{
|
||||
search: string
|
||||
replace: string
|
||||
start_line?: number
|
||||
end_line?: number
|
||||
use_regex?: boolean
|
||||
ignore_case?: boolean
|
||||
regex_flags?: string
|
||||
}>
|
||||
|
||||
try {
|
||||
parsedOperations = JSON.parse(operations)
|
||||
if (!Array.isArray(parsedOperations)) {
|
||||
throw new Error("Operations must be an array")
|
||||
}
|
||||
} catch (error) {
|
||||
this.consecutiveMistakeCount++
|
||||
await this.say("error", `Failed to parse operations JSON: ${error.message}`)
|
||||
pushToolResult(formatResponse.toolError("Invalid operations JSON format"))
|
||||
break
|
||||
}
|
||||
|
||||
// Read the original file content
|
||||
const fileContent = await fs.readFile(absolutePath, "utf-8")
|
||||
const lines = fileContent.split("\n")
|
||||
let newContent = fileContent
|
||||
|
||||
// Apply each search/replace operation
|
||||
for (const op of parsedOperations) {
|
||||
const searchPattern = op.use_regex
|
||||
? new RegExp(op.search, op.regex_flags || (op.ignore_case ? "gi" : "g"))
|
||||
: new RegExp(escapeRegExp(op.search), op.ignore_case ? "gi" : "g")
|
||||
|
||||
if (op.start_line || op.end_line) {
|
||||
// Line-restricted replacement
|
||||
const startLine = (op.start_line || 1) - 1
|
||||
const endLine = (op.end_line || lines.length) - 1
|
||||
|
||||
const beforeLines = lines.slice(0, startLine)
|
||||
const targetLines = lines.slice(startLine, endLine + 1)
|
||||
const afterLines = lines.slice(endLine + 1)
|
||||
|
||||
const modifiedLines = targetLines.map((line) =>
|
||||
line.replace(searchPattern, op.replace),
|
||||
)
|
||||
|
||||
newContent = [...beforeLines, ...modifiedLines, ...afterLines].join("\n")
|
||||
} else {
|
||||
// Global replacement
|
||||
newContent = newContent.replace(searchPattern, op.replace)
|
||||
}
|
||||
}
|
||||
|
||||
this.consecutiveMistakeCount = 0
|
||||
|
||||
// Show diff preview
|
||||
const diff = formatResponse.createPrettyPatch(
|
||||
relPath,
|
||||
this.diffViewProvider.originalContent,
|
||||
newContent,
|
||||
)
|
||||
|
||||
if (!diff) {
|
||||
pushToolResult(`No changes needed for '${relPath}'`)
|
||||
break
|
||||
}
|
||||
|
||||
await this.diffViewProvider.open(relPath)
|
||||
await this.diffViewProvider.update(newContent, true)
|
||||
this.diffViewProvider.scrollToFirstDiff()
|
||||
|
||||
const completeMessage = JSON.stringify({
|
||||
...sharedMessageProps,
|
||||
diff: diff,
|
||||
} satisfies ClineSayTool)
|
||||
|
||||
const didApprove = await askApproval("tool", completeMessage)
|
||||
if (!didApprove) {
|
||||
await this.diffViewProvider.revertChanges() // This likely handles closing the diff view
|
||||
break
|
||||
}
|
||||
|
||||
const { newProblemsMessage, userEdits, finalContent } =
|
||||
await this.diffViewProvider.saveChanges()
|
||||
this.didEditFile = true // used to determine if we should wait for busy terminal to update before sending api request
|
||||
if (userEdits) {
|
||||
await this.say(
|
||||
"user_feedback_diff",
|
||||
JSON.stringify({
|
||||
tool: fileExists ? "editedExistingFile" : "newFileCreated",
|
||||
path: getReadablePath(cwd, relPath),
|
||||
diff: userEdits,
|
||||
} satisfies ClineSayTool),
|
||||
)
|
||||
pushToolResult(
|
||||
`The user made the following updates to your content:\n\n${userEdits}\n\n` +
|
||||
`The updated content, which includes both your original modifications and the user's edits, has been successfully saved to ${relPath.toPosix()}. Here is the full, updated content of the file, including line numbers:\n\n` +
|
||||
`<final_file_content path="${relPath.toPosix()}">\n${addLineNumbers(finalContent || "")}\n</final_file_content>\n\n` +
|
||||
`Please note:\n` +
|
||||
`1. You do not need to re-write the file with these changes, as they have already been applied.\n` +
|
||||
`2. Proceed with the task using this updated file content as the new baseline.\n` +
|
||||
`3. If the user's edits have addressed part of the task or changed the requirements, adjust your approach accordingly.` +
|
||||
`${newProblemsMessage}`,
|
||||
)
|
||||
} else {
|
||||
pushToolResult(
|
||||
`Changes successfully applied to ${relPath.toPosix()}:\n\n${newProblemsMessage}`,
|
||||
)
|
||||
}
|
||||
await this.diffViewProvider.reset()
|
||||
break
|
||||
}
|
||||
} catch (error) {
|
||||
await handleError("applying search and replace", error)
|
||||
await this.diffViewProvider.reset()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
case "read_file": {
|
||||
const relPath: string | undefined = block.params.path
|
||||
const sharedMessageProps: ClineSayTool = {
|
||||
@@ -2750,3 +3072,7 @@ export class Cline {
|
||||
return `<environment_details>\n${details.trim()}\n</environment_details>`
|
||||
}
|
||||
}
|
||||
|
||||
function escapeRegExp(string: string): string {
|
||||
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
||||
}
|
||||
|
||||
@@ -13,6 +13,8 @@ export const toolUseNames = [
|
||||
"read_file",
|
||||
"write_to_file",
|
||||
"apply_diff",
|
||||
"insert_code_block",
|
||||
"search_and_replace",
|
||||
"search_files",
|
||||
"list_files",
|
||||
"list_code_definition_names",
|
||||
@@ -50,6 +52,7 @@ export const toolParamNames = [
|
||||
"end_line",
|
||||
"mode_slug",
|
||||
"reason",
|
||||
"operations",
|
||||
] as const
|
||||
|
||||
export type ToolParamName = (typeof toolParamNames)[number]
|
||||
@@ -78,6 +81,11 @@ export interface WriteToFileToolUse extends ToolUse {
|
||||
params: Partial<Pick<Record<ToolParamName, string>, "path" | "content" | "line_count">>
|
||||
}
|
||||
|
||||
export interface InsertCodeBlockToolUse extends ToolUse {
|
||||
name: "insert_code_block"
|
||||
params: Partial<Pick<Record<ToolParamName, string>, "path" | "operations">>
|
||||
}
|
||||
|
||||
export interface SearchFilesToolUse extends ToolUse {
|
||||
name: "search_files"
|
||||
params: Partial<Pick<Record<ToolParamName, string>, "path" | "regex" | "file_pattern">>
|
||||
|
||||
31
src/core/diff/insert-groups.ts
Normal file
31
src/core/diff/insert-groups.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* Inserts multiple groups of elements at specified indices in an array
|
||||
* @param original Array to insert into, split by lines
|
||||
* @param insertGroups Array of groups to insert, each with an index and elements to insert
|
||||
* @returns New array with all insertions applied
|
||||
*/
|
||||
export interface InsertGroup {
|
||||
index: number
|
||||
elements: string[]
|
||||
}
|
||||
|
||||
export function insertGroups(original: string[], insertGroups: InsertGroup[]): string[] {
|
||||
// Sort groups by index to maintain order
|
||||
insertGroups.sort((a, b) => a.index - b.index)
|
||||
|
||||
let result: string[] = []
|
||||
let lastIndex = 0
|
||||
|
||||
insertGroups.forEach(({ index, elements }) => {
|
||||
// Add elements from original array up to insertion point
|
||||
result.push(...original.slice(lastIndex, index))
|
||||
// Add the group of elements
|
||||
result.push(...elements)
|
||||
lastIndex = index
|
||||
})
|
||||
|
||||
// Add remaining elements from original array
|
||||
result.push(...original.slice(lastIndex))
|
||||
|
||||
return result
|
||||
}
|
||||
@@ -3,6 +3,8 @@ import { getReadFileDescription } from "./read-file"
|
||||
import { getWriteToFileDescription } from "./write-to-file"
|
||||
import { getSearchFilesDescription } from "./search-files"
|
||||
import { getListFilesDescription } from "./list-files"
|
||||
import { getInsertCodeBlockDescription } from "./insert-code-block"
|
||||
import { getSearchAndReplaceDescription } from "./search-and-replace"
|
||||
import { getListCodeDefinitionNamesDescription } from "./list-code-definition-names"
|
||||
import { getBrowserActionDescription } from "./browser-action"
|
||||
import { getAskFollowupQuestionDescription } from "./ask-followup-question"
|
||||
@@ -30,6 +32,8 @@ const toolDescriptionMap: Record<string, (args: ToolArgs) => string | undefined>
|
||||
use_mcp_tool: (args) => getUseMcpToolDescription(args),
|
||||
access_mcp_resource: (args) => getAccessMcpResourceDescription(args),
|
||||
switch_mode: () => getSwitchModeDescription(),
|
||||
insert_code_block: (args) => getInsertCodeBlockDescription(args),
|
||||
search_and_replace: (args) => getSearchAndReplaceDescription(args),
|
||||
apply_diff: (args) =>
|
||||
args.diffStrategy ? args.diffStrategy.getToolDescription({ cwd: args.cwd, toolOptions: args.toolOptions }) : "",
|
||||
}
|
||||
@@ -100,4 +104,6 @@ export {
|
||||
getUseMcpToolDescription,
|
||||
getAccessMcpResourceDescription,
|
||||
getSwitchModeDescription,
|
||||
getInsertCodeBlockDescription,
|
||||
getSearchAndReplaceDescription,
|
||||
}
|
||||
|
||||
35
src/core/prompts/tools/insert-code-block.ts
Normal file
35
src/core/prompts/tools/insert-code-block.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { ToolArgs } from "./types"
|
||||
|
||||
export function getInsertCodeBlockDescription(args: ToolArgs): string {
|
||||
return `## insert_code_block
|
||||
Description: Inserts code blocks at specific line positions in a file. This is the primary tool for adding new code (functions/methods/classes, imports, attributes etc.) as it allows for precise insertions without overwriting existing content. The tool uses an efficient line-based insertion system that maintains file integrity and proper ordering of multiple insertions. Beware to use the proper indentation. This tool is the preferred way to add new code to files.
|
||||
Parameters:
|
||||
- path: (required) The path of the file to insert code into (relative to the current working directory ${args.cwd.toPosix()})
|
||||
- operations: (required) A JSON array of insertion operations. Each operation is an object with:
|
||||
* start_line: (required) The line number where the code block should be inserted
|
||||
* content: (required) The code block to insert at the specified position
|
||||
Usage:
|
||||
<insert_code_block>
|
||||
<path>File path here</path>
|
||||
<operations>[
|
||||
{
|
||||
"start_line": 10,
|
||||
"content": "Your code block here"
|
||||
}
|
||||
]</operations>
|
||||
</insert_code_block>
|
||||
Example: Insert a new function and its import statement
|
||||
<insert_code_block>
|
||||
<path>src/app.ts</path>
|
||||
<operations>[
|
||||
{
|
||||
"start_line": 1,
|
||||
"content": "import { sum } from './utils';"
|
||||
},
|
||||
{
|
||||
"start_line": 10,
|
||||
"content": "function calculateTotal(items: number[]): number {\n return items.reduce((sum, item) => sum + item, 0);\n}"
|
||||
}
|
||||
]</operations>
|
||||
</insert_code_block>`
|
||||
}
|
||||
52
src/core/prompts/tools/search-and-replace.ts
Normal file
52
src/core/prompts/tools/search-and-replace.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { ToolArgs } from "./types"
|
||||
|
||||
export function getSearchAndReplaceDescription(args: ToolArgs): string {
|
||||
return `## search_and_replace
|
||||
Description: Request to perform search and replace operations on a file. Each operation can specify a search pattern (string or regex) and replacement text, with optional line range restrictions and regex flags. Shows a diff preview before applying changes.
|
||||
Parameters:
|
||||
- path: (required) The path of the file to modify (relative to the current working directory ${args.cwd.toPosix()})
|
||||
- operations: (required) A JSON array of search/replace operations. Each operation is an object with:
|
||||
* search: (required) The text or pattern to search for
|
||||
* replace: (required) The text to replace matches with
|
||||
* start_line: (optional) Starting line number for restricted replacement
|
||||
* end_line: (optional) Ending line number for restricted replacement
|
||||
* use_regex: (optional) Whether to treat search as a regex pattern
|
||||
* ignore_case: (optional) Whether to ignore case when matching
|
||||
* regex_flags: (optional) Additional regex flags when use_regex is true
|
||||
Usage:
|
||||
<search_and_replace>
|
||||
<path>File path here</path>
|
||||
<operations>[
|
||||
{
|
||||
"search": "text to find",
|
||||
"replace": "replacement text",
|
||||
"start_line": 1,
|
||||
"end_line": 10
|
||||
}
|
||||
]</operations>
|
||||
</search_and_replace>
|
||||
Example: Replace "foo" with "bar" in lines 1-10 of example.ts
|
||||
<search_and_replace>
|
||||
<path>example.ts</path>
|
||||
<operations>[
|
||||
{
|
||||
"search": "foo",
|
||||
"replace": "bar",
|
||||
"start_line": 1,
|
||||
"end_line": 10
|
||||
}
|
||||
]</operations>
|
||||
</search_and_replace>
|
||||
Example: Replace all occurrences of "old" with "new" using regex
|
||||
<search_and_replace>
|
||||
<path>example.ts</path>
|
||||
<operations>[
|
||||
{
|
||||
"search": "old\\w+",
|
||||
"replace": "new$&",
|
||||
"use_regex": true,
|
||||
"ignore_case": true
|
||||
}
|
||||
]</operations>
|
||||
</search_and_replace>`
|
||||
}
|
||||
@@ -21,7 +21,7 @@ export const TOOL_DISPLAY_NAMES = {
|
||||
// Define available tool groups
|
||||
export const TOOL_GROUPS: Record<string, ToolGroupValues> = {
|
||||
read: ["read_file", "search_files", "list_files", "list_code_definition_names"],
|
||||
edit: ["write_to_file", "apply_diff"],
|
||||
edit: ["write_to_file", "apply_diff", "insert_code_block", "search_and_replace"],
|
||||
browser: ["browser_action"],
|
||||
command: ["execute_command"],
|
||||
mcp: ["use_mcp_tool", "access_mcp_resource"],
|
||||
|
||||
Reference in New Issue
Block a user