ApplyPatchHandler: parche multi-archivo y SEARCH/REPLACE
Responsabilidades
ApplyPatchHandler es el procesador de parche multi-archivo introducido en Cline v4, correspondiente al nombre de herramienta ClineDefaultTool.APPLY_PATCH. Recibe el texto completo *** Begin Patch / *** End Patch que escupe el LLM y, dentro de un único bloque tool_use, modifica, agrega, borra y mueve múltiples archivos a la vez: comprime en una sola llamada a herramienta lo que antes requería N llamadas write_to_file/replace_in_file para cambios en varios archivos (class declaration:45-46).
No sigue la misma ruta diff que WriteToFileToolHandler. replace_in_file usa bloques SEARCH/REPLACE con valla de 7 caracteres (parseado por constructNewFileContent, ver SEARCH block markers:1-3) y solo cambia un archivo por vez; ApplyPatchHandler usa otro formato: comienza con *** Begin Patch, separa archivos con *** Add File: path / *** Update File: path / *** Delete File: path, y dentro de cada bloque usa líneas +/- para inserciones y borrados (PATCH_MARKERS:4-13). Este formato proviene del protocolo apply_patch de OpenAI y permite expresar de una sola vez cambios que cruzan varios archivos.
En la arquitectura de Cline también implementa IFullyManagedTool, pero su estado es más complejo que el de WriteToFileToolHandler: mantiene dos ayudantes, PathResolver y FileProviderOperations, y en execute corre el flujo «preprocesamiento → cargar todos los archivos a modificar → PatchParser parsea → convertir a commit → por archivo prepare → aprobación → save → cierre». Cada archivo pasa por prepare + aprobación + save por separado, no de una sola vez.
Motivación de diseño
- Multi-archivo en una sola llamada: el modelo emite de una vez un conjunto de cambios lógicamente relacionados (p. ej. cambiar una interfaz y todos sus call sites), evitando el back-and-forth de N rondas con WriteToFileToolHandler (
execute entry:210). - Reutilizar vista diff: los cambios de cada archivo siguen presentándose en DiffViewProvider, de modo que el parche multi-archivo se aprueba archivo por archivo con diff, no como una caja negra monolítica (
prepareFileChange:632). - Fallback para wrapper bash: el modelo suele envolver el parche en
```bash/apply_patch/EOF;stripBashWrapperquita esa envoltura antes de parsear (stripBashWrapper:433). - Error por centinela incompleto: si solo hay
*** Begin Patcho solo*** End Patch, lanzaDiffErrorpara que el modelo reintente con un parche más pequeño, sin intentar adivinar (preprocessLines:416-431). - MOVE como create+delete: en Update +
*** Move to:, el prepare trata la nueva ruta como create, y tras guardar exitosamente borra la ruta original (move handling:316-323). - Passthrough de fuzz: PatchParser permite fuzz matching al parsear; si fuzz > 0, el resultado incluye una línea de aviso para que el modelo sepa que el match no fue exacto (
fuzz note:403-405).
Archivos clave
class declaration:45—export class ApplyPatchHandler implements IFullyManagedTool,name = ClineDefaultTool.APPLY_PATCH.handlePartialBlock:66— vista previa en streaming del primer archivo del parche; primeroextractAllFilespara ver el path, luegopreviewPatchStreamabre la vista diff.execute:210— flujo principal: preprocess → loadFiles → PatchParser.parse → patchToCommit → por archivo prepare/approve/save.preprocessLines:416-431— completa centinelas BEGIN/END faltantes; si solo falta uno, lanzaDiffError("incomplete sentinels").stripBashWrapper:433— quita las envolturas%%bash/apply_patch/EOF/ ``` conservando el cuerpo del parche.loadFiles:514— lee de una vez todas las rutas UPDATE/DELETE; clineignore o archivo inexistente lanzan DiffError.patchToCommit:539— convierte el Patch en estructuraCommit: DELETE guarda oldContent, ADD guarda newContent, UPDATE llamaapplyChunks.applyChunks:584— según el origIndex del chunk, corta el archivo original en rebanadas, copia segmentos sin cambio, inserta insLines y salta delLines.prepareFileChange:632— usa FileProviderOperations para abrir diff view y update, sin save.handleApproval:718— un ask por archivo, con fileOps (filesCreated/Deleted/Moved) para telemetría.constructNewFileContent:245— parser SEARCH/REPLACE que usa replace_in_file; con apply_patch es una ruta independiente.PATCH_MARKERS:4— constantes de todos los centinelas de parche.
Flujo de datos
execute arranca con preprocesamiento y carga, parsea el texto completo del parche en un commit estructurado y luego corre por archivo el ciclo aprobación-guardar. El núcleo es la conversión 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 completa o rechaza entradas sin centinelas (preprocessLines:416); loadFiles lee en memoria los archivos originales afectados por UPDATE/DELETE; PatchParser convierte el arreglo de líneas en Patch (acciones + chunks + factor fuzz); patchToCommit transforma cada acción en FileChange. La rama UPDATE invoca applyChunks, que esencialmente corta y recompone el arreglo de líneas original según 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))Tras construir el commit, generateChangeSummary genera un mensaje ClineSayTool por archivo; luego, por archivo: prepareFileChange (abre diff view, update, sin save) → handleApproval (auto o ask) → saveFileChange. Para MOVE, tras guardar exitosamente el archivo nuevo, se llama deleteFile(originalPath). Cuando todos los archivos terminan, se ejecuta markFileAsEditedByCline + trackFileContext("cline_edited") + invalidación de fileReadCache sobre cada changedFilePath.
Límites y fallos
- Centinelas incompletos: si solo está BEGIN o solo END, lanza DiffError sugiriendo al modelo «reintentar con un parche más pequeño» (
incomplete sentinels:429-431). - clineignore coincide: si loadFiles acierta en clineignore, lanza DiffError y aborta todo el parche, sin aplicación parcial (
clineignore throw:522-526). - Archivo no existe: si un archivo objetivo de UPDATE/DELETE no se encuentra en disco, lanza DiffError
File not found(file not found:528-530). - Usuario rechaza uno, rollback total: si
handleApprovaldevuelve false, se llamarevertChanges+resety se devuelve el mensaje de rechazo; no se procesan más archivos (reject abort:304-310). - Invalidación de caché para MOVE: el fileReadCache de MOVE debe invalidar tanto la ruta nueva como la vieja, para que la próxima lectura del archivo viejo no devuelva contenido ya borrado (
move cache invalidate:343-345). - Partial block silencioso: cualquier fallo de parseo en
previewPatchStreamse atrapa en silencio y se devuelve, esperando más datos en el stream para reintentar (partial catch:83-85). - Orden de chunks roto: si currentIndex es mayor que origIndex (el orden de los chunks no se corresponde con el archivo original), lanza DiffError (
chunk order check:597-599).
Resumen
ApplyPatchHandler es la versión multi-archivo de WriteToFileToolHandler, usando el protocolo *** Begin Patch / *** End Patch para expresar cambios cross-file en un único tool_use. Junto a replace_in_file, cada uno recorre una ruta de parseo independiente: el primero corta por líneas con PatchParser; el segundo localiza con constructNewFileContent por bloques SEARCH/REPLACE. Ambos terminan presentándose en DiffViewProvider al usuario.
Para profundizar:
- Sobreescritura de un solo archivo / SEARCH/REPLACE:
/edit-tools/write-to-file - Base de la vista diff:
/edit-tools/diff-view-provider - Cómo Task despacha herramientas:
/agent-loop/task-class