applyDiffTool.ts 7.3 KB

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