appendToFileTool.ts 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175
  1. import * as vscode from "vscode"
  2. import { Cline } from "../Cline"
  3. import { ClineSayTool } from "../../shared/ExtensionMessage"
  4. import { ToolUse } from "../assistant-message"
  5. import { formatResponse } from "../prompts/responses"
  6. import { AskApproval, HandleError, PushToolResult, RemoveClosingTag } from "./types"
  7. import { RecordSource } from "../context-tracking/FileContextTrackerTypes"
  8. import path from "path"
  9. import { fileExistsAtPath } from "../../utils/fs"
  10. import { addLineNumbers, stripLineNumbers } from "../../integrations/misc/extract-text"
  11. import { getReadablePath } from "../../utils/path"
  12. import { isPathOutsideWorkspace } from "../../utils/pathUtils"
  13. import { everyLineHasLineNumbers } from "../../integrations/misc/extract-text"
  14. import delay from "delay"
  15. import { unescapeHtmlEntities } from "../../utils/text-normalization"
  16. export async function appendToFileTool(
  17. cline: Cline,
  18. block: ToolUse,
  19. askApproval: AskApproval,
  20. handleError: HandleError,
  21. pushToolResult: PushToolResult,
  22. removeClosingTag: RemoveClosingTag,
  23. ) {
  24. const relPath: string | undefined = block.params.path
  25. let newContent: string | undefined = block.params.content
  26. if (!relPath || !newContent) {
  27. return
  28. }
  29. const accessAllowed = cline.rooIgnoreController?.validateAccess(relPath)
  30. if (!accessAllowed) {
  31. await cline.say("rooignore_error", relPath)
  32. pushToolResult(formatResponse.toolError(formatResponse.rooIgnoreError(relPath)))
  33. return
  34. }
  35. // Check if file exists using cached map or fs.access
  36. let fileExists: boolean
  37. if (cline.diffViewProvider.editType !== undefined) {
  38. fileExists = cline.diffViewProvider.editType === "modify"
  39. } else {
  40. const absolutePath = path.resolve(cline.cwd, relPath)
  41. fileExists = await fileExistsAtPath(absolutePath)
  42. cline.diffViewProvider.editType = fileExists ? "modify" : "create"
  43. }
  44. // pre-processing newContent for cases where weaker models might add artifacts
  45. if (newContent.startsWith("```")) {
  46. newContent = newContent.split("\n").slice(1).join("\n").trim()
  47. }
  48. if (newContent.endsWith("```")) {
  49. newContent = newContent.split("\n").slice(0, -1).join("\n").trim()
  50. }
  51. if (!cline.api.getModel().id.includes("claude")) {
  52. newContent = unescapeHtmlEntities(newContent)
  53. }
  54. // Determine if the path is outside the workspace
  55. const fullPath = relPath ? path.resolve(cline.cwd, removeClosingTag("path", relPath)) : ""
  56. const isOutsideWorkspace = isPathOutsideWorkspace(fullPath)
  57. const sharedMessageProps: ClineSayTool = {
  58. tool: fileExists ? "appliedDiff" : "newFileCreated",
  59. path: getReadablePath(cline.cwd, removeClosingTag("path", relPath)),
  60. isOutsideWorkspace,
  61. }
  62. try {
  63. if (block.partial) {
  64. // update gui message
  65. const partialMessage = JSON.stringify(sharedMessageProps)
  66. await cline.ask("tool", partialMessage, block.partial).catch(() => {})
  67. // update editor
  68. if (!cline.diffViewProvider.isEditing) {
  69. await cline.diffViewProvider.open(relPath)
  70. }
  71. // If file exists, append newContent to existing content
  72. if (fileExists && cline.diffViewProvider.originalContent) {
  73. newContent = cline.diffViewProvider.originalContent + "\n" + newContent
  74. }
  75. // editor is open, stream content in
  76. await cline.diffViewProvider.update(
  77. everyLineHasLineNumbers(newContent) ? stripLineNumbers(newContent) : newContent,
  78. false,
  79. )
  80. return
  81. } else {
  82. if (!relPath) {
  83. cline.consecutiveMistakeCount++
  84. pushToolResult(await cline.sayAndCreateMissingParamError("append_to_file", "path"))
  85. await cline.diffViewProvider.reset()
  86. return
  87. }
  88. if (!newContent) {
  89. cline.consecutiveMistakeCount++
  90. pushToolResult(await cline.sayAndCreateMissingParamError("append_to_file", "content"))
  91. await cline.diffViewProvider.reset()
  92. return
  93. }
  94. cline.consecutiveMistakeCount = 0
  95. if (!cline.diffViewProvider.isEditing) {
  96. const partialMessage = JSON.stringify(sharedMessageProps)
  97. await cline.ask("tool", partialMessage, true).catch(() => {})
  98. await cline.diffViewProvider.open(relPath)
  99. }
  100. // If file exists, append newContent to existing content
  101. if (fileExists && cline.diffViewProvider.originalContent) {
  102. newContent = cline.diffViewProvider.originalContent + "\n" + newContent
  103. }
  104. await cline.diffViewProvider.update(
  105. everyLineHasLineNumbers(newContent) ? stripLineNumbers(newContent) : newContent,
  106. true,
  107. )
  108. await delay(300) // wait for diff view to update
  109. cline.diffViewProvider.scrollToFirstDiff()
  110. const completeMessage = JSON.stringify({
  111. ...sharedMessageProps,
  112. content: fileExists ? undefined : newContent,
  113. diff: fileExists
  114. ? formatResponse.createPrettyPatch(relPath, cline.diffViewProvider.originalContent, newContent)
  115. : undefined,
  116. } satisfies ClineSayTool)
  117. const didApprove = await askApproval("tool", completeMessage)
  118. if (!didApprove) {
  119. await cline.diffViewProvider.revertChanges()
  120. return
  121. }
  122. const { newProblemsMessage, userEdits, finalContent } = await cline.diffViewProvider.saveChanges()
  123. // Track file edit operation
  124. if (relPath) {
  125. await cline.getFileContextTracker().trackFileContext(relPath, "roo_edited" as RecordSource)
  126. }
  127. cline.didEditFile = true
  128. if (userEdits) {
  129. await cline.say(
  130. "user_feedback_diff",
  131. JSON.stringify({
  132. tool: fileExists ? "appliedDiff" : "newFileCreated",
  133. path: getReadablePath(cline.cwd, relPath),
  134. diff: userEdits,
  135. } satisfies ClineSayTool),
  136. )
  137. pushToolResult(
  138. `The user made the following updates to your content:\n\n${userEdits}\n\n` +
  139. `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` +
  140. `<final_file_content path="${relPath.toPosix()}">\n${addLineNumbers(
  141. finalContent || "",
  142. )}\n</final_file_content>\n\n` +
  143. `Please note:\n` +
  144. `1. You do not need to re-write the file with these changes, as they have already been applied.\n` +
  145. `2. Proceed with the task using this updated file content as the new baseline.\n` +
  146. `3. If the user's edits have addressed part of the task or changed the requirements, adjust your approach accordingly.` +
  147. `${newProblemsMessage}`,
  148. )
  149. } else {
  150. pushToolResult(`The content was successfully appended to ${relPath.toPosix()}.${newProblemsMessage}`)
  151. }
  152. await cline.diffViewProvider.reset()
  153. return
  154. }
  155. } catch (error) {
  156. await handleError("appending to file", error)
  157. await cline.diffViewProvider.reset()
  158. return
  159. }
  160. }