WriteToFileToolHandler: Datei-Schreiben und Diff-Präsentation
Verantwortung
WriteToFileToolHandler ist der zentrale Werkzeug-Handler von Cline zum Persistieren von Dateiänderungen. Er ist unter dem Namen ClineDefaultTool.FILE_NEW registriert, trägt aber tatsächlich drei Aufrufarten: write_to_file (gesamte Datei überschreiben), replace_in_file (SEARCH/REPLACE-Block-Patch) und new_rule (Regeldatei überschreiben) (class declaration:26-27). Ein Handler, drei Werkzeugnamen, weil alle denselben Pfad „Pfad validieren → newContent konstruieren → Diff-Ansicht öffnen → Approval → Speichern" durchlaufen.
In der Cline-Werkzeug-Architektur ist er der „vom ToolExecutor aufgerufene konkrete Handler" und implementiert das Interface IFullyManagedTool. Jeder Handler hat zwei Eingänge: handlePartialBlock treibt die UI an, während das LLM noch streamt (Inhalt stückweise in den DiffViewProvider schieben); execute übernimmt nach vollständigem Block das echte Schreiben, Approval und die Checkpoint-Aufräumarbeiten. Jegliche UI-Interaktion läuft über config.callbacks und uiHelpers zurück zur Task, der Handler selbst berührt die Webview nicht direkt.
Er ist außerdem die letzte Verteidigungslinie vor dem Schreiben: validiert clineignore, löst Multi-Workspace-Pfade auf, räumt häufige Modell-Müll-Ausgaben auf (drei-fache Backtick-Umrandung, nicht-escapte HTML-Entities, überflüssige Escape-Zeichen) und normalisiert die unregelmäßige Modellausgabe zu speicherfähigem Inhalt, bevor newContent an den DiffViewProvider übergeben wird, der ihn im VSCode-Diff-Editor dem Nutzer präsentiert.
Entwurfsmotivation
- Ein Handler, drei Namen: write_to_file, replace_in_file und new_rule haben denselben Speicherpfad; sie verzweigen nur bei „wie newContent konstruiert wird" (ganzer Inhalt vs. SEARCH/REPLACE vs. Regeldatei) und sind in einer Klasse zusammengefasst, um Duplikation zu vermeiden (
validateAndPrepareFileOperation:436). - Streaming-Vorschau: Während das LLM content streamt, schiebt der Handler es stückweise in den DiffViewProvider; der Nutzer sieht den Diff sich verändern, bevor das LLM fertig gesprochen hat. Daher darf
handlePartialBlockbei unvollständigem Inhalt keinen Fehler werfen (handlePartialBlock:35). - Fallback für Modellausgaben: Schwache Modelle geben content oft mit zusätzlichen ```-Umkreis oder nicht-escapten HTML-Entities; der Handler entfernt vor dem Speichern die Umrandung und ruft
applyModelContentFixeszur Reparatur der HTML-Entities auf (markdown strip:564-573). - PreToolUse hook nach Approval: Der Hook läuft nach der Nutzer-Freigabe und vor dem echten Speichern, damit er bei Ablehnung revertChanges aufrufen und die Diff-Ansicht wiederherstellen kann (
PreToolUse hook:344-356). - Wahrnehmung von Nutzer-Editierungen: Wenn der Nutzer im Approval-Fenster manuell ändert, sendet der Handler diese userEdits separat an die LLM zurück und unterscheidet in der Telemetrie zwischen agent accepted und human accepted (
user edits:380-411). - mistake-Zähler verzögert zurücksetzen: Er wird nicht bei Eintritt in execute zurückgesetzt, sondern erst nach erfolgreichem
saveChangesauf 0 gesetzt, damit sich aufeinanderfolgende Fehler bis zum YOLO-Limit akkumulieren können (reset counter:365-366).
Schlüsseldateien
class declaration:26—export class WriteToFileToolHandler implements IFullyManagedTool,name = ClineDefaultTool.FILE_NEW; der Kommentar erläutert die Wiederverwendung von drei Werkzeugnamen.handlePartialBlock:35— Streaming-Eingang; öffnet die Diff-Ansicht, sobald path + content/diff vollständig sind, und ruft während des Streamsupdate(newContent, false)auf.execute:97— Hauptfluss nach vollständigem Block: Validierung → newContent konstruieren → Approval → Speichern → userEdits behandeln.missing content error:126-150— Progressiver Fehler bei fehlendem content in write_to_file; mit Kontextnutzungs-Hinweis, nach zwei aufeinanderfolgenden Fehlern wechselt der Hinweistext.auto-approval flow:205-235— Auto-Approval ruft say statt ask auf, dannsetTimeoutPromise(3_500)warten auf diagnostics.validateAndPrepareFileOperation:436— Gemeinsame Validierungslogik: Multi-Workspace-Pfad auflösen, clineignore-Prüfung, editType entscheiden, newContent konstruieren.diff construct:490-558— replace_in_file-Zweig: zuerstapplyModelContentFixesrepariert den Diff-Text, dannconstructNewFileContentauf das originalContent angewandt; bei Misserfolg telemetrisch nach Fehlertyp getrennt.content branch:559-577— write_to_file-Zweig: ```-Umrandung entfernen,applyModelContentFixesfür modellspezifische Probleme aufrufen.save & track:359-377—markFileAsEditedByCline+saveChanges+ fileReadCache invalidieren +trackFileContext("cline_edited").user edits:380-411— Erkennt manuelle Nutzer-Editierungen im Approval-Fenster; mitapplyPatch(newContent, userEdits)wird der pre-save-Inhalt rekonstruiert und jeweils human accepted-Telemetrie gemeldet.
Datenfluss
Nach Eintritt in execute wird zuerst validateAndPrepareFileOperation aufgerufen, um Parameter und newContent auf Vorrat vorzubereiten; danach folgen Approval und Speichern. Der zentrale Konstruktionsabschnitt ist der Diff-Anwendungspfad von replace_in_file:
// 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 verwendet die 7-Zeichen-Begrenzer der SEARCH/REPLACE-Blocks (------- SEARCH / ======= / +++++++ REPLACE), um das SEARCH-Segment im originalContent zu lokalisieren; bei Misserfolg wird auf eine fuzz-Strategie zurückgegriffen (constructNewFileContent:245). Sobald newContent vorliegt, wird die Diff-Ansicht geöffnet, update(newContent, true) schiebt den finalen Inhalt hinein und finalisiert; dann scrollt scrollToFirstDiff den Cursor zur ersten Differenz.
Nach Approval schreibt saveChanges wirklich auf die Platte und gibt userEdits / autoFormattingEdits / finalContent zurück (saveChanges:337). Hat der Nutzer im Approval-Fenster Änderungen vorgenommen, rekonstruiert der Handler mit applyPatch(newContent, userEdits) den pre-save-Inhalt, meldet human accepted-Telemetrie und schickt die userEdits als user_feedback_diff an die LLM zurück. Nach erfolgreichem Speichern führt die Task nach Abschluss aller Werkzeugausführungen einheitlich checkpointManager.saveCheckpoint() aus (post-tool checkpoint:3811), um die Dateiänderung als Checkpoint zu fixieren.
Grenzen und Fehler
- Diff-Anwendung fehlgeschlagen: Wenn das SEARCH-Segment in der Originaldatei nicht lokalisiert werden kann, wird geworfen; bei partial-block still return, um Streaming-Flackern zu vermeiden; bei vollständigem Block erfolgt ein
diff_error-Hinweis und telemetrisch nachsearch_not_found/other_diff_errorgetrennt (diff error path:509-558). - clineignore lehnt ab: Wenn der Pfad eine clineignore-Regel trifft, gibt pushToolResult direkt
clineIgnoreErrorzurück, ohne die Diff-Ansicht zu öffnen (clineignore check:454-474). - Leerer content: Bei write_to_file ist ein leerer content-String zulässig (legitimes Leeren der Datei), daher wird mit
== nullstatt truthy geprüft; fehlt content, erfolgt ein progressiver Fehler mit Kontextnutzungs-Hinweis zum Wiederholen (missing content error:126-150). - PreToolUse hook bricht ab: Wirft der Hook
PreToolUseHookCancellationError, werden revertChanges + reset aufgerufen undtoolDeniedzurückgegeben, ohne an die Oberfläche zu werfen (PreToolUse hook:344-356). - Nutzer lehnt ab: revertChanges stellt die Diff-Ansicht wieder her;
didRejectTool = true; nachfolgende text-Blocks werden auf Task-Ebene übersprungen (revert on reject:300). - Cache invalidieren: Nach dem Speichern wird
fileReadCache.delete(absolutePath.toLowerCase())aufgerufen, damit der nächste read_file nicht veralteten Inhalt liest; nach jedem execute_command leert die Task den gesamten Cache (cache invalidate:371). - Checkpoint-Ablage: Der Handler selbst speichert keinen Checkpoint; nach dem Speichern legt die Task einheitlich an, sobald
userMessageContentReadyerfüllt ist, und erneut, wenn der Nutzer im Approval-Fenster Feedback gibt (user feedback checkpoint:1542).
Zusammenfassung
WriteToFileToolHandler fasst „gesamte Datei überschreiben" und „SEARCH/REPLACE-Patch" in einer Klasse zusammen; über validateAndPrepareFileOperation teilt sich die Vorab-Prüfung, und über den DiffViewProvider wird die Änderung im nativen VSCode-Diff-Editor präsentiert und ein Rückfluss manueller Nutzer-Editierungen unterstützt. Jegliche Modellausgabe läuft durch den applyModelContentFixes-Fallback, sodass überflüssige Umrandungen schwacher Modelle und nicht-escapte HTML-Entities vor dem Speichern bereinigt werden.
Wer tiefer einsteigen will, kann weiterlesen bei:
- Die Multi-File-Patch-Alternative:
/edit-tools/apply-patch - Wie die Task ihn ansteuert:
/agent-loop/task-class - Diff-Ansicht-Grundlage:
/edit-tools/diff-view-provider
Siehe offizielle Dokumentation: Cline-Dokumentation · README