insertContentTool.ts 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194
  1. import delay from "delay"
  2. import fs from "fs/promises"
  3. import path from "path"
  4. import { getReadablePath } from "../../utils/path"
  5. import { Task } from "../task/Task"
  6. import { ToolUse, AskApproval, HandleError, PushToolResult, RemoveClosingTag } from "../../shared/tools"
  7. import { formatResponse } from "../prompts/responses"
  8. import { ClineSayTool } from "../../shared/ExtensionMessage"
  9. import { RecordSource } from "../context-tracking/FileContextTrackerTypes"
  10. import { fileExistsAtPath } from "../../utils/fs"
  11. import { insertGroups } from "../diff/insert-groups"
  12. import { DEFAULT_WRITE_DELAY_MS } from "@roo-code/types"
  13. import { EXPERIMENT_IDS, experiments } from "../../shared/experiments"
  14. export async function insertContentTool(
  15. cline: Task,
  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. const line: string | undefined = block.params.line
  24. const content: string | undefined = block.params.content
  25. const sharedMessageProps: ClineSayTool = {
  26. tool: "insertContent",
  27. path: getReadablePath(cline.cwd, removeClosingTag("path", relPath)),
  28. diff: content,
  29. lineNumber: line ? parseInt(line, 10) : undefined,
  30. }
  31. try {
  32. if (block.partial) {
  33. await cline.ask("tool", JSON.stringify(sharedMessageProps), block.partial).catch(() => {})
  34. return
  35. }
  36. // Validate required parameters
  37. if (!relPath) {
  38. cline.consecutiveMistakeCount++
  39. cline.recordToolError("insert_content")
  40. pushToolResult(await cline.sayAndCreateMissingParamError("insert_content", "path"))
  41. return
  42. }
  43. if (!line) {
  44. cline.consecutiveMistakeCount++
  45. cline.recordToolError("insert_content")
  46. pushToolResult(await cline.sayAndCreateMissingParamError("insert_content", "line"))
  47. return
  48. }
  49. if (content === undefined) {
  50. cline.consecutiveMistakeCount++
  51. cline.recordToolError("insert_content")
  52. pushToolResult(await cline.sayAndCreateMissingParamError("insert_content", "content"))
  53. return
  54. }
  55. const accessAllowed = cline.rooIgnoreController?.validateAccess(relPath)
  56. if (!accessAllowed) {
  57. await cline.say("rooignore_error", relPath)
  58. pushToolResult(formatResponse.toolError(formatResponse.rooIgnoreError(relPath)))
  59. return
  60. }
  61. // Check if file is write-protected
  62. const isWriteProtected = cline.rooProtectedController?.isWriteProtected(relPath) || false
  63. const absolutePath = path.resolve(cline.cwd, relPath)
  64. const lineNumber = parseInt(line, 10)
  65. if (isNaN(lineNumber) || lineNumber < 0) {
  66. cline.consecutiveMistakeCount++
  67. cline.recordToolError("insert_content")
  68. pushToolResult(formatResponse.toolError("Invalid line number. Must be a non-negative integer."))
  69. return
  70. }
  71. const fileExists = await fileExistsAtPath(absolutePath)
  72. let fileContent: string = ""
  73. if (!fileExists) {
  74. if (lineNumber > 1) {
  75. cline.consecutiveMistakeCount++
  76. cline.recordToolError("insert_content")
  77. const formattedError = `Cannot insert content at line ${lineNumber} into a non-existent file. For new files, 'line' must be 0 (to append) or 1 (to insert at the beginning).`
  78. await cline.say("error", formattedError)
  79. pushToolResult(formattedError)
  80. return
  81. }
  82. } else {
  83. fileContent = await fs.readFile(absolutePath, "utf8")
  84. }
  85. cline.consecutiveMistakeCount = 0
  86. cline.diffViewProvider.editType = fileExists ? "modify" : "create"
  87. cline.diffViewProvider.originalContent = fileContent
  88. const lines = fileExists ? fileContent.split("\n") : []
  89. const updatedContent = insertGroups(lines, [
  90. {
  91. index: lineNumber - 1,
  92. elements: content.split("\n"),
  93. },
  94. ]).join("\n")
  95. // Check if preventFocusDisruption experiment is enabled
  96. const provider = cline.providerRef.deref()
  97. const state = await provider?.getState()
  98. const diagnosticsEnabled = state?.diagnosticsEnabled ?? true
  99. const writeDelayMs = state?.writeDelayMs ?? DEFAULT_WRITE_DELAY_MS
  100. const isPreventFocusDisruptionEnabled = experiments.isEnabled(
  101. state?.experiments ?? {},
  102. EXPERIMENT_IDS.PREVENT_FOCUS_DISRUPTION,
  103. )
  104. // For consistency with writeToFileTool, handle new files differently
  105. let diff: string | undefined
  106. let approvalContent: string | undefined
  107. if (fileExists) {
  108. // For existing files, generate diff and check for changes
  109. diff = formatResponse.createPrettyPatch(relPath, fileContent, updatedContent)
  110. if (!diff) {
  111. pushToolResult(`No changes needed for '${relPath}'`)
  112. return
  113. }
  114. approvalContent = undefined
  115. } else {
  116. // For new files, skip diff generation and provide full content
  117. diff = undefined
  118. approvalContent = updatedContent
  119. }
  120. // Prepare the approval message (same for both flows)
  121. const completeMessage = JSON.stringify({
  122. ...sharedMessageProps,
  123. diff,
  124. content: approvalContent,
  125. lineNumber: lineNumber,
  126. isProtected: isWriteProtected,
  127. } satisfies ClineSayTool)
  128. // Show diff view if focus disruption prevention is disabled
  129. if (!isPreventFocusDisruptionEnabled) {
  130. await cline.diffViewProvider.open(relPath)
  131. await cline.diffViewProvider.update(updatedContent, true)
  132. cline.diffViewProvider.scrollToFirstDiff()
  133. }
  134. // Ask for approval (same for both flows)
  135. const didApprove = await askApproval("tool", completeMessage, undefined, isWriteProtected)
  136. if (!didApprove) {
  137. // Revert changes if diff view was shown
  138. if (!isPreventFocusDisruptionEnabled) {
  139. await cline.diffViewProvider.revertChanges()
  140. }
  141. pushToolResult("Changes were rejected by the user.")
  142. await cline.diffViewProvider.reset()
  143. return
  144. }
  145. // Save the changes
  146. if (isPreventFocusDisruptionEnabled) {
  147. // Direct file write without diff view or opening the file
  148. await cline.diffViewProvider.saveDirectly(relPath, updatedContent, false, diagnosticsEnabled, writeDelayMs)
  149. } else {
  150. // Call saveChanges to update the DiffViewProvider properties
  151. await cline.diffViewProvider.saveChanges(diagnosticsEnabled, writeDelayMs)
  152. }
  153. // Track file edit operation
  154. if (relPath) {
  155. await cline.fileContextTracker.trackFileContext(relPath, "roo_edited" as RecordSource)
  156. }
  157. cline.didEditFile = true
  158. // Get the formatted response message
  159. const message = await cline.diffViewProvider.pushToolWriteResult(cline, cline.cwd, !fileExists)
  160. pushToolResult(message)
  161. await cline.diffViewProvider.reset()
  162. } catch (error) {
  163. handleError("insert content", error)
  164. await cline.diffViewProvider.reset()
  165. }
  166. }