Skip to content

WriteToFileToolHandler : écriture de fichier et présentation diff

源码版本v4.0.10

Responsabilités

WriteToFileToolHandler est le processeur principal de Cline pour persister les modifications de fichiers. Enregistré sous le nom ClineDefaultTool.FILE_NEW, il gère en réalité trois appels distincts : write_to_file (remplacement total du fichier), replace_in_file (patch par blocs SEARCH/REPLACE), new_rule (remplacement de fichier de règles) (class declaration:26-27). Un seul handler pour trois noms d'outil parce qu'ils empruntent le même pipeline « valider le chemin → construire newContent → ouvrir la vue diff → approbation → écriture disque ».

Dans le système d'outils de Cline, il se positionne comme « handler concret invoqué par ToolExecutor » et implémente l'interface IFullyManagedTool. Chaque handler expose deux entrées : handlePartialBlock pilote l'UI pendant que le LLM stream encore le contenu (pousse le contenu au fil de l'eau dans DiffViewProvider), tandis que execute, une fois le block complet, fait la vraie écriture disque, l'approbation et le checkpoint final. Toute interaction UI passe par config.callbacks et uiHelpers vers Task ; le handler ne touche pas la webview directement.

Il sert aussi de dernier remont avant l'écriture disque : validation clineignore, parsing multi-workspace des chemins, nettoyage des déchets classiques que produisent les modèles (triple backticks, entités HTML non échappées, barres d'échappement superflues). Il normalise la sortie modèle en contenu persistable, puis confie newContent à DiffViewProvider pour présentation à l'utilisateur dans l'éditeur diff de VSCode.

Motivation de conception

  • Un handler, trois noms : write_to_file, replace_in_file et new_rule partagent le même chemin de persistance ; ils ne se distinguent que par « comment construire newContent » (contenu entier vs SEARCH/REPLACE vs fichier de règles), d'où leur fusion dans une seule classe pour éviter la duplication (validateAndPrepareFileOperation:436).
  • Aperçu en streaming : le LLM émet content au fur et à mesure, le handler le pousse dans DiffViewProvider au fil de l'eau, l'utilisateur voit le diff évoluer avant la fin du message LLM. Cela impose à handlePartialBlock de ne pas lever d'erreur sur un contenu incomplet (handlePartialBlock:35).
  • Repli sur sortie modèle : les modèles faibles ajoutent souvent des ``` ou oublient d'échapper des entités HTML ; le handler retire les fences avant écriture et appelle applyModelContentFixes pour réparer les entités (markdown strip:564-573).
  • hook PreToolUse post-approbation : le hook s'exécute après approbation utilisateur mais avant l'écriture réelle, de sorte qu'en cas de refus du hook on puisse revertChanges et restaurer la vue diff (PreToolUse hook:344-356).
  • Sensibilité aux éditions utilisateur : si l'utilisateur modifie le contenu dans la fenêtre d'approbation, le handler renvoie ces userEdits au LLM, et la télémétrie distingue agent accepted de human accepted (user edits:380-411).
  • Compteur d'erreurs à remise différée : il n'est pas remis à zéro à l'entrée d'execute, seulement après saveChanges réussi, pour que les erreurs consécutives s'accumulent jusqu'au plafond YOLO (reset counter:365-366).

Fichiers clés

  • class declaration:26export class WriteToFileToolHandler implements IFullyManagedTool, name = ClineDefaultTool.FILE_NEW ; le commentaire précise qu'il couvre trois noms d'outil.
  • handlePartialBlock:35 — entrée streaming : ouvre la vue diff dès que path + content/diff sont disponibles, puis update(newContent, false) au fil de l'eau.
  • execute:97 — flux principal post-complétion : validation → construction de newContent → approbation → écriture → gestion des userEdits.
  • missing content error:126-150 — erreur progressive quand write_to_file manque content, avec hint sur le taux d'utilisation de contexte et un message renforcé après deux erreurs consécutives.
  • auto-approval flow:205-235 — l'auto-approbation passe par say plutôt que ask, puis setTimeoutPromise(3_500) pour laisser les diagnostics suivre.
  • validateAndPrepareFileOperation:436 — logique de validation partagée : parsing multi-workspace des chemins, vérification clineignore, décision editType, construction de newContent.
  • diff construct:490-558 — branche replace_in_file : applyModelContentFixes répare le texte diff, puis constructNewFileContent l'applique à originalContent ; en cas d'échec, télémétrie bucketisée par type d'erreur.
  • content branch:559-577 — branche write_to_file : retire les fences ``` et appelle applyModelContentFixes pour les problèmes spécifiques au modèle.
  • save & track:359-377markFileAsEditedByCline + saveChanges + invalidation fileReadCache + trackFileContext("cline_edited").
  • user edits:380-411 — détecte les modifications utilisateur dans la fenêtre d'approbation, restaure le contenu pré-save via applyPatch(newContent, userEdits), et émet la télémétrie human accepted séparément.

Flux de données

À l'entrée d'execute, on appelle validateAndPrepareFileOperation pour préparer paramètres et newContent, puis on enchaîne approbation et écriture disque. Le cœur de la construction est le chemin d'application du diff pour replace_in_file :

typescript
// apps/vscode/src/core/task/tools/handlers/WriteToFileToolHandler.ts
if (diff) {
    diff = applyModelContentFixes(diff, config.api.getModel().id, resolvedPath)
    if (!config.services.diffViewProvider.isEditing) {
        await config.services.diffViewProvider.open(absolutePath, { displayPath: relPath })
    }
    try {
        const result = await constructNewFileContent(
            diff,
            config.services.diffViewProvider.originalContent || "",
            !block.partial,
        )
        newContent = result.newContent
        matchIndices = result.matchIndices
    } catch (error) {
        if (block.partial) {
            return
        }
        config.taskState.consecutiveMistakeCount++
        await config.callbacks.removeLastPartialMessageIfExistsWithType("say", "diff_error")
        await config.callbacks.say("diff_error", relPath, undefined, undefined, true)
        // ...
    }
}

constructNewFileContent utilise les délimiteurs 7 caractères des blocs SEARCH/REPLACE (------- SEARCH / ======= / +++++++ REPLACE) pour localiser le segment SEARCH dans originalContent ; en cas d'échec, repli par stratégie fuzz (constructNewFileContent:245). Une fois newContent obtenu, la vue diff est ouverte, update(newContent, true) pousse le contenu final et finalize, puis scrollToFirstDiff ramène le curseur sur la première différence.

Après approbation, saveChanges écrit réellement sur le disque et renvoie userEdits / autoFormattingEdits / finalContent (saveChanges:337). Si l'utilisateur a modifié le contenu dans la fenêtre d'approbation, le handler utilise applyPatch(newContent, userEdits) pour restaurer le contenu pré-save, émet la télémétrie human accepted et renvoie userEdits au LLM sous forme de user_feedback_diff. Une fois l'écriture terminée, Task lance un checkpointManager.saveCheckpoint() unifié à la fin de tous les outils (post-tool checkpoint:3811) pour figer cette modification de fichier en checkpoint.

Limites et échecs

  • échec d'application du diff : si le segment SEARCH ne peut être localisé dans le fichier original, on lève une erreur ; partial block retourne silencieusement pour éviter le bruit en streaming, block complet émet un diff_error et remonte la télémétrie bucketisée search_not_found / other_diff_error (diff error path:509-558).
  • refus clineignore : si le chemin correspond à (match) une règle clineignore, on push directement un clineIgnoreError sans ouvrir la vue diff (clineignore check:454-474).
  • content vide : un content chaîne vide reçu par write_to_file est légitime (vider un fichier), testé via == null plutôt que truthy ; mais un content absent déclenche l'erreur progressive et un hint de retry selon le taux d'utilisation du contexte (missing content error:126-150).
  • hook PreToolUse annule : s'il lève PreToolUseHookCancellationError, on revertChanges + reset et on renvoie toolDenied sans remonter l'erreur à l'étage supérieur (PreToolUse hook:344-356).
  • refus utilisateur : revertChanges restaure la vue diff à son état d'origine, didRejectTool = true, et les blocs de texte suivants seront skipés par Task (revert on reject:300).
  • invalidation du cache : après écriture disque, fileReadCache.delete(absolutePath.toLowerCase()) évite qu'un prochain read_file lise du contenu obsolète ; après n'importe quel execute_command, Task vide le cache dans son ensemble (cache invalidate:371).
  • ancrage du checkpoint : le handler ne stocke pas lui-même le checkpoint ; après écriture, Task le fait uniformément une fois userMessageContentReady satisfait, et une seconde fois lorsque l'utilisateur donne son feedback dans la fenêtre d'approbation (user feedback checkpoint:1542).

Résumé

WriteToFileToolHandler fusionne « remplacement total de fichier » et « patch SEARCH/REPLACE » dans une seule classe qui partage la validation amont via validateAndPrepareFileOperation et la présentation via DiffViewProvider dans l'éditeur diff natif de VSCode, avec retour des éditions utilisateur. Toute sortie modèle passe par applyModelContentFixes : les fences en trop et les entités HTML non échappées sont nettoyées avant l'écriture disque.

Pour aller plus loin :

  • Le patch multi-fichiers qui le supplée : /edit-tools/apply-patch
  • Comment Task le distribue : /agent-loop/task-class
  • Sous-couche de la vue diff : /edit-tools/diff-view-provider

Voir la documentation officielle : documentation Cline · README.