WriteToFileToolHandler: escritura de archivos y presentación diff
Responsabilidades
WriteToFileToolHandler es el procesador principal de Cline para llevar a disco los cambios de archivos. Se registra bajo el nombre ClineDefaultTool.FILE_NEW, pero en la práctica cubre tres llamadas: write_to_file (sobrescritura completa), replace_in_file (parche con bloques SEARCH/REPLACE), new_rule (sobrescritura de archivos de reglas) (class declaration:26-27). Un único handler para tres nombres de herramienta porque los tres recorren la misma pipeline «validar ruta → construir newContent → abrir diff view → aprobación → guardar a disco».
Dentro del sistema de herramientas de Cline es «el handler concreto levantado por ToolExecutor», e implementa la interfaz IFullyManagedTool. Cada handler tiene dos entradas: handlePartialBlock impulsa la UI mientras el LLM sigue emitiendo contenido (lo va empujando a DiffViewProvider conforme llega); execute, tras recibir el block completo, hace la verdadera escritura, aprobación y cierre del checkpoint. Toda interacción UI va por config.callbacks y uiHelpers de vuelta a Task; el handler no toca la webview directamente.
Además, actúa como la última línea de defensa antes de escribir a disco: valida clineignore, resuelve rutas multi-workspace y limpia el output errático habitual del modelo (cercas de triple comilla, entidades HTML sin escapar, escapes de más), normalizando lo que el modelo produce en contenido escribible, y luego entrega newContent a DiffViewProvider para presentarlo al usuario en el editor diff de VSCode.
Motivación de diseño
- Un handler, tres nombres: write_to_file, replace_in_file y new_rule comparten la ruta de guardado; solo bifurcan en «cómo se construye newContent» (contenido completo vs SEARCH/REPLACE vs archivo de reglas), y se unifican en una sola clase para evitar duplicación (
validateAndPrepareFileOperation:436). - Vista previa en streaming: el LLM va emitiendo content y el handler lo va empujando a DiffViewProvider, de modo que el usuario ve el diff cambiar mientras el LLM aún no terminó. Esto exige que
handlePartialBlockno lance errores cuando el contenido está incompleto (handlePartialBlock:35). - Fallback para output del modelo: los modelos débiles suelen añadir cercas ``` extra o no escapar entidades HTML; el handler las quita antes de guardar y llama
applyModelContentFixespara reparar entidades HTML (markdown strip:564-573). - PreToolUse hook post-aprobación: el hook corre tras la aprobación del usuario y antes de escribir a disco, de modo que si el hook rechaza, puede revertir la vista diff a su estado original (
PreToolUse hook:344-356). - Sensibilidad a ediciones del usuario: si el usuario modifica a mano el contenido en la ventana de aprobación, el handler devuelve ese userEdits al LLM y diferencia en telemetría entre agent accepted y human accepted (
user edits:380-411). - Reset por lotes del mistake counter: no se resetea al entrar a execute; solo pasa a 0 cuando
saveChangestiene éxito, para que los errores consecutivos acumulen hasta el límite del modo YOLO (reset counter:365-366).
Archivos clave
class declaration:26—export class WriteToFileToolHandler implements IFullyManagedTool,name = ClineDefaultTool.FILE_NEW; el comentario explica que reutiliza tres nombres de herramienta.handlePartialBlock:35— entrada en streaming; cuando path + content/diff están listos, abre diff view y va llamandoupdate(newContent, false).execute:97— flujo principal tras recibir el block: validación → construir newContent → aprobación → guardado → procesar userEdits.missing content error:126-150— error progresivo cuando write_to_file carece de content; incluye contexto de uso y, tras dos fallos consecutivos, cambia el mensaje.auto-approval flow:205-235— auto-aprobación va por say y no por ask; luegosetTimeoutPromise(3_500)espera a que los diagnostics lleguen.validateAndPrepareFileOperation:436— lógica de validación compartida: resolver rutas multi-workspace, revisar clineignore, decidir editType y construir newContent.diff construct:490-558— rama replace_in_file: primeroapplyModelContentFixesrepara el texto diff, luegoconstructNewFileContentlo aplica sobre originalContent; en fallo, telemetría por tipo de error.content branch:559-577— rama write_to_file: quita cercas ```, llamaapplyModelContentFixespara arreglar problemas específicos del modelo.save & track:359-377—markFileAsEditedByCline+saveChanges+ invalidar fileReadCache +trackFileContext("cline_edited").user edits:380-411— detecta modificaciones manuales del usuario en la ventana; usaapplyPatch(newContent, userEdits)para reconstruir el contenido pre-save y reporta telemetría human accepted.
Flujo de datos
Al entrar en execute, primero llama validateAndPrepareFileOperation para preparar parámetros y newContent, y luego va por aprobación y guardado. La pieza constructiva clave es la ruta de aplicación de diff que recorre replace_in_file:
// 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 usa la valla de 7 caracteres de los bloques SEARCH/REPLACE (------- SEARCH / ======= / +++++++ REPLACE) para localizar el segmento SEARCH dentro de originalContent; si falla, recurre a una estrategia fuzz (constructNewFileContent:245). Con newContent listo, se abre la vista diff, update(newContent, true) empuja el contenido final y finaliza, y scrollToFirstDiff lleva el cursor a la primera diferencia.
Tras la aprobación, saveChanges escribe a disco y devuelve userEdits / autoFormattingEdits / finalContent (saveChanges:337). Si el usuario modificó el contenido en la ventana, el handler usa applyPatch(newContent, userEdits) para reconstruir el contenido pre-save y reporta telemetría human accepted; además envía userEdits al LLM como user_feedback_diff. Al terminar, Task corre checkpointManager.saveCheckpoint() de forma unificada tras la ejecución de todas las herramientas (post-tool checkpoint:3811), fijando ese cambio como checkpoint.
Límites y fallos
- Diff application fallida: cuando el segmento SEARCH no se localiza en el archivo original, lanza error; partial block hace return silencioso para evitar jitter en streaming; block completo va por
diff_errory reporta telemetría segúnsearch_not_found/other_diff_error(diff error path:509-558). - clineignore rechaza: si la ruta coincide con reglas clineignore, devuelve directamente
clineIgnoreErrorvía pushToolResult, sin pasar por diff view (clineignore check:454-474). - Content vacío: recibir content como string vacío en write_to_file es válido (vaciar el archivo); se juzga con
== nully no con truthy, pero la ausencia de content dispara error progresivo con sugerencia según uso de contexto (missing content error:126-150). - PreToolUse hook cancela: si el hook lanza
PreToolUseHookCancellationError, se hace revertChanges + reset y se devuelvetoolDeniedsin propagar a la capa superior (PreToolUse hook:344-356). - Usuario rechaza: revertChanges restaura la vista diff a su estado original,
didRejectTool = true; los bloques de texto posteriores son skippeados por Task (revert on reject:300). - Invalidación de caché: tras guardar,
fileReadCache.delete(absolutePath.toLowerCase())evita que la próxima read_file lea contenido viejo; Task limpia toda la caché tras cualquier execute_command (cache invalidate:371). - Punto de checkpoint: el handler no guarda checkpoint por sí mismo; tras guardar, Task lo hace de forma unificada cuando se satisface
userMessageContentReady, y nuevamente cuando el usuario da feedback en la ventana de aprobación (user feedback checkpoint:1542).
Resumen
WriteToFileToolHandler unifica «sobrescritura completa» y «parche SEARCH/REPLACE» en una sola clase, compartiendo la validación previa con validateAndPrepareFileOperation y presentando los cambios al usuario en el editor diff nativo de VSCode con DiffViewProvider, soportando el reflujo de ediciones manuales. Todo output del modelo pasa por applyModelContentFixes: las cercas extras de modelos débiles y las entidades HTML sin escapar se limpian antes de escribir a disco.
Para profundizar:
- El parche multi-archivo que lo complementa:
/edit-tools/apply-patch - Cómo Task lo despacha:
/agent-loop/task-class - Base de la vista diff:
/edit-tools/diff-view-provider