From 02f63fc522dc6e1edb13ab1923c76d87226f976b Mon Sep 17 00:00:00 2001 From: Matt Rubens Date: Sun, 30 Mar 2025 01:04:38 -0400 Subject: [PATCH] Move apply_diff to a tool file (#2094) --- src/core/Cline.ts | 178 +------------------------------ src/core/tools/applyDiffTool.ts | 181 ++++++++++++++++++++++++++++++++ 2 files changed, 186 insertions(+), 173 deletions(-) create mode 100644 src/core/tools/applyDiffTool.ts diff --git a/src/core/Cline.ts b/src/core/Cline.ts index 6f6984c915..e8e2e72deb 100644 --- a/src/core/Cline.ts +++ b/src/core/Cline.ts @@ -86,6 +86,7 @@ import { validateToolUse, isToolAllowedForMode, ToolName } from "./mode-validato import { parseXml } from "../utils/xml" import { getWorkspacePath } from "../utils/path" import { writeToFileTool } from "./tools/writeToFileTool" +import { applyDiffTool } from "./tools/applyDiffTool" export type ToolResponse = string | Array type UserContent = Array @@ -151,7 +152,7 @@ export class Cline extends EventEmitter { private lastMessageTs?: number // Not private since it needs to be accessible by tools consecutiveMistakeCount: number = 0 - private consecutiveMistakeCountForApplyDiff: Map = new Map() + consecutiveMistakeCountForApplyDiff: Map = new Map() // Not private since it needs to be accessible by tools providerRef: WeakRef private abort: boolean = false @@ -1568,178 +1569,9 @@ export class Cline extends EventEmitter { case "write_to_file": await writeToFileTool(this, block, askApproval, handleError, pushToolResult, removeClosingTag) break - case "apply_diff": { - const relPath: string | undefined = block.params.path - const diffContent: string | undefined = block.params.diff - - const sharedMessageProps: ClineSayTool = { - tool: "appliedDiff", - path: getReadablePath(this.cwd, removeClosingTag("path", relPath)), - } - - try { - if (block.partial) { - // update gui message - let toolProgressStatus - if (this.diffStrategy && this.diffStrategy.getProgressStatus) { - toolProgressStatus = this.diffStrategy.getProgressStatus(block) - } - - const partialMessage = JSON.stringify(sharedMessageProps) - - await this.ask("tool", partialMessage, block.partial, toolProgressStatus).catch( - () => {}, - ) - break - } else { - if (!relPath) { - this.consecutiveMistakeCount++ - pushToolResult(await this.sayAndCreateMissingParamError("apply_diff", "path")) - break - } - if (!diffContent) { - this.consecutiveMistakeCount++ - pushToolResult(await this.sayAndCreateMissingParamError("apply_diff", "diff")) - break - } - - const accessAllowed = this.rooIgnoreController?.validateAccess(relPath) - if (!accessAllowed) { - await this.say("rooignore_error", relPath) - pushToolResult(formatResponse.toolError(formatResponse.rooIgnoreError(relPath))) - - break - } - - const absolutePath = path.resolve(this.cwd, relPath) - const fileExists = await fileExistsAtPath(absolutePath) - - if (!fileExists) { - this.consecutiveMistakeCount++ - const formattedError = `File does not exist at path: ${absolutePath}\n\n\nThe specified file could not be found. Please verify the file path and try again.\n` - await this.say("error", formattedError) - pushToolResult(formattedError) - break - } - - const originalContent = await fs.readFile(absolutePath, "utf-8") - - // Apply the diff to the original content - const diffResult = (await this.diffStrategy?.applyDiff( - originalContent, - diffContent, - parseInt(block.params.start_line ?? ""), - parseInt(block.params.end_line ?? ""), - )) ?? { - success: false, - error: "No diff strategy available", - } - let partResults = "" - - if (!diffResult.success) { - this.consecutiveMistakeCount++ - const currentCount = - (this.consecutiveMistakeCountForApplyDiff.get(relPath) || 0) + 1 - this.consecutiveMistakeCountForApplyDiff.set(relPath, currentCount) - let formattedError = "" - if (diffResult.failParts && diffResult.failParts.length > 0) { - for (const failPart of diffResult.failParts) { - if (failPart.success) { - continue - } - const errorDetails = failPart.details - ? JSON.stringify(failPart.details, null, 2) - : "" - formattedError = `\n${ - failPart.error - }${errorDetails ? `\n\nDetails:\n${errorDetails}` : ""}\n` - partResults += formattedError - } - } else { - const errorDetails = diffResult.details - ? JSON.stringify(diffResult.details, null, 2) - : "" - formattedError = `Unable to apply diff to file: ${absolutePath}\n\n\n${ - diffResult.error - }${errorDetails ? `\n\nDetails:\n${errorDetails}` : ""}\n` - } - - if (currentCount >= 2) { - await this.say("error", formattedError) - } - pushToolResult(formattedError) - break - } - - this.consecutiveMistakeCount = 0 - this.consecutiveMistakeCountForApplyDiff.delete(relPath) - // Show diff view before asking for approval - this.diffViewProvider.editType = "modify" - await this.diffViewProvider.open(relPath) - await this.diffViewProvider.update(diffResult.content, true) - await this.diffViewProvider.scrollToFirstDiff() - - const completeMessage = JSON.stringify({ - ...sharedMessageProps, - diff: diffContent, - } satisfies ClineSayTool) - - let toolProgressStatus - if (this.diffStrategy && this.diffStrategy.getProgressStatus) { - toolProgressStatus = this.diffStrategy.getProgressStatus(block, diffResult) - } - - const didApprove = await askApproval("tool", completeMessage, toolProgressStatus) - 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 - let partFailHint = "" - if (diffResult.failParts && diffResult.failParts.length > 0) { - partFailHint = `But unable to apply all diff parts to file: ${absolutePath}, silently use tool to check newest file version and re-apply diffs\n` - } - if (userEdits) { - await this.say( - "user_feedback_diff", - JSON.stringify({ - tool: fileExists ? "editedExistingFile" : "newFileCreated", - path: getReadablePath(this.cwd, relPath), - diff: userEdits, - } satisfies ClineSayTool), - ) - pushToolResult( - `The user made the following updates to your content:\n\n${userEdits}\n\n` + - partFailHint + - `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` + - `\n${addLineNumbers( - finalContent || "", - )}\n\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}\n` + - partFailHint, - ) - } - await this.diffViewProvider.reset() - break - } - } catch (error) { - await handleError("applying diff", error) - await this.diffViewProvider.reset() - break - } - } - + case "apply_diff": + await applyDiffTool(this, block, askApproval, handleError, pushToolResult, removeClosingTag) + break case "insert_content": { const relPath: string | undefined = block.params.path const operations: string | undefined = block.params.operations diff --git a/src/core/tools/applyDiffTool.ts b/src/core/tools/applyDiffTool.ts new file mode 100644 index 0000000000..a20bace097 --- /dev/null +++ b/src/core/tools/applyDiffTool.ts @@ -0,0 +1,181 @@ +import { ClineSayTool } from "../../shared/ExtensionMessage" +import { getReadablePath } from "../../utils/path" +import { ToolUse } from "../assistant-message" +import { Cline } from "../Cline" +import { RemoveClosingTag } from "./types" +import { formatResponse } from "../prompts/responses" +import { AskApproval, HandleError, PushToolResult } from "./types" +import { fileExistsAtPath } from "../../utils/fs" +import { addLineNumbers } from "../../integrations/misc/extract-text" +import path from "path" +import fs from "fs/promises" + +export async function applyDiffTool( + cline: Cline, + block: ToolUse, + askApproval: AskApproval, + handleError: HandleError, + pushToolResult: PushToolResult, + removeClosingTag: RemoveClosingTag, +) { + const relPath: string | undefined = block.params.path + const diffContent: string | undefined = block.params.diff + + const sharedMessageProps: ClineSayTool = { + tool: "appliedDiff", + path: getReadablePath(cline.cwd, removeClosingTag("path", relPath)), + } + + try { + if (block.partial) { + // update gui message + let toolProgressStatus + if (cline.diffStrategy && cline.diffStrategy.getProgressStatus) { + toolProgressStatus = cline.diffStrategy.getProgressStatus(block) + } + + const partialMessage = JSON.stringify(sharedMessageProps) + + await cline.ask("tool", partialMessage, block.partial, toolProgressStatus).catch(() => {}) + return + } else { + if (!relPath) { + cline.consecutiveMistakeCount++ + pushToolResult(await cline.sayAndCreateMissingParamError("apply_diff", "path")) + return + } + if (!diffContent) { + cline.consecutiveMistakeCount++ + pushToolResult(await cline.sayAndCreateMissingParamError("apply_diff", "diff")) + return + } + + const accessAllowed = cline.rooIgnoreController?.validateAccess(relPath) + if (!accessAllowed) { + await cline.say("rooignore_error", relPath) + pushToolResult(formatResponse.toolError(formatResponse.rooIgnoreError(relPath))) + + return + } + + const absolutePath = path.resolve(cline.cwd, relPath) + const fileExists = await fileExistsAtPath(absolutePath) + + if (!fileExists) { + cline.consecutiveMistakeCount++ + const formattedError = `File does not exist at path: ${absolutePath}\n\n\nThe specified file could not be found. Please verify the file path and try again.\n` + await cline.say("error", formattedError) + pushToolResult(formattedError) + return + } + + const originalContent = await fs.readFile(absolutePath, "utf-8") + + // Apply the diff to the original content + const diffResult = (await cline.diffStrategy?.applyDiff( + originalContent, + diffContent, + parseInt(block.params.start_line ?? ""), + parseInt(block.params.end_line ?? ""), + )) ?? { + success: false, + error: "No diff strategy available", + } + let partResults = "" + + if (!diffResult.success) { + cline.consecutiveMistakeCount++ + const currentCount = (cline.consecutiveMistakeCountForApplyDiff.get(relPath) || 0) + 1 + cline.consecutiveMistakeCountForApplyDiff.set(relPath, currentCount) + let formattedError = "" + if (diffResult.failParts && diffResult.failParts.length > 0) { + for (const failPart of diffResult.failParts) { + if (failPart.success) { + continue + } + const errorDetails = failPart.details ? JSON.stringify(failPart.details, null, 2) : "" + formattedError = `\n${ + failPart.error + }${errorDetails ? `\n\nDetails:\n${errorDetails}` : ""}\n` + partResults += formattedError + } + } else { + const errorDetails = diffResult.details ? JSON.stringify(diffResult.details, null, 2) : "" + formattedError = `Unable to apply diff to file: ${absolutePath}\n\n\n${ + diffResult.error + }${errorDetails ? `\n\nDetails:\n${errorDetails}` : ""}\n` + } + + if (currentCount >= 2) { + await cline.say("error", formattedError) + } + pushToolResult(formattedError) + return + } + + cline.consecutiveMistakeCount = 0 + cline.consecutiveMistakeCountForApplyDiff.delete(relPath) + // Show diff view before asking for approval + cline.diffViewProvider.editType = "modify" + await cline.diffViewProvider.open(relPath) + await cline.diffViewProvider.update(diffResult.content, true) + await cline.diffViewProvider.scrollToFirstDiff() + + const completeMessage = JSON.stringify({ + ...sharedMessageProps, + diff: diffContent, + } satisfies ClineSayTool) + + let toolProgressStatus + if (cline.diffStrategy && cline.diffStrategy.getProgressStatus) { + toolProgressStatus = cline.diffStrategy.getProgressStatus(block, diffResult) + } + + const didApprove = await askApproval("tool", completeMessage, toolProgressStatus) + if (!didApprove) { + await cline.diffViewProvider.revertChanges() // cline likely handles closing the diff view + return + } + + const { newProblemsMessage, userEdits, finalContent } = await cline.diffViewProvider.saveChanges() + cline.didEditFile = true // used to determine if we should wait for busy terminal to update before sending api request + let partFailHint = "" + if (diffResult.failParts && diffResult.failParts.length > 0) { + partFailHint = `But unable to apply all diff parts to file: ${absolutePath}, silently use tool to check newest file version and re-apply diffs\n` + } + if (userEdits) { + await cline.say( + "user_feedback_diff", + JSON.stringify({ + tool: fileExists ? "editedExistingFile" : "newFileCreated", + path: getReadablePath(cline.cwd, relPath), + diff: userEdits, + } satisfies ClineSayTool), + ) + pushToolResult( + `The user made the following updates to your content:\n\n${userEdits}\n\n` + + partFailHint + + `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` + + `\n${addLineNumbers( + finalContent || "", + )}\n\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 cline 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}\n` + partFailHint, + ) + } + await cline.diffViewProvider.reset() + return + } + } catch (error) { + await handleError("applying diff", error) + await cline.diffViewProvider.reset() + return + } +}