Checkpoint on insert and search/replace tools

This commit is contained in:
Matt Rubens
2025-01-21 23:49:48 -08:00
committed by sam hoang
parent f07109b686
commit 2c97b59ed1
7 changed files with 459 additions and 1 deletions

View File

@@ -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, "\\$&")
}