mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-06 08:18:39 +00:00
264 lines
8.5 KiB
TypeScript
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()
|