| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318 |
- import path from "path"
- import delay from "delay"
- import * as vscode from "vscode"
- import fs from "fs/promises"
- import { Task } from "../task/Task"
- import { ClineSayTool } from "../../shared/ExtensionMessage"
- import { formatResponse } from "../prompts/responses"
- import { ToolUse, AskApproval, HandleError, PushToolResult, RemoveClosingTag } from "../../shared/tools"
- import { RecordSource } from "../context-tracking/FileContextTrackerTypes"
- import { fileExistsAtPath } from "../../utils/fs"
- import { stripLineNumbers, everyLineHasLineNumbers } from "../../integrations/misc/extract-text"
- import { getReadablePath } from "../../utils/path"
- import { isPathOutsideWorkspace } from "../../utils/pathUtils"
- import { detectCodeOmission } from "../../integrations/editor/detect-omission"
- import { unescapeHtmlEntities } from "../../utils/text-normalization"
- import { DEFAULT_WRITE_DELAY_MS } from "@roo-code/types"
- import { EXPERIMENT_IDS, experiments } from "../../shared/experiments"
- export async function writeToFileTool(
- cline: Task,
- block: ToolUse,
- askApproval: AskApproval,
- handleError: HandleError,
- pushToolResult: PushToolResult,
- removeClosingTag: RemoveClosingTag,
- ) {
- const relPath: string | undefined = block.params.path
- let newContent: string | undefined = block.params.content
- let predictedLineCount: number | undefined = parseInt(block.params.line_count ?? "0")
- if (block.partial && (!relPath || newContent === undefined)) {
- // checking for newContent ensure relPath is complete
- // wait so we can determine if it's a new file or editing an existing file
- return
- }
- if (!relPath) {
- cline.consecutiveMistakeCount++
- cline.recordToolError("write_to_file")
- pushToolResult(await cline.sayAndCreateMissingParamError("write_to_file", "path"))
- await cline.diffViewProvider.reset()
- return
- }
- if (newContent === undefined) {
- cline.consecutiveMistakeCount++
- cline.recordToolError("write_to_file")
- pushToolResult(await cline.sayAndCreateMissingParamError("write_to_file", "content"))
- await cline.diffViewProvider.reset()
- return
- }
- const accessAllowed = cline.rooIgnoreController?.validateAccess(relPath)
- if (!accessAllowed) {
- await cline.say("rooignore_error", relPath)
- pushToolResult(formatResponse.toolError(formatResponse.rooIgnoreError(relPath)))
- return
- }
- // Check if file is write-protected
- const isWriteProtected = cline.rooProtectedController?.isWriteProtected(relPath) || false
- // Check if file exists using cached map or fs.access
- let fileExists: boolean
- if (cline.diffViewProvider.editType !== undefined) {
- fileExists = cline.diffViewProvider.editType === "modify"
- } else {
- const absolutePath = path.resolve(cline.cwd, relPath)
- fileExists = await fileExistsAtPath(absolutePath)
- cline.diffViewProvider.editType = fileExists ? "modify" : "create"
- }
- // pre-processing newContent for cases where weaker models might add artifacts like markdown codeblock markers (deepseek/llama) or extra escape characters (gemini)
- if (newContent.startsWith("```")) {
- // cline handles cases where it includes language specifiers like ```python ```js
- newContent = newContent.split("\n").slice(1).join("\n")
- }
- if (newContent.endsWith("```")) {
- newContent = newContent.split("\n").slice(0, -1).join("\n")
- }
- if (!cline.api.getModel().id.includes("claude")) {
- newContent = unescapeHtmlEntities(newContent)
- }
- // Determine if the path is outside the workspace
- const fullPath = relPath ? path.resolve(cline.cwd, removeClosingTag("path", relPath)) : ""
- const isOutsideWorkspace = isPathOutsideWorkspace(fullPath)
- const sharedMessageProps: ClineSayTool = {
- tool: fileExists ? "editedExistingFile" : "newFileCreated",
- path: getReadablePath(cline.cwd, removeClosingTag("path", relPath)),
- content: newContent,
- isOutsideWorkspace,
- isProtected: isWriteProtected,
- }
- try {
- if (block.partial) {
- // Check if preventFocusDisruption experiment is enabled
- const provider = cline.providerRef.deref()
- const state = await provider?.getState()
- const isPreventFocusDisruptionEnabled = experiments.isEnabled(
- state?.experiments ?? {},
- EXPERIMENT_IDS.PREVENT_FOCUS_DISRUPTION,
- )
- if (!isPreventFocusDisruptionEnabled) {
- // update gui message
- const partialMessage = JSON.stringify(sharedMessageProps)
- await cline.ask("tool", partialMessage, block.partial).catch(() => {})
- // update editor
- if (!cline.diffViewProvider.isEditing) {
- // open the editor and prepare to stream content in
- await cline.diffViewProvider.open(relPath)
- }
- // editor is open, stream content in
- await cline.diffViewProvider.update(
- everyLineHasLineNumbers(newContent) ? stripLineNumbers(newContent) : newContent,
- false,
- )
- }
- return
- } else {
- if (predictedLineCount === undefined) {
- cline.consecutiveMistakeCount++
- cline.recordToolError("write_to_file")
- // Calculate the actual number of lines in the content
- const actualLineCount = newContent.split("\n").length
- // Check if this is a new file or existing file
- const isNewFile = !fileExists
- // Check if diffStrategy is enabled
- const diffStrategyEnabled = !!cline.diffStrategy
- // Use more specific error message for line_count that provides guidance based on the situation
- await cline.say(
- "error",
- `Roo tried to use write_to_file${
- relPath ? ` for '${relPath.toPosix()}'` : ""
- } but the required parameter 'line_count' was missing or truncated after ${actualLineCount} lines of content were written. Retrying...`,
- )
- pushToolResult(
- formatResponse.toolError(
- formatResponse.lineCountTruncationError(actualLineCount, isNewFile, diffStrategyEnabled),
- ),
- )
- await cline.diffViewProvider.revertChanges()
- return
- }
- cline.consecutiveMistakeCount = 0
- // Check if preventFocusDisruption experiment is enabled
- const provider = cline.providerRef.deref()
- const state = await provider?.getState()
- const diagnosticsEnabled = state?.diagnosticsEnabled ?? true
- const writeDelayMs = state?.writeDelayMs ?? DEFAULT_WRITE_DELAY_MS
- const isPreventFocusDisruptionEnabled = experiments.isEnabled(
- state?.experiments ?? {},
- EXPERIMENT_IDS.PREVENT_FOCUS_DISRUPTION,
- )
- if (isPreventFocusDisruptionEnabled) {
- // Direct file write without diff view
- // Check for code omissions before proceeding
- if (detectCodeOmission(cline.diffViewProvider.originalContent || "", newContent, predictedLineCount)) {
- if (cline.diffStrategy) {
- pushToolResult(
- formatResponse.toolError(
- `Content appears to be truncated (file has ${
- newContent.split("\n").length
- } lines but was predicted to have ${predictedLineCount} lines), and found comments indicating omitted code (e.g., '// rest of code unchanged', '/* previous code */'). Please provide the complete file content without any omissions if possible, or otherwise use the 'apply_diff' tool to apply the diff to the original file.`,
- ),
- )
- return
- } else {
- vscode.window
- .showWarningMessage(
- "Potential code truncation detected. cline happens when the AI reaches its max output limit.",
- "Follow cline guide to fix the issue",
- )
- .then((selection) => {
- if (selection === "Follow cline guide to fix the issue") {
- vscode.env.openExternal(
- vscode.Uri.parse(
- "https://github.com/cline/cline/wiki/Troubleshooting-%E2%80%90-Cline-Deleting-Code-with-%22Rest-of-Code-Here%22-Comments",
- ),
- )
- }
- })
- }
- }
- const completeMessage = JSON.stringify({
- ...sharedMessageProps,
- content: newContent,
- } satisfies ClineSayTool)
- const didApprove = await askApproval("tool", completeMessage, undefined, isWriteProtected)
- if (!didApprove) {
- return
- }
- // Set up diffViewProvider properties needed for saveDirectly
- cline.diffViewProvider.editType = fileExists ? "modify" : "create"
- if (fileExists) {
- const absolutePath = path.resolve(cline.cwd, relPath)
- cline.diffViewProvider.originalContent = await fs.readFile(absolutePath, "utf-8")
- } else {
- cline.diffViewProvider.originalContent = ""
- }
- // Save directly without showing diff view or opening the file
- await cline.diffViewProvider.saveDirectly(relPath, newContent, false, diagnosticsEnabled, writeDelayMs)
- } else {
- // Original behavior with diff view
- // if isEditingFile false, that means we have the full contents of the file already.
- // it's important to note how cline function works, you can't make the assumption that the block.partial conditional will always be called since it may immediately get complete, non-partial data. So cline part of the logic will always be called.
- // in other words, you must always repeat the block.partial logic here
- if (!cline.diffViewProvider.isEditing) {
- // show gui message before showing edit animation
- const partialMessage = JSON.stringify(sharedMessageProps)
- await cline.ask("tool", partialMessage, true).catch(() => {}) // sending true for partial even though it's not a partial, cline shows the edit row before the content is streamed into the editor
- await cline.diffViewProvider.open(relPath)
- }
- await cline.diffViewProvider.update(
- everyLineHasLineNumbers(newContent) ? stripLineNumbers(newContent) : newContent,
- true,
- )
- await delay(300) // wait for diff view to update
- cline.diffViewProvider.scrollToFirstDiff()
- // Check for code omissions before proceeding
- if (detectCodeOmission(cline.diffViewProvider.originalContent || "", newContent, predictedLineCount)) {
- if (cline.diffStrategy) {
- await cline.diffViewProvider.revertChanges()
- pushToolResult(
- formatResponse.toolError(
- `Content appears to be truncated (file has ${
- newContent.split("\n").length
- } lines but was predicted to have ${predictedLineCount} lines), and found comments indicating omitted code (e.g., '// rest of code unchanged', '/* previous code */'). Please provide the complete file content without any omissions if possible, or otherwise use the 'apply_diff' tool to apply the diff to the original file.`,
- ),
- )
- return
- } else {
- vscode.window
- .showWarningMessage(
- "Potential code truncation detected. cline happens when the AI reaches its max output limit.",
- "Follow cline guide to fix the issue",
- )
- .then((selection) => {
- if (selection === "Follow cline guide to fix the issue") {
- vscode.env.openExternal(
- vscode.Uri.parse(
- "https://github.com/cline/cline/wiki/Troubleshooting-%E2%80%90-Cline-Deleting-Code-with-%22Rest-of-Code-Here%22-Comments",
- ),
- )
- }
- })
- }
- }
- const completeMessage = JSON.stringify({
- ...sharedMessageProps,
- content: fileExists ? undefined : newContent,
- diff: fileExists
- ? formatResponse.createPrettyPatch(relPath, cline.diffViewProvider.originalContent, newContent)
- : undefined,
- } satisfies ClineSayTool)
- const didApprove = await askApproval("tool", completeMessage, undefined, isWriteProtected)
- if (!didApprove) {
- await cline.diffViewProvider.revertChanges()
- return
- }
- // Call saveChanges to update the DiffViewProvider properties
- await cline.diffViewProvider.saveChanges(diagnosticsEnabled, writeDelayMs)
- }
- // Track file edit operation
- if (relPath) {
- await cline.fileContextTracker.trackFileContext(relPath, "roo_edited" as RecordSource)
- }
- cline.didEditFile = true // used to determine if we should wait for busy terminal to update before sending api request
- // Get the formatted response message
- const message = await cline.diffViewProvider.pushToolWriteResult(cline, cline.cwd, !fileExists)
- pushToolResult(message)
- await cline.diffViewProvider.reset()
- return
- }
- } catch (error) {
- await handleError("writing file", error)
- await cline.diffViewProvider.reset()
- return
- }
- }
|