Skip to content

WriteToFileToolHandler: Writing Files and Presenting Diffs

源码版本v4.0.10

Responsibilities

WriteToFileToolHandler is Cline's workhorse tool handler for persisting file changes. It is registered under the name ClineDefaultTool.FILE_NEW, but actually serves three call types: write_to_file (full-file overwrite), replace_in_file (SEARCH/REPLACE block patch), and new_rule (rule-file overwrite) (class declaration:26-27). One handler backs three tool names because all three walk the same pipeline: "validate path → construct newContent → open diff view → approve → save to disk".

Within Cline's tool system it sits as "the concrete handler invoked by ToolExecutor" and implements the IFullyManagedTool interface. Each handler has two entry points: handlePartialBlock drives the UI while the LLM is still streaming content (pushing content into DiffViewProvider as it arrives), and execute does the real write, approval, and checkpoint finalization once the block has fully arrived. All UI interaction goes back through config.callbacks and uiHelpers to the Task; the handler never touches the webview directly.

It also acts as the last line of defense before a file is written: it validates against clineignore, resolves multi-workspace paths, and cleans up the garbage output that models frequently emit (triple-quote fences, unescaped HTML entities, stray escape characters). It tidies the model's output into something that can actually be written to disk, then hands newContent to DiffViewProvider, which renders it in VSCode's diff editor for the user.

Design motivation

  • One handler, three names: write_to_file, replace_in_file, and new_rule share the same save-to-disk pipeline; they only diverge on "how newContent is constructed" (full content vs SEARCH/REPLACE vs rule file). Merging them into one class avoids duplication (validateAndPrepareFileOperation:436).
  • Streaming preview: as the LLM streams content, the handler pushes it into DiffViewProvider so the user can watch the diff evolve before the LLM has finished. This means handlePartialBlock must not throw on incomplete content (handlePartialBlock:35).
  • Fallbacks for model output: weak models frequently add stray ``` fences or leave HTML entities unescaped inside content. Before saving, the handler strips fences and calls applyModelContentFixes to repair HTML entities (markdown strip:564-573).
  • PreToolUse hook runs after approval: the hook runs after the user approves but before the actual save, so a hook rejection can revertChanges and restore the diff view to its original state (PreToolUse hook:344-356).
  • User-edit awareness: if the user manually edits content in the approval window, the handler sends those userEdits back to the LLM and reports telemetry distinguishing "agent accepted" from "human accepted" (user edits:380-411).
  • Mistake counter resets in batches: the counter is not reset on entering execute; it is only zeroed after saveChanges succeeds, so consecutive mistakes can accumulate up to the YOLO-mode cap (reset counter:365-366).

Key files

  • class declaration:26export class WriteToFileToolHandler implements IFullyManagedTool, name = ClineDefaultTool.FILE_NEW; the comment notes it is shared by three tool names.
  • handlePartialBlock:35 — the streaming entry point; once path + content/diff are both present, opens the diff view and calls update(newContent, false) as content streams in.
  • execute:97 — the main flow once the block has fully arrived: validate → construct newContent → approve → save → handle userEdits.
  • missing content error:126-150 — the progressive error for write_to_file with missing content; includes a context-usage hint and switches wording after two consecutive mistakes.
  • auto-approval flow:205-235 — auto-approval goes through say rather than ask, then setTimeoutPromise(3_500) waits for diagnostics to catch up.
  • validateAndPrepareFileOperation:436 — shared validation logic: resolve multi-workspace paths, clineignore check, decide editType, construct newContent.
  • diff construct:490-558 — the replace_in_file branch: first applyModelContentFixes repairs the diff text, then constructNewFileContent applies it to originalContent; on failure, telemetry is bucketed by error type.
  • content branch:559-577 — the write_to_file branch: strips ``` fences and calls applyModelContentFixes to repair model-specific issues.
  • save & track:359-377markFileAsEditedByCline + saveChanges + invalidate fileReadCache + trackFileContext("cline_edited").
  • user edits:380-411 — detects manual edits from the approval window, uses applyPatch(newContent, userEdits) to recover the pre-save content, and reports human-accepted telemetry separately.

Data flow

On entering execute, the handler first calls validateAndPrepareFileOperation to prepare the parameters and newContent, then proceeds to approval and save. The core construction segment is the diff-application path used by replace_in_file:

typescript
// apps/vscode/src/core/task/tools/handlers/WriteToFileToolHandler.ts
if (diff) {
    diff = applyModelContentFixes(diff, config.api.getModel().id, resolvedPath)
    if (!config.services.diffViewProvider.isEditing) {
        await config.services.diffViewProvider.open(absolutePath, { displayPath: relPath })
    }
    try {
        const result = await constructNewFileContent(
            diff,
            config.services.diffViewProvider.originalContent || "",
            !block.partial,
        )
        newContent = result.newContent
        matchIndices = result.matchIndices
    } catch (error) {
        if (block.partial) {
            return
        }
        config.taskState.consecutiveMistakeCount++
        await config.callbacks.removeLastPartialMessageIfExistsWithType("say", "diff_error")
        await config.callbacks.say("diff_error", relPath, undefined, undefined, true)
        // ...
    }
}

constructNewFileContent uses the 7-character fences of SEARCH/REPLACE blocks (------- SEARCH / ======= / +++++++ REPLACE) to locate the SEARCH segment inside originalContent; on failure it falls back to a fuzz strategy (constructNewFileContent:245). Once newContent is in hand, the diff view is opened, update(newContent, true) pushes the final content in and finalizes it, and scrollToFirstDiff scrolls the cursor to the first difference.

After approval, saveChanges actually writes to disk and returns userEdits / autoFormattingEdits / finalContent (saveChanges:337). If the user edited content in the approval window, the handler uses applyPatch(newContent, userEdits) to recover the pre-save content for human-accepted telemetry and sends the userEdits back to the LLM as user_feedback_diff. Once the save is done, the Task runs a single checkpointManager.saveCheckpoint() after all tool execution finishes (post-tool checkpoint:3811), persisting this file change as a checkpoint.

Boundaries and failure modes

  • Diff application failure: when a SEARCH segment cannot be located in the original file, an error is thrown; partial blocks return silently to avoid streaming jitter, and complete blocks emit a diff_error hint and bucket telemetry as search_not_found / other_diff_error (diff error path:509-558).
  • clineignore rejection: when a path matches a clineignore rule, the handler pushes clineIgnoreError and returns without ever opening the diff view (clineignore check:454-474).
  • Empty content: an empty string content for write_to_file is legal (it clears the file). The check uses == null rather than truthiness, but missing content takes the progressive-error path with a context-usage hint (missing content error:126-150).
  • PreToolUse hook cancellation: when the hook throws PreToolUseHookCancellationError, the handler calls revertChanges + reset and returns toolDenied without throwing to the caller (PreToolUse hook:344-356).
  • User rejection: revertChanges restores the diff view to its original state, didRejectTool = true, and subsequent text blocks are skipped at the Task layer (revert on reject:300).
  • Cache invalidation: after saving, fileReadCache.delete(absolutePath.toLowerCase()) runs so the next read_file does not return stale content; after any execute_command runs, the Task clears the cache wholesale (cache invalidate:371).
  • Checkpoint placement: the handler does not save checkpoints itself; after the save, the Task saves one once userMessageContentReady is satisfied, and saves again when the user gives feedback in the approval window (user feedback checkpoint:1542).

Summary

WriteToFileToolHandler unifies "full-file overwrite" and "SEARCH/REPLACE patch" into one class, sharing front-end validation through validateAndPrepareFileOperation and presenting changes through DiffViewProvider in VSCode's native diff editor, with support for user edits to flow back. All model output is funneled through applyModelContentFixes so that fences and unescaped HTML entities from weak models are scrubbed before the save.

To dig deeper, continue with:

  • The multi-file alternative: /edit-tools/apply-patch
  • How the Task schedules it: /agent-loop/task-class
  • The diff view internals: /edit-tools/diff-view-provider

See official docs: Cline 文档 · README