Roo-Code/src/core/tools/WriteToFileTool.ts
roomote[bot] 86edc01cb1
fix: remove omission detection logic to fix false positives (#9787)
Co-authored-by: Roo Code <roomote@roocode.com>
2025-12-03 13:20:22 -05:00

264 lines
8.5 KiB
TypeScript

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 { RecordSource } from "../context-tracking/FileContextTrackerTypes"
import { fileExistsAtPath, createDirectoriesForFile } from "../../utils/fs"
import { stripLineNumbers, everyLineHasLineNumbers } from "../../integrations/misc/extract-text"
import { getReadablePath } from "../../utils/path"
import { isPathOutsideWorkspace } from "../../utils/pathUtils"
import { unescapeHtmlEntities } from "../../utils/text-normalization"
import { DEFAULT_WRITE_DELAY_MS } from "@roo-code/types"
import { EXPERIMENT_IDS, experiments } from "../../shared/experiments"
import { convertNewFileToUnifiedDiff, computeDiffStats, sanitizeUnifiedDiff } from "../diff/stats"
import { BaseTool, ToolCallbacks } from "./BaseTool"
import type { ToolUse } from "../../shared/tools"
interface WriteToFileParams {
path: string
content: string
}
export class WriteToFileTool extends BaseTool<"write_to_file"> {
readonly name = "write_to_file" as const
parseLegacy(params: Partial<Record<string, string>>): WriteToFileParams {
return {
path: params.path || "",
content: params.content || "",
}
}
async execute(params: WriteToFileParams, task: Task, callbacks: ToolCallbacks): Promise<void> {
const { pushToolResult, handleError, askApproval, removeClosingTag } = callbacks
const relPath = params.path
let newContent = params.content
if (!relPath) {
task.consecutiveMistakeCount++
task.recordToolError("write_to_file")
pushToolResult(await task.sayAndCreateMissingParamError("write_to_file", "path"))
await task.diffViewProvider.reset()
return
}
if (newContent === undefined) {
task.consecutiveMistakeCount++
task.recordToolError("write_to_file")
pushToolResult(await task.sayAndCreateMissingParamError("write_to_file", "content"))
await task.diffViewProvider.reset()
return
}
const accessAllowed = task.rooIgnoreController?.validateAccess(relPath)
if (!accessAllowed) {
await task.say("rooignore_error", relPath)
pushToolResult(formatResponse.rooIgnoreError(relPath))
return
}
const isWriteProtected = task.rooProtectedController?.isWriteProtected(relPath) || false
let fileExists: boolean
const absolutePath = path.resolve(task.cwd, relPath)
if (task.diffViewProvider.editType !== undefined) {
fileExists = task.diffViewProvider.editType === "modify"
} else {
fileExists = await fileExistsAtPath(absolutePath)
task.diffViewProvider.editType = fileExists ? "modify" : "create"
}
// Create parent directories early for new files to prevent ENOENT errors
// in subsequent operations (e.g., diffViewProvider.open, fs.readFile)
if (!fileExists) {
await createDirectoriesForFile(absolutePath)
}
if (newContent.startsWith("```")) {
newContent = newContent.split("\n").slice(1).join("\n")
}
if (newContent.endsWith("```")) {
newContent = newContent.split("\n").slice(0, -1).join("\n")
}
if (!task.api.getModel().id.includes("claude")) {
newContent = unescapeHtmlEntities(newContent)
}
const fullPath = relPath ? path.resolve(task.cwd, removeClosingTag("path", relPath)) : ""
const isOutsideWorkspace = isPathOutsideWorkspace(fullPath)
const sharedMessageProps: ClineSayTool = {
tool: fileExists ? "editedExistingFile" : "newFileCreated",
path: getReadablePath(task.cwd, removeClosingTag("path", relPath)),
content: newContent,
isOutsideWorkspace,
isProtected: isWriteProtected,
}
try {
task.consecutiveMistakeCount = 0
const provider = task.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) {
task.diffViewProvider.editType = fileExists ? "modify" : "create"
if (fileExists) {
const absolutePath = path.resolve(task.cwd, relPath)
task.diffViewProvider.originalContent = await fs.readFile(absolutePath, "utf-8")
} else {
task.diffViewProvider.originalContent = ""
}
let unified = fileExists
? formatResponse.createPrettyPatch(relPath, task.diffViewProvider.originalContent, newContent)
: convertNewFileToUnifiedDiff(newContent, relPath)
unified = sanitizeUnifiedDiff(unified)
const completeMessage = JSON.stringify({
...sharedMessageProps,
content: unified,
diffStats: computeDiffStats(unified) || undefined,
} satisfies ClineSayTool)
const didApprove = await askApproval("tool", completeMessage, undefined, isWriteProtected)
if (!didApprove) {
return
}
await task.diffViewProvider.saveDirectly(relPath, newContent, false, diagnosticsEnabled, writeDelayMs)
} else {
if (!task.diffViewProvider.isEditing) {
const partialMessage = JSON.stringify(sharedMessageProps)
await task.ask("tool", partialMessage, true).catch(() => {})
await task.diffViewProvider.open(relPath)
}
await task.diffViewProvider.update(
everyLineHasLineNumbers(newContent) ? stripLineNumbers(newContent) : newContent,
true,
)
await delay(300)
task.diffViewProvider.scrollToFirstDiff()
let unified = fileExists
? formatResponse.createPrettyPatch(relPath, task.diffViewProvider.originalContent, newContent)
: convertNewFileToUnifiedDiff(newContent, relPath)
unified = sanitizeUnifiedDiff(unified)
const completeMessage = JSON.stringify({
...sharedMessageProps,
content: unified,
diffStats: computeDiffStats(unified) || undefined,
} satisfies ClineSayTool)
const didApprove = await askApproval("tool", completeMessage, undefined, isWriteProtected)
if (!didApprove) {
await task.diffViewProvider.revertChanges()
return
}
await task.diffViewProvider.saveChanges(diagnosticsEnabled, writeDelayMs)
}
if (relPath) {
await task.fileContextTracker.trackFileContext(relPath, "roo_edited" as RecordSource)
}
task.didEditFile = true
const message = await task.diffViewProvider.pushToolWriteResult(task, task.cwd, !fileExists)
pushToolResult(message)
await task.diffViewProvider.reset()
task.processQueuedMessages()
return
} catch (error) {
await handleError("writing file", error as Error)
await task.diffViewProvider.reset()
return
}
}
override async handlePartial(task: Task, block: ToolUse<"write_to_file">): Promise<void> {
const relPath: string | undefined = block.params.path
let newContent: string | undefined = block.params.content
if (!relPath || newContent === undefined) {
return
}
const provider = task.providerRef.deref()
const state = await provider?.getState()
const isPreventFocusDisruptionEnabled = experiments.isEnabled(
state?.experiments ?? {},
EXPERIMENT_IDS.PREVENT_FOCUS_DISRUPTION,
)
if (isPreventFocusDisruptionEnabled) {
return
}
let fileExists: boolean
const absolutePath = path.resolve(task.cwd, relPath)
if (task.diffViewProvider.editType !== undefined) {
fileExists = task.diffViewProvider.editType === "modify"
} else {
fileExists = await fileExistsAtPath(absolutePath)
task.diffViewProvider.editType = fileExists ? "modify" : "create"
}
// Create parent directories early for new files to prevent ENOENT errors
// in subsequent operations (e.g., diffViewProvider.open)
if (!fileExists) {
await createDirectoriesForFile(absolutePath)
}
const isWriteProtected = task.rooProtectedController?.isWriteProtected(relPath) || false
const fullPath = absolutePath
const isOutsideWorkspace = isPathOutsideWorkspace(fullPath)
const sharedMessageProps: ClineSayTool = {
tool: fileExists ? "editedExistingFile" : "newFileCreated",
path: getReadablePath(task.cwd, relPath),
content: newContent || "",
isOutsideWorkspace,
isProtected: isWriteProtected,
}
const partialMessage = JSON.stringify(sharedMessageProps)
await task.ask("tool", partialMessage, block.partial).catch(() => {})
if (newContent) {
if (!task.diffViewProvider.isEditing) {
await task.diffViewProvider.open(relPath)
}
await task.diffViewProvider.update(
everyLineHasLineNumbers(newContent) ? stripLineNumbers(newContent) : newContent,
false,
)
}
}
}
export const writeToFileTool = new WriteToFileTool()