Skip to content

Commit 21fa5a7

Browse files
committed
Move apply_diff to a tool file
1 parent 26c3fe6 commit 21fa5a7

File tree

2 files changed

+186
-173
lines changed

2 files changed

+186
-173
lines changed

src/core/Cline.ts

Lines changed: 5 additions & 173 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ import { validateToolUse, isToolAllowedForMode, ToolName } from "./mode-validato
8686
import { parseXml } from "../utils/xml"
8787
import { getWorkspacePath } from "../utils/path"
8888
import { writeToFileTool } from "./tools/writeToFileTool"
89+
import { applyDiffTool } from "./tools/applyDiffTool"
8990

9091
export type ToolResponse = string | Array<Anthropic.TextBlockParam | Anthropic.ImageBlockParam>
9192
type UserContent = Array<Anthropic.Messages.ContentBlockParam>
@@ -151,7 +152,7 @@ export class Cline extends EventEmitter<ClineEvents> {
151152
private lastMessageTs?: number
152153
// Not private since it needs to be accessible by tools
153154
consecutiveMistakeCount: number = 0
154-
private consecutiveMistakeCountForApplyDiff: Map<string, number> = new Map()
155+
consecutiveMistakeCountForApplyDiff: Map<string, number> = new Map()
155156
// Not private since it needs to be accessible by tools
156157
providerRef: WeakRef<ClineProvider>
157158
private abort: boolean = false
@@ -1568,178 +1569,9 @@ export class Cline extends EventEmitter<ClineEvents> {
15681569
case "write_to_file":
15691570
await writeToFileTool(this, block, askApproval, handleError, pushToolResult, removeClosingTag)
15701571
break
1571-
case "apply_diff": {
1572-
const relPath: string | undefined = block.params.path
1573-
const diffContent: string | undefined = block.params.diff
1574-
1575-
const sharedMessageProps: ClineSayTool = {
1576-
tool: "appliedDiff",
1577-
path: getReadablePath(this.cwd, removeClosingTag("path", relPath)),
1578-
}
1579-
1580-
try {
1581-
if (block.partial) {
1582-
// update gui message
1583-
let toolProgressStatus
1584-
if (this.diffStrategy && this.diffStrategy.getProgressStatus) {
1585-
toolProgressStatus = this.diffStrategy.getProgressStatus(block)
1586-
}
1587-
1588-
const partialMessage = JSON.stringify(sharedMessageProps)
1589-
1590-
await this.ask("tool", partialMessage, block.partial, toolProgressStatus).catch(
1591-
() => {},
1592-
)
1593-
break
1594-
} else {
1595-
if (!relPath) {
1596-
this.consecutiveMistakeCount++
1597-
pushToolResult(await this.sayAndCreateMissingParamError("apply_diff", "path"))
1598-
break
1599-
}
1600-
if (!diffContent) {
1601-
this.consecutiveMistakeCount++
1602-
pushToolResult(await this.sayAndCreateMissingParamError("apply_diff", "diff"))
1603-
break
1604-
}
1605-
1606-
const accessAllowed = this.rooIgnoreController?.validateAccess(relPath)
1607-
if (!accessAllowed) {
1608-
await this.say("rooignore_error", relPath)
1609-
pushToolResult(formatResponse.toolError(formatResponse.rooIgnoreError(relPath)))
1610-
1611-
break
1612-
}
1613-
1614-
const absolutePath = path.resolve(this.cwd, relPath)
1615-
const fileExists = await fileExistsAtPath(absolutePath)
1616-
1617-
if (!fileExists) {
1618-
this.consecutiveMistakeCount++
1619-
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>`
1620-
await this.say("error", formattedError)
1621-
pushToolResult(formattedError)
1622-
break
1623-
}
1624-
1625-
const originalContent = await fs.readFile(absolutePath, "utf-8")
1626-
1627-
// Apply the diff to the original content
1628-
const diffResult = (await this.diffStrategy?.applyDiff(
1629-
originalContent,
1630-
diffContent,
1631-
parseInt(block.params.start_line ?? ""),
1632-
parseInt(block.params.end_line ?? ""),
1633-
)) ?? {
1634-
success: false,
1635-
error: "No diff strategy available",
1636-
}
1637-
let partResults = ""
1638-
1639-
if (!diffResult.success) {
1640-
this.consecutiveMistakeCount++
1641-
const currentCount =
1642-
(this.consecutiveMistakeCountForApplyDiff.get(relPath) || 0) + 1
1643-
this.consecutiveMistakeCountForApplyDiff.set(relPath, currentCount)
1644-
let formattedError = ""
1645-
if (diffResult.failParts && diffResult.failParts.length > 0) {
1646-
for (const failPart of diffResult.failParts) {
1647-
if (failPart.success) {
1648-
continue
1649-
}
1650-
const errorDetails = failPart.details
1651-
? JSON.stringify(failPart.details, null, 2)
1652-
: ""
1653-
formattedError = `<error_details>\n${
1654-
failPart.error
1655-
}${errorDetails ? `\n\nDetails:\n${errorDetails}` : ""}\n</error_details>`
1656-
partResults += formattedError
1657-
}
1658-
} else {
1659-
const errorDetails = diffResult.details
1660-
? JSON.stringify(diffResult.details, null, 2)
1661-
: ""
1662-
formattedError = `Unable to apply diff to file: ${absolutePath}\n\n<error_details>\n${
1663-
diffResult.error
1664-
}${errorDetails ? `\n\nDetails:\n${errorDetails}` : ""}\n</error_details>`
1665-
}
1666-
1667-
if (currentCount >= 2) {
1668-
await this.say("error", formattedError)
1669-
}
1670-
pushToolResult(formattedError)
1671-
break
1672-
}
1673-
1674-
this.consecutiveMistakeCount = 0
1675-
this.consecutiveMistakeCountForApplyDiff.delete(relPath)
1676-
// Show diff view before asking for approval
1677-
this.diffViewProvider.editType = "modify"
1678-
await this.diffViewProvider.open(relPath)
1679-
await this.diffViewProvider.update(diffResult.content, true)
1680-
await this.diffViewProvider.scrollToFirstDiff()
1681-
1682-
const completeMessage = JSON.stringify({
1683-
...sharedMessageProps,
1684-
diff: diffContent,
1685-
} satisfies ClineSayTool)
1686-
1687-
let toolProgressStatus
1688-
if (this.diffStrategy && this.diffStrategy.getProgressStatus) {
1689-
toolProgressStatus = this.diffStrategy.getProgressStatus(block, diffResult)
1690-
}
1691-
1692-
const didApprove = await askApproval("tool", completeMessage, toolProgressStatus)
1693-
if (!didApprove) {
1694-
await this.diffViewProvider.revertChanges() // This likely handles closing the diff view
1695-
break
1696-
}
1697-
1698-
const { newProblemsMessage, userEdits, finalContent } =
1699-
await this.diffViewProvider.saveChanges()
1700-
this.didEditFile = true // used to determine if we should wait for busy terminal to update before sending api request
1701-
let partFailHint = ""
1702-
if (diffResult.failParts && diffResult.failParts.length > 0) {
1703-
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`
1704-
}
1705-
if (userEdits) {
1706-
await this.say(
1707-
"user_feedback_diff",
1708-
JSON.stringify({
1709-
tool: fileExists ? "editedExistingFile" : "newFileCreated",
1710-
path: getReadablePath(this.cwd, relPath),
1711-
diff: userEdits,
1712-
} satisfies ClineSayTool),
1713-
)
1714-
pushToolResult(
1715-
`The user made the following updates to your content:\n\n${userEdits}\n\n` +
1716-
partFailHint +
1717-
`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` +
1718-
`<final_file_content path="${relPath.toPosix()}">\n${addLineNumbers(
1719-
finalContent || "",
1720-
)}\n</final_file_content>\n\n` +
1721-
`Please note:\n` +
1722-
`1. You do not need to re-write the file with these changes, as they have already been applied.\n` +
1723-
`2. Proceed with the task using this updated file content as the new baseline.\n` +
1724-
`3. If the user's edits have addressed part of the task or changed the requirements, adjust your approach accordingly.` +
1725-
`${newProblemsMessage}`,
1726-
)
1727-
} else {
1728-
pushToolResult(
1729-
`Changes successfully applied to ${relPath.toPosix()}:\n\n${newProblemsMessage}\n` +
1730-
partFailHint,
1731-
)
1732-
}
1733-
await this.diffViewProvider.reset()
1734-
break
1735-
}
1736-
} catch (error) {
1737-
await handleError("applying diff", error)
1738-
await this.diffViewProvider.reset()
1739-
break
1740-
}
1741-
}
1742-
1572+
case "apply_diff":
1573+
await applyDiffTool(this, block, askApproval, handleError, pushToolResult, removeClosingTag)
1574+
break
17431575
case "insert_content": {
17441576
const relPath: string | undefined = block.params.path
17451577
const operations: string | undefined = block.params.operations

src/core/tools/applyDiffTool.ts

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
import { ClineSayTool } from "../../shared/ExtensionMessage"
2+
import { getReadablePath } from "../../utils/path"
3+
import { ToolUse } from "../assistant-message"
4+
import { Cline } from "../Cline"
5+
import { RemoveClosingTag } from "./types"
6+
import { formatResponse } from "../prompts/responses"
7+
import { AskApproval, HandleError, PushToolResult } from "./types"
8+
import { fileExistsAtPath } from "../../utils/fs"
9+
import { addLineNumbers } from "../../integrations/misc/extract-text"
10+
import path from "path"
11+
import fs from "fs/promises"
12+
13+
export async function applyDiffTool(
14+
cline: Cline,
15+
block: ToolUse,
16+
askApproval: AskApproval,
17+
handleError: HandleError,
18+
pushToolResult: PushToolResult,
19+
removeClosingTag: RemoveClosingTag,
20+
) {
21+
const relPath: string | undefined = block.params.path
22+
const diffContent: string | undefined = block.params.diff
23+
24+
const sharedMessageProps: ClineSayTool = {
25+
tool: "appliedDiff",
26+
path: getReadablePath(cline.cwd, removeClosingTag("path", relPath)),
27+
}
28+
29+
try {
30+
if (block.partial) {
31+
// update gui message
32+
let toolProgressStatus
33+
if (cline.diffStrategy && cline.diffStrategy.getProgressStatus) {
34+
toolProgressStatus = cline.diffStrategy.getProgressStatus(block)
35+
}
36+
37+
const partialMessage = JSON.stringify(sharedMessageProps)
38+
39+
await cline.ask("tool", partialMessage, block.partial, toolProgressStatus).catch(() => {})
40+
return
41+
} else {
42+
if (!relPath) {
43+
cline.consecutiveMistakeCount++
44+
pushToolResult(await cline.sayAndCreateMissingParamError("apply_diff", "path"))
45+
return
46+
}
47+
if (!diffContent) {
48+
cline.consecutiveMistakeCount++
49+
pushToolResult(await cline.sayAndCreateMissingParamError("apply_diff", "diff"))
50+
return
51+
}
52+
53+
const accessAllowed = cline.rooIgnoreController?.validateAccess(relPath)
54+
if (!accessAllowed) {
55+
await cline.say("rooignore_error", relPath)
56+
pushToolResult(formatResponse.toolError(formatResponse.rooIgnoreError(relPath)))
57+
58+
return
59+
}
60+
61+
const absolutePath = path.resolve(cline.cwd, relPath)
62+
const fileExists = await fileExistsAtPath(absolutePath)
63+
64+
if (!fileExists) {
65+
cline.consecutiveMistakeCount++
66+
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>`
67+
await cline.say("error", formattedError)
68+
pushToolResult(formattedError)
69+
return
70+
}
71+
72+
const originalContent = await fs.readFile(absolutePath, "utf-8")
73+
74+
// Apply the diff to the original content
75+
const diffResult = (await cline.diffStrategy?.applyDiff(
76+
originalContent,
77+
diffContent,
78+
parseInt(block.params.start_line ?? ""),
79+
parseInt(block.params.end_line ?? ""),
80+
)) ?? {
81+
success: false,
82+
error: "No diff strategy available",
83+
}
84+
let partResults = ""
85+
86+
if (!diffResult.success) {
87+
cline.consecutiveMistakeCount++
88+
const currentCount = (cline.consecutiveMistakeCountForApplyDiff.get(relPath) || 0) + 1
89+
cline.consecutiveMistakeCountForApplyDiff.set(relPath, currentCount)
90+
let formattedError = ""
91+
if (diffResult.failParts && diffResult.failParts.length > 0) {
92+
for (const failPart of diffResult.failParts) {
93+
if (failPart.success) {
94+
continue
95+
}
96+
const errorDetails = failPart.details ? JSON.stringify(failPart.details, null, 2) : ""
97+
formattedError = `<error_details>\n${
98+
failPart.error
99+
}${errorDetails ? `\n\nDetails:\n${errorDetails}` : ""}\n</error_details>`
100+
partResults += formattedError
101+
}
102+
} else {
103+
const errorDetails = diffResult.details ? JSON.stringify(diffResult.details, null, 2) : ""
104+
formattedError = `Unable to apply diff to file: ${absolutePath}\n\n<error_details>\n${
105+
diffResult.error
106+
}${errorDetails ? `\n\nDetails:\n${errorDetails}` : ""}\n</error_details>`
107+
}
108+
109+
if (currentCount >= 2) {
110+
await cline.say("error", formattedError)
111+
}
112+
pushToolResult(formattedError)
113+
return
114+
}
115+
116+
cline.consecutiveMistakeCount = 0
117+
cline.consecutiveMistakeCountForApplyDiff.delete(relPath)
118+
// Show diff view before asking for approval
119+
cline.diffViewProvider.editType = "modify"
120+
await cline.diffViewProvider.open(relPath)
121+
await cline.diffViewProvider.update(diffResult.content, true)
122+
await cline.diffViewProvider.scrollToFirstDiff()
123+
124+
const completeMessage = JSON.stringify({
125+
...sharedMessageProps,
126+
diff: diffContent,
127+
} satisfies ClineSayTool)
128+
129+
let toolProgressStatus
130+
if (cline.diffStrategy && cline.diffStrategy.getProgressStatus) {
131+
toolProgressStatus = cline.diffStrategy.getProgressStatus(block, diffResult)
132+
}
133+
134+
const didApprove = await askApproval("tool", completeMessage, toolProgressStatus)
135+
if (!didApprove) {
136+
await cline.diffViewProvider.revertChanges() // cline likely handles closing the diff view
137+
return
138+
}
139+
140+
const { newProblemsMessage, userEdits, finalContent } = await cline.diffViewProvider.saveChanges()
141+
cline.didEditFile = true // used to determine if we should wait for busy terminal to update before sending api request
142+
let partFailHint = ""
143+
if (diffResult.failParts && diffResult.failParts.length > 0) {
144+
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`
145+
}
146+
if (userEdits) {
147+
await cline.say(
148+
"user_feedback_diff",
149+
JSON.stringify({
150+
tool: fileExists ? "editedExistingFile" : "newFileCreated",
151+
path: getReadablePath(cline.cwd, relPath),
152+
diff: userEdits,
153+
} satisfies ClineSayTool),
154+
)
155+
pushToolResult(
156+
`The user made the following updates to your content:\n\n${userEdits}\n\n` +
157+
partFailHint +
158+
`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` +
159+
`<final_file_content path="${relPath.toPosix()}">\n${addLineNumbers(
160+
finalContent || "",
161+
)}\n</final_file_content>\n\n` +
162+
`Please note:\n` +
163+
`1. You do not need to re-write the file with these changes, as they have already been applied.\n` +
164+
`2. Proceed with the task using cline updated file content as the new baseline.\n` +
165+
`3. If the user's edits have addressed part of the task or changed the requirements, adjust your approach accordingly.` +
166+
`${newProblemsMessage}`,
167+
)
168+
} else {
169+
pushToolResult(
170+
`Changes successfully applied to ${relPath.toPosix()}:\n\n${newProblemsMessage}\n` + partFailHint,
171+
)
172+
}
173+
await cline.diffViewProvider.reset()
174+
return
175+
}
176+
} catch (error) {
177+
await handleError("applying diff", error)
178+
await cline.diffViewProvider.reset()
179+
return
180+
}
181+
}

0 commit comments

Comments
 (0)