applyDiffTool.ts 6.9 KB

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