Skip to content

ApplyPatchHandler: parche multi-archivo y SEARCH/REPLACE

源码版本v4.0.10

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; stripBashWrapper quita esa envoltura antes de parsear (stripBashWrapper:433).
  • Error por centinela incompleto: si solo hay *** Begin Patch o solo *** End Patch, lanza DiffError para 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:45export class ApplyPatchHandler implements IFullyManagedTool, name = ClineDefaultTool.APPLY_PATCH.
  • handlePartialBlock:66 — vista previa en streaming del primer archivo del parche; primero extractAllFiles para ver el path, luego previewPatchStream abre 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, lanza DiffError("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 estructura Commit: DELETE guarda oldContent, ADD guarda newContent, UPDATE llama applyChunks.
  • 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:

typescript
// 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:

typescript
// 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 handleApproval devuelve false, se llama revertChanges + reset y 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 previewPatchStream se 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

Véase la documentación oficial: Cline 文档 · README