Skip to content

WriteToFileToolHandler:ファイル書き込みと diff 表示

源码版本v4.0.10

役割

WriteToFileToolHandler は Cline がファイル変更をディスクに落とすための主力ツールハンドラである。ClineDefaultTool.FILE_NEW という名前で登録されるが、実際には三種類の呼び出しを受け持つ: write_to_file (ファイル全体の上書き)、replace_in_file (SEARCH/REPLACE ブロックによるパッチ)、new_rule (ルールファイルの上書き) (class declaration:26-27)。一つのハンドラで三つのツール名を再利用するのは、いずれも「パス検証 → newContent 構築 → diff ビューを開く → 承認 → ディスクに落とす」という同じパイプラインを走るためである。

Cline のツール体系の中での位置は「ToolExecutor から起動される具象 handler」で、IFullyManagedTool インターフェースを実装する。各 handler には二つの入口がある: handlePartialBlock は LLM がストリーミングで内容を吐いている間に UI を駆動し (内容を吐きながら DiffViewProvider に反映)、execute は block が揃った後に本当にディスクへ書き、承認、チェックポイントの収尾を行う。すべての UI 操作は config.callbacksuiHelpers を経由して Task に戻り、handler 自身は直接 webview を触らない。

さらに、ファイル書き込み前の最後の防線も兼ねる: clineignore の検証、複数ワークスペースのパス解析、モデルがよく出す文字化けのクリーンアップ (三重引用符フェンス、未エスケープの HTML 実体、過剰なエスケープ文字) を行い、不規則なモデル出力をディスクに書ける形に整えた上で、newContent を DiffViewProvider に渡して VSCode の diff エディタでユーザーに提示する。

設計動機

  • 一つの handler で三つの名前: write_to_file、replace_in_file、new_rule はディスクへの書き込みパスが本質的に同じで、「newContent をどう構築するか」だけで分岐する (全体内容 vs SEARCH/REPLACE vs ルールファイル)。一つのクラスにまとめることで重複を避ける (validateAndPrepareFileOperation:436)。
  • ストリーミングプレビュー: LLM が content を吐いている間、handler はそれを DiffViewProvider に逐次反映し、ユーザーは LLM の発話が終わる前に diff が変わるのを見られる。これには handlePartialBlock が内容不全の段階でエラーを投げないことが求められる (handlePartialBlock:35)。
  • モデル出力の安全網: 弱いモデルは content に余分な ``` フェンスを入れたり HTML 実体を未エスケープのままにしたりする。handler はディスク書き込み前にフェンスを剥がし、applyModelContentFixes で HTML 実体を修復する (markdown strip:564-573)。
  • PreToolUse hook を承認後に配置: hook はユーザー承認が通った後、本当にディスクに落ちる前に走る。これにより hook が拒否した時に revertChanges で diff ビューを元に戻せる (PreToolUse hook:344-356)。
  • ユーザー編集の感知: ユーザーが承認ウィンドウで内容を手動で書き換えた場合、handler はその userEdits を取り出して個別に LLM に送り返し、テレメトリでも agent accepted と human accepted の二つの来源を区別する (user edits:380-411)。
  • mistake カウンタは段階的にリセット: execute 入口ですぐリセットするのではなく、saveChanges が成功した後でのみ 0 に戻す。連続エラーが YOLO モードの上限まで累積できるようにする (reset counter:365-366)。

主要ファイル

  • class declaration:26export class WriteToFileToolHandler implements IFullyManagedToolname = ClineDefaultTool.FILE_NEW。コメントで三つのツール名を再利用することが説明されている。
  • handlePartialBlock:35 — ストリーミング入口。path + content/diff が揃った時点で diff ビューを開き、吐きながら update(newContent, false) する。
  • execute:97 — block が揃った後の主フロー: 検証 → newContent 構築 → 承認 → ディスク書き込み → userEdits 処理。
  • missing content error:126-150 — write_to_file の content 欠落時の段階的エラー。コンテキスト使用率のヒントを付け、2 回連続エラー後にメッセージを切り替える。
  • auto-approval flow:205-235 — 自動承認は ask ではなく say で進め、その後 setTimeoutPromise(3_500) で diagnostics が追いつくのを待つ。
  • validateAndPrepareFileOperation:436 — 共有検証ロジック: 複数ワークスペースのパス解析、clineignore チェック、editType 決定、newContent 構築。
  • diff construct:490-558 — replace_in_file 分岐: 先に applyModelContentFixes で diff テキストを修復し、constructNewFileContent で originalContent に適用する。失敗時はエラー種別でテレメトリを振り分ける。
  • content branch:559-577 — write_to_file 分岐: ``` フェンスを剥がし、applyModelContentFixes でモデル固有の問題を修復する。
  • save & track:359-377markFileAsEditedByCline + saveChanges + fileReadCache 無効化 + trackFileContext("cline_edited")
  • user edits:380-411 — 承認ウィンドウでのユーザー手修正を検出し、applyPatch(newContent, userEdits) で保存前の内容を復元して human accepted テレメトリを個別に報告する。

データフロー

execute に入った後、まず validateAndPrepareFileOperation を呼んでパラメータと newContent を整え、その後に承認とディスク書き込みに進む。核となる構築区間は replace_in_file が通る diff 適用パス:

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 は SEARCH/REPLACE ブロックの 7 文字フェンス (------- SEARCH / ======= / +++++++ REPLACE) で SEARCH セグメントを originalContent 中に位置決めし、失敗時は fuzz ポリシーでフォールバックする (constructNewFileContent:245)。newContent を受け取ったら diff ビューを開き、update(newContent, true) で最終内容を反映させて finalize し、scrollToFirstDiff で最初の差異にカーソルを滚らせる。

承認が通ると、saveChanges が実際にディスクに書き込み、userEdits / autoFormattingEdits / finalContent を返す (saveChanges:337)。ユーザーが承認ウィンドウで内容を書き換えていた場合、handler は applyPatch(newContent, userEdits) で保存前の内容を復元して human accepted テレメトリを報告し、userEdits を user_feedback_diff として LLM に送り返す。ディスク書き込み完了後、Task は全ツールの実行終了後に一度だけ checkpointManager.saveCheckpoint() を統一して走らせ (post-tool checkpoint:3811)、今回のファイル変更を checkpoint として固化する。

境界と失敗

  • diff 適用失敗: SEARCH セグメントが元ファイルに見つからない時にエラーを投げる。partial block では黙って return してストリーミングのジッタを避け、complete block では diff_error でヒントを出し、search_not_found / other_diff_error でバケット分けしてテレメトリを送る (diff error path:509-558)。
  • clineignore 拒否: パスが clineignore ルールにヒットした場合は直接 pushToolResult で clineIgnoreError を返し、diff ビューは通さない (clineignore check:454-474)。
  • 空 content: write_to_file が空文字列の content を受け取るのは合法 (ファイルを空にする正当な操作)。== null で判定し truthy 判定はしない。ただし content 欠落は段階的エラーで、コンテキスト使用率のヒントを添えてリトライを促す (missing content error:126-150)。
  • PreToolUse hook のキャンセル: hook が PreToolUseHookCancellationError を投げた場合は revertChanges + reset して toolDenied を返し、エラーは上層に伝播させない (PreToolUse hook:344-356)。
  • ユーザー拒否: revertChanges で diff ビューを元の状態に戻し、didRejectTool = true とする。後続の text block は Task 層で skip される (revert on reject:300)。
  • キャッシュ無効化: ディスク書き込み後に fileReadCache.delete(absolutePath.toLowerCase()) し、次回 read_file が旧内容を読まないようにする。execute_command が実行された後は Task がキャッシュを全体クリアする (cache invalidate:371)。
  • checkpoint の落ちどころ: handler 自身は checkpoint を保存せず、ディスク書き込み後に Task が userMessageContentReady を満たしたタイミングで統一保存する。ユーザーが承認ウィンドウでフィードバックを返した時にもう一度保存する (user feedback checkpoint:1542)。

まとめ

WriteToFileToolHandler は「ファイル全体上書き」と「SEARCH/REPLACE パッチ」を一つのクラスにまとめ、validateAndPrepareFileOperation で事前検証を共有する。DiffViewProvider を通じて VSCode ネイティブの diff エディタで変更を表示し、ユーザー手修正の還流もサポートする。モデル出力はすべて applyModelContentFixes で安全網を通し、弱いモデルが余分に付けたフェンスや未エスケープの HTML 実体はディスク書き込み前に取り除かれる。

さらに掘り下げたい場合は:

  • 代替となる複数ファイルパッチ: /edit-tools/apply-patch
  • Task がどうスケジュールするか: /agent-loop/task-class
  • diff ビューの下層: /edit-tools/diff-view-provider

公式資料: Cline 文档 · README