WriteToFileToolHandler:ファイル書き込みと diff 表示
役割
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.callbacks と uiHelpers を経由して 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:26—export class WriteToFileToolHandler implements IFullyManagedTool、name = 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-377—markFileAsEditedByCline+saveChanges+ fileReadCache 無効化 +trackFileContext("cline_edited")。user edits:380-411— 承認ウィンドウでのユーザー手修正を検出し、applyPatch(newContent, userEdits)で保存前の内容を復元して human accepted テレメトリを個別に報告する。
データフロー
execute に入った後、まず validateAndPrepareFileOperation を呼んでパラメータと newContent を整え、その後に承認とディスク書き込みに進む。核となる構築区間は replace_in_file が通る diff 適用パス:
// 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