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