ApplyPatchHandler: Multi-File-Patches und SEARCH/REPLACE
Verantwortung
ApplyPatchHandler ist der in Cline v4 eingeführte Multi-File-Patch-Prozessor und entspricht dem Werkzeugnamen ClineDefaultTool.APPLY_PATCH. Er nimmt einen kompletten, vom LLM ausgegebenen *** Begin Patch / *** End Patch-Textblock entgegen und kann in einem einzigen tool_use-Block mehrere Dateien gleichzeitig ändern, hinzufügen, löschen und verschieben, sodass Mehrfachdatei-Änderungen, die früher in N aufeinanderfolgende write_to_file/replace_in_file-Aufrufe aufgespalten werden mussten, in einem einzigen Werkzeugaufruf zusammenlaufen (class declaration:45-46).
Er läuft nicht über denselben Diff-Pfad wie WriteToFileToolHandler. replace_in_file verwendet SEARCH/REPLACE-Blocks mit 7-Zeichen-Begrenzern (geparst von constructNewFileContent, siehe SEARCH block markers:1-3) und kann nur eine Datei pro Aufruf ändern; ApplyPatchHandler nutzt ein anderes Format: Eingeleitet mit *** Begin Patch, dann *** Add File: path / *** Update File: path / *** Delete File: path als Datei-Blöcke, innerhalb derer +/--Zeilen Hinzufügungen und Löschungen markieren (PATCH_MARKERS:4-13). Dieses Format stammt aus dem apply_patch-Protokoll von OpenAI und eignet sich, um Änderungen über mehrere Dateien hinweg in einem Schritt auszudrücken.
In der Cline-Architektur implementiert er ebenfalls IFullyManagedTool, ist aber zustandskomplexer als WriteToFileToolHandler: Er hält zwei Helfer, PathResolver und FileProviderOperations, und führt in execute die Pipeline „Präprozessierung → Laden aller zu ändernden Dateien → PatchParser-Parsing → Umwandlung in Commit → Datei für Datei prepare → Approval → save → Aufräumen" aus. Jede Datei durchläuft einzeln prepare + Approval + save, nicht alles in einem Rutsch.
Entwurfsmotivation
- Multi-File, einzelner Aufruf: Das Modell kann eine Gruppe logisch zusammenhängender Änderungen (z. B. Interface ändern und gleichzeitig alle Aufrufer anpassen) in einem Schritt ausgeben, um den Overhead von N aufeinanderfolgenden WriteToFileToolHandler-Aufrufen zu vermeiden (
execute entry:210). - Diff-Ansicht wiederverwenden: Die Änderung jeder Datei wird weiterhin über den DiffViewProvider dem Nutzer präsentiert, sodass der Approval-Schritt für einen Multi-File-Patch dateiweise als Diff erfolgt statt als einmaliger Blackbox (
prepareFileChange:632). - bash-Wrapper-Fallback: Modelle packen den Patch oft in
```bash/apply_patch/EOFein;stripBashWrapperentfernt diese Hüllen vor dem Parsen (stripBashWrapper:433). - Fehler bei unvollständigen Sentinels: Wenn nur
*** Begin Patchoder nur*** End Patchvorhanden ist, wird einDiffErrorgeworfen, damit das Modell in kleineren Schritten neu versucht, ohne zu raten (preprocessLines:416-431). - MOVE als eigener create+delete-Pfad: Bei Update +
*** Move to:behandelt die prepare-Phase den neuen Pfad als create; nach erfolgreichem save wird der Originalpfad gelöscht (move handling:316-323). - fuzz durchreichen: PatchParser erlaubt fuzz-Matching beim Parsen; bei fuzz > 0 wird im Ergebnis ein Hinweis angefügt, damit das Modell erfährt, dass die Übereinstimmung ungenau war (
fuzz note:403-405).
Schlüsseldateien
class declaration:45—export class ApplyPatchHandler implements IFullyManagedTool,name = ClineDefaultTool.APPLY_PATCH.handlePartialBlock:66— Streamt eine Vorschau des Patches der ersten Datei; zuerstextractAllFilesfür den Pfad, dannpreviewPatchStreamöffnet die Diff-Ansicht.execute:210— Hauptfluss: preprocess → loadFiles → PatchParser.parse → patchToCommit → dateiweises prepare/approve/save.preprocessLines:416-431— Fehlende BEGIN/END-Sentinels ergänzen; fehlt genau eines, wirdDiffError("incomplete sentinels")geworfen.stripBashWrapper:433— Entfernt%%bash/apply_patch/EOF/ ```-Wrapper und behält den Patch-Körper.loadFiles:514— Liest alle UPDATE/DELETE-Pfade auf einmal ein; clineignore-Treffer oder nicht existierende Datei werfen DiffError.patchToCommit:539— Wandelt einen Patch in eineCommit-Struktur um: DELETE speichert oldContent, ADD speichert newContent, UPDATE ruftapplyChunksauf.applyChunks:584— Schneidet die Originaldatei an chunk.origIndex, kopiert unveränderte Segmente, fügt insLines ein und überspringt delLines.prepareFileChange:632— Öffnet mit FileProviderOperations die Diff-Ansicht und update, ohne zu save.handleApproval:718— Fragt für jede Datei einzeln ab; mit fileOps (filesCreated/Deleted/Moved) wird Telemetrie gemeldet.constructNewFileContent:245— Der SEARCH/REPLACE-Parser, über den replace_in_file läuft; er und apply_patch sind zwei unabhängige Pfade.PATCH_MARKERS:4— Konstanten für alle Patch-Sentinels.
Datenfluss
execute beginnt mit Präprozessierung und Laden; der ganze Patch-Text wird in einen strukturierten Commit geparst und dann dateiweise durch den Approval-Speicher-Zyklus geführt. Im Zentrum steht die Umwandlung patch → commit:
// apps/vscode/src/core/task/tools/handlers/ApplyPatchHandler.ts
const lines = this.preprocessLines(rawInput)
const filesToLoad = this.extractFilesForOperations(rawInput, [PATCH_MARKERS.UPDATE, PATCH_MARKERS.DELETE])
const currentFiles = await this.loadFiles(config, filesToLoad)
const parser = new PatchParser(lines, currentFiles)
const { patch, fuzz } = parser.parse()
const commit = await this.patchToCommit(patch, currentFiles)preprocessLines ergänzt Eingaben mit fehlenden Sentinels oder lehnt sie ab (preprocessLines:416); loadFiles liest die von UPDATE/DELETE betroffenen Originaldateien in den Speicher; PatchParser parst das Zeilen-Array in einen Patch (actions + chunks + fuzz-Faktor); patchToCommit wandelt jede action in eine FileChange um. Der UPDATE-Zweig ruft applyChunks auf, was im Wesentlichen das Zeilen-Array der Originaldatei anhand von origIndex neu zusammensetzt:
// apps/vscode/src/core/task/tools/handlers/ApplyPatchHandler.ts
for (const chunk of chunks) {
if (chunk.origIndex > lines.length) {
throw new DiffError(`${path}: chunk.origIndex ${chunk.origIndex} > lines.length ${lines.length}`)
}
// Copy lines before the chunk
result.push(...lines.slice(currentIndex, chunk.origIndex))
const originalLines = lines.slice(chunk.origIndex, chunk.origIndex + chunk.delLines.length)
const insertedLines = chunk.insLines.map((line) => {
if (tryPreserveEscaping && originalText) {
return preserveEscaping(originalText, line)
}
return line
})
result.push(...insertedLines)
currentIndex = chunk.origIndex + chunk.delLines.length
}
result.push(...lines.slice(currentIndex))Nachdem der Commit konstruiert ist, erzeugt generateChangeSummary für jede Datei eine ClineSayTool-Nachricht; dann wird dateiweise prepareFileChange (Diff-Ansicht öffnen, update, nicht save) → handleApproval (auto oder ask) → saveFileChange ausgeführt. Bei einer MOVE-Operation wird nach erfolgreichem save der neuen Datei deleteFile(originalPath) aufgerufen. Nachdem alle Dateien durchlaufen sind, wird auf jedem changedFilePath einheitlich markFileAsEditedByCline + trackFileContext("cline_edited") ausgeführt und der fileReadCache invalidiert.
Grenzen und Fehler
- Unvollständige Sentinels: Nur BEGIN oder nur END wirft sofort DiffError und weist das Modell an, „in kleinere Patches aufzuspalten und neu zu versuchen" (
incomplete sentinels:429-431). - clineignore-Treffer: Bei Treffer in loadFiles wird DiffError geworfen, der gesamte Patch wird abgebrochen und nicht teilweise angewendet (
clineignore throw:522-526). - Datei nicht vorhanden: Ist die von UPDATE/DELETE angesprochene Datei auf der Platte nicht zu finden, wird ein
File not foundDiffError geworfen (file not found:528-530). - Nutzer lehnt eine Datei ab, alle werden zurückgerollt: Gibt
handleApprovalfalse zurück, wird sofortrevertChanges+resetaufgerufen und eine Ablehnungsnachricht zurückgegeben; die weiteren Dateien werden nicht mehr bearbeitet (reject abort:304-310). - MOVE: alter Pfad verliert Cache: Der fileReadCache einer MOVE muss gleichzeitig für den neuen und den alten Pfad invalidiert werden, damit ein künftiger Lesezugriff auf die alte Datei nicht aus dem Cache gelöschten Inhalt liefert (
move cache invalidate:343-345). - partial block schweigt: Jeder Parse-Fehler in
previewPatchStreamwird imcatchstillgeschwiegen; es wird auf weitere Daten gewartet und dann erneut versucht (partial catch:83-85). - chunk-Reihenfolge falsch: Wenn currentIndex größer als origIndex ist (die chunk-Reihenfolge passt nicht zur Originaldatei), wird DiffError geworfen (
chunk order check:597-599).
Zusammenfassung
ApplyPatchHandler ist die Multi-File-Version des WriteToFileToolHandler: Sie verwendet das *** Begin Patch / *** End Patch-Protokoll, um dateiübergreifende Änderungen in einem einzelnen tool_use auszudrücken. Sie und replace_in_file laufen über jeweils einen unabhängigen Parse-Pfad: Ersterer nutzt PatchParser zum zeilenweisen Zerschneiden, Letzterer nutzt constructNewFileContent zur Lokalisierung über SEARCH/REPLACE-Blocks. Beide landen letztlich im DiffViewProvider, um dem Nutzer präsentiert zu werden.
Wer tiefer einsteigen will, kann weiterlesen bei:
- Single-File-Überschreibung / SEARCH/REPLACE:
/edit-tools/write-to-file - Diff-Ansicht-Grundlage:
/edit-tools/diff-view-provider - Wie die Task Werkzeuge ansteuert:
/agent-loop/task-class
Siehe offizielle Dokumentation: Cline-Dokumentation · README