ApplyPatchHandler: Multi-File Patches and SEARCH/REPLACE
Responsibilities
ApplyPatchHandler is the multi-file patch handler introduced in Cline v4, registered to the tool name ClineDefaultTool.APPLY_PATCH. It takes a block of text the LLM emits between *** Begin Patch / *** End Patch markers and modifies, adds, deletes, and moves multiple files in a single tool_use block — collapsing what used to require N separate write_to_file/replace_in_file calls into one tool invocation (class declaration:45-46).
It does not share the diff path with WriteToFileToolHandler. The replace_in_file tool uses 7-character-fenced SEARCH/REPLACE blocks (parsed by constructNewFileContent; see SEARCH block markers:1-3) and can only touch one file at a time. ApplyPatchHandler uses a different format: it begins with *** Begin Patch, separates per-file blocks with *** Add File: path / *** Update File: path / *** Delete File: path, and within each block uses +/- lines to express additions and deletions (PATCH_MARKERS:4-13). This format originates from OpenAI's apply_patch protocol and is well suited to expressing cross-file changes in a single shot.
Within Cline's tool system it also implements IFullyManagedTool, but its state is more complex than WriteToFileToolHandler: it holds two helpers — PathResolver and FileProviderOperations — and execute runs a pipeline of "preprocess → load all files to modify → PatchParser parse → convert to commit → per-file prepare → approve → save → finalize". Each file goes through its own prepare + approve + save cycle, not one bulk pass.
Design motivation
- Multiple files, one call: lets the model emit a set of logically related changes (e.g. editing an interface and all of its call sites at once) in a single output, avoiding the back-and-forth of N serial WriteToFileToolHandler calls (
execute entry:210). - Reuse the diff view: each file's change still goes through DiffViewProvider, so a multi-file patch is presented to the user as a per-file diff at approval time, not a single black-box operation (
prepareFileChange:632). - Bash-wrapper fallback: models often wrap the patch in
```bash/apply_patch/EOF.stripBashWrapperstrips these shells off before parsing (stripBashWrapper:433). - Incomplete sentinels throw: when only
*** Begin Patchor only*** End Patchis present, it throws aDiffErrorto prompt the model to retry with a smaller patch — no guessing (preprocessLines:416-431). - MOVE goes through create + delete: for Update +
*** Move to:, the prepare phase treats the new path as a create; only after the save succeeds does it delete the original path (move handling:316-323). - Fuzz is passed through: PatchParser allows fuzzy matching during parsing; when fuzz > 0, a hint line is attached to the returned result so the model knows the match was not exact (
fuzz note:403-405).
Key files
class declaration:45—export class ApplyPatchHandler implements IFullyManagedTool,name = ClineDefaultTool.APPLY_PATCH.handlePartialBlock:66— streams a preview of the first file's patch: callsextractAllFilesto find the path, thenpreviewPatchStreamto open the diff view.execute:210— main flow: preprocess → loadFiles → PatchParser.parse → patchToCommit → per-file prepare/approve/save.preprocessLines:416-431— fills in missing BEGIN/END sentinels; if only one is missing, throwsDiffError("incomplete sentinels").stripBashWrapper:433— strips%%bash/apply_patch/EOF/ ``` wrappers, keeping the patch body.loadFiles:514— reads all UPDATE/DELETE paths in one pass; clineignore hits and missing files both throw DiffError.patchToCommit:539— converts the Patch into aCommitstructure: DELETE stores oldContent, ADD stores newContent, UPDATE callsapplyChunks.applyChunks:584— slices the original file by each chunk's origIndex, copying unchanged segments, inserting insLines, and skipping delLines.prepareFileChange:632— uses FileProviderOperations to open and update the diff view, but does not save.handleApproval:718— asks the user once per file, attaching fileOps (filesCreated/Deleted/Moved) to telemetry.constructNewFileContent:245— the SEARCH/REPLACE parser used by replace_in_file; an independent path from apply_patch.PATCH_MARKERS:4— all patch sentinel constants.
Data flow
execute opens with preprocessing and loading: it parses the whole patch text into a structured commit, then runs the approve-and-save loop file by file. The core is the patch → commit conversion:
// 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 first fills in or rejects inputs with missing sentinels (preprocessLines:416); loadFiles reads the original files touched by UPDATE/DELETE into memory; PatchParser parses the line array into a Patch (actions + chunks + fuzz factor); and patchToCommit converts each action into a FileChange. The UPDATE branch calls applyChunks, which is essentially slicing and reassembling the original file's line array by origIndex:
// 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))Once the commit is built, generateChangeSummary emits a ClineSayTool message for each file, and then for each file it runs prepareFileChange (open diff view, update, no save) → handleApproval (auto or ask) → saveFileChange. For MOVE, after the new file saves successfully, it calls deleteFile(originalPath). After every file is done, it runs markFileAsEditedByCline on each changedFilePath, calls trackFileContext("cline_edited"), and invalidates the fileReadCache.
Boundaries and failure modes
- Incomplete sentinels: only BEGIN or only END throws DiffError immediately, with a hint to "retry with a smaller patch" (
incomplete sentinels:429-431). - clineignore hit: if any file hits clineignore during loadFiles, a DiffError is thrown and the entire patch aborts — no partial application (
clineignore throw:522-526). - File not found: when an UPDATE/DELETE target file is not on disk, a
File not foundDiffError is thrown (file not found:528-530). - One user rejection rolls back everything: when
handleApprovalreturns false, the handler immediately callsrevertChanges+resetand returns the rejection message; the remaining files are not processed (reject abort:304-310). - MOVE invalidates the old path: invalidating the fileReadCache for a MOVE must cover both the new path and the old path, so a later read of the old file does not return the now-deleted content from cache (
move cache invalidate:343-345). - Partial block is silent: any parse failure inside
previewPatchStreamiscatch-silently returned; the handler waits for more data to arrive and retries (partial catch:83-85). - Out-of-order chunks: when currentIndex is greater than origIndex (i.e. chunk order does not correspond to the original file), a DiffError is thrown (
chunk order check:597-599).
Summary
ApplyPatchHandler is the multi-file version of WriteToFileToolHandler. It uses the *** Begin Patch / *** End Patch protocol to express cross-file changes in a single tool_use. It and replace_in_file walk separate parsing paths: the former slices lines via PatchParser, the latter locates blocks via constructNewFileContent using SEARCH/REPLACE. Both ultimately land on DiffViewProvider for presentation to the user.
To dig deeper, continue with:
- Single-file overwrite / SEARCH/REPLACE:
/edit-tools/write-to-file - The diff view internals:
/edit-tools/diff-view-provider - How the Task schedules tools:
/agent-loop/task-class