mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-07 08:26:51 +00:00
Move apply_diff to a tool file (#2094)
This commit is contained in:
parent
26c3fe6535
commit
02f63fc522
2 changed files with 186 additions and 173 deletions
|
|
@ -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<Anthropic.TextBlockParam | Anthropic.ImageBlockParam>
|
||||
type UserContent = Array<Anthropic.Messages.ContentBlockParam>
|
||||
|
|
@ -151,7 +152,7 @@ export class Cline extends EventEmitter<ClineEvents> {
|
|||
private lastMessageTs?: number
|
||||
// Not private since it needs to be accessible by tools
|
||||
consecutiveMistakeCount: number = 0
|
||||
private consecutiveMistakeCountForApplyDiff: Map<string, number> = new Map()
|
||||
consecutiveMistakeCountForApplyDiff: Map<string, number> = new Map()
|
||||
// Not private since it needs to be accessible by tools
|
||||
providerRef: WeakRef<ClineProvider>
|
||||
private abort: boolean = false
|
||||
|
|
@ -1568,178 +1569,9 @@ export class Cline extends EventEmitter<ClineEvents> {
|
|||
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<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
|
||||
}
|
||||
|
||||
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 = `<error_details>\n${
|
||||
failPart.error
|
||||
}${errorDetails ? `\n\nDetails:\n${errorDetails}` : ""}\n</error_details>`
|
||||
partResults += formattedError
|
||||
}
|
||||
} else {
|
||||
const errorDetails = diffResult.details
|
||||
? JSON.stringify(diffResult.details, null, 2)
|
||||
: ""
|
||||
formattedError = `Unable to apply diff to file: ${absolutePath}\n\n<error_details>\n${
|
||||
diffResult.error
|
||||
}${errorDetails ? `\n\nDetails:\n${errorDetails}` : ""}\n</error_details>`
|
||||
}
|
||||
|
||||
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 <read_file> 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` +
|
||||
`<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}\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
|
||||
|
|
|
|||
181
src/core/tools/applyDiffTool.ts
Normal file
181
src/core/tools/applyDiffTool.ts
Normal file
|
|
@ -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<error_details>\nThe specified file could not be found. Please verify the file path and try again.\n</error_details>`
|
||||
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 = `<error_details>\n${
|
||||
failPart.error
|
||||
}${errorDetails ? `\n\nDetails:\n${errorDetails}` : ""}\n</error_details>`
|
||||
partResults += formattedError
|
||||
}
|
||||
} else {
|
||||
const errorDetails = diffResult.details ? JSON.stringify(diffResult.details, null, 2) : ""
|
||||
formattedError = `Unable to apply diff to file: ${absolutePath}\n\n<error_details>\n${
|
||||
diffResult.error
|
||||
}${errorDetails ? `\n\nDetails:\n${errorDetails}` : ""}\n</error_details>`
|
||||
}
|
||||
|
||||
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 <read_file> 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` +
|
||||
`<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 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
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue