ApplyPatchHandler : patch multi-fichiers et SEARCH/REPLACE
Responsabilités
ApplyPatchHandler est le processeur de patch multi-fichiers introduit par Cline v4, correspondant au nom d'outil ClineDefaultTool.APPLY_PATCH. Il prend le texte *** Begin Patch / *** End Patch produit par le LLM et, dans un seul bloc tool_use, modifie, ajoute, supprime et déplace plusieurs fichiers simultanément, compressant en un seul appel d'outil ce qui aurait autrefois demandé N write_to_file/replace_in_file successifs (class declaration:45-46).
Il n'emprunte pas le même chemin diff que WriteToFileToolHandler. replace_in_file utilise des blocs SEARCH/REPLACE à délimiteurs 7 caractères (parsés par constructNewFileContent, voir SEARCH block markers:1-3), un seul fichier à la fois ; ApplyPatchHandler utilise un autre format : *** Begin Patch en en-tête, puis *** Add File: path / *** Update File: path / *** Delete File: path pour sectionner par fichier, et à l'intérieur de chaque section des lignes +/- pour ajouter/supprimer (PATCH_MARKERS:4-13). Ce format est issu du protocole apply_patch d'OpenAI, adapté à l'expression d'un diff multi-fichiers en un seul tenant.
Dans l'architecture de Cline, il implémente lui aussi IFullyManagedTool, mais avec un état plus complexe que WriteToFileToolHandler : il détient deux assistants, PathResolver et FileProviderOperations, et execute enchaîne « prétraitement → chargement de tous les fichiers → parsing PatchParser → conversion en commit → prepare par fichier → approbation → save → nettoyage ». Chaque fichier passe individuellement par prepare + approbation + save, et non en un seul lot.
Motivation de conception
- Multi-fichiers en un seul appel : le modèle peut émettre un groupe de changements logiquement liés (modifier une interface et tous ses appelants, par exemple), évitant les allers-retours de N
write_to_filesuccessifs (execute entry:210). - Réutiliser la vue diff : chaque modification de fichier continue de transiter par DiffViewProvider, donc le patch multi-fichiers se présente à l'approbation comme un diff par fichier, pas comme une boîte noire globale (
prepareFileChange:632). - Repli sur wrapper bash : le modèle emballe souvent le patch dans
```bash/apply_patch/EOF;stripBashWrapperdébarrasse ces enveloppes avant le parsing (stripBashWrapper:433). - Erreur sur sentinelles incomplètes : si seul
*** Begin Patchou*** End Patchest présent, on lèveDiffErrorpour que le modèle réessaie en plus petit, sans tenter de deviner (preprocessLines:416-431). - MOVE géré en create + delete : sur Update +
*** Move to:, le prepare traite le nouveau chemin comme un create, et après save réussie on supprime le chemin d'origine (move handling:316-323). - Fuzz en transparence : PatchParser tolère le fuzz matching ; si fuzz > 0, on ajoute une ligne au résultat pour informer le modèle que la correspondance n'est pas exacte (
fuzz note:403-405).
Fichiers clés
class declaration:45—export class ApplyPatchHandler implements IFullyManagedTool,name = ClineDefaultTool.APPLY_PATCH.handlePartialBlock:66— aperçu streaming du patch du premier fichier :extractAllFilesrécupère le path, puispreviewPatchStreamouvre la vue diff.execute:210— flux principal : preprocess → loadFiles → PatchParser.parse → patchToCommit → prepare/approve/save par fichier.preprocessLines:416-431— complète les sentinelles BEGIN/END, lèveDiffError("incomplete sentinels")s'il en manque une.stripBashWrapper:433— retire les enveloppes%%bash/apply_patch/EOF/ ``` et conserve le corps du patch.loadFiles:514— charge d'un coup tous les chemins UPDATE/DELETE ; une correspondance clineignore (hit) ou un fichier absent lève DiffError.patchToCommit:539— convertit le Patch enCommit: DELETE stocke oldContent, ADD stocke newContent, UPDATE appelleapplyChunks.applyChunks:584— découpe le fichier original selon l'origIndex de chaque chunk, copie les segments non modifiés, insère insLines, saute delLines.prepareFileChange:632— utilise FileProviderOperations pour ouvrir la vue diff et update, sans save.handleApproval:718— un ask par fichier, avec télémétrie fileOps (filesCreated/Deleted/Moved).constructNewFileContent:245— parseur SEARCH/REPLACE utilisé par replace_in_file, chemin indépendant d'apply_patch.PATCH_MARKERS:4— constantes de toutes les sentinelles de patch.
Flux de données
execute démarre par prétraitement et chargement, parse le texte du patch en un commit structuré, puis boucle par fichier sur approbation puis écriture. Le cœur est la conversion 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 complète ou rejette les entrées sans sentinelles (preprocessLines:416) ; loadFiles lit en mémoire les fichiers originaux visés par UPDATE/DELETE ; PatchParser parse le tableau de lignes en Patch (actions + chunks + facteur fuzz) ; patchToCommit convertit chaque action en FileChange. La branche UPDATE appelle applyChunks, qui découpe et réassemble le tableau de lignes du fichier original selon 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))Une fois le commit construit, generateChangeSummary produit un message ClineSayTool par fichier, puis on enchaîne par fichier prepareFileChange (ouvre la vue diff, update, sans save) → handleApproval (auto ou ask) → saveFileChange. L'opération MOVE déclenche deleteFile(originalPath) après save réussie du nouveau fichier. Quand tous les fichiers sont passés, on appelle uniformément markFileAsEditedByCline + trackFileContext("cline_edited") + invalidation du fileReadCache sur chaque changedFilePath.
Limites et échecs
- Sentinelles incomplètes : si seul BEGIN ou seul END est présent, on lève DiffError et on invite le modèle à « découper en plus petits patchs » (
incomplete sentinels:429-431). - clineignore touché : une correspondance (hit) à l'étape loadFiles lève DiffError, le patch entier est abandonné, aucune application partielle (
clineignore throw:522-526). - fichier absent : si la cible d'un UPDATE/DELETE n'est pas sur le disque, on lève
File not foundDiffError (file not found:528-530). - un refus utilisateur, tout revient en arrière : si
handleApprovalrenvoie false, on appelle immédiatementrevertChanges+resetet on renvoie un message de refus ; les fichiers suivants ne sont pas traités (reject abort:304-310). - invalidation du cache MOVE : pour un MOVE, on invalide le fileReadCache à la fois sur le nouveau chemin et sur l'ancien, afin que la prochaine lecture de l'ancien fichier ne récupère pas un contenu déjà supprimé depuis le cache (
move cache invalidate:343-345). - partial block silencieux : toute erreur de parsing dans
previewPatchStreamestcatchée silencieusement, en attendant davantage de données pour réessayer (partial catch:83-85). - chunk désordonné : si currentIndex dépasse origIndex (les chunks ne correspondent plus au fichier original), on lève DiffError (
chunk order check:597-599).
Résumé
ApplyPatchHandler est la version multi-fichiers de WriteToFileToolHandler : il exprime des changements inter-fichiers dans un seul tool_use via le protocole *** Begin Patch / *** End Patch. Il et replace_in_file suivent deux chemins de parsing indépendants : le premier découpe par lignes avec PatchParser, le second localise via constructNewFileContent et ses blocs SEARCH/REPLACE. Les deux aboutissent in fine au DiffViewProvider pour présentation à l'utilisateur.
Pour aller plus loin :
- Override de fichier unique / SEARCH/REPLACE :
/edit-tools/write-to-file - Sous-couche de la vue diff :
/edit-tools/diff-view-provider - Comment Task distribue les outils :
/agent-loop/task-class
Voir la documentation officielle : documentation Cline · README.