Controller: ensamblado de dependencias y fábrica de Task
Responsabilidades
Controller es la capa intermedia entre extension.ts y Task. Cuando activate() termina de ejecutar initialize(), lo que se obtiene en webview.controller es precisamente él (class Controller:70). Mantiene la instancia de Task (this.task), gestiona varios servicios de ciclo de vida largo (StateManager, AuthService, OcaAuthService, ClineAccountService, BannerService, McpHub) y centraliza capacidades transversales como task history, telemetry y configuración remota. En una frase: Controller es «la fábrica de Task + el contenedor de servicios compartidos entre tareas».
Sus métodos públicos pueden agruparse en tres categorías. La primera, ciclo de vida de tarea: initTask, cancelTask, clearTask, reinitExistingTaskFromId. La segunda, sincronización de estado: postStateToWebview y getStateToPostToWebview, que empaquetan clineMessages, taskHistory, apiConfiguration y un gran volumen de estado en un ExtensionState y lo empujan al webview. La tercera, configuración y cuentas: handleSignOut, setUserInfo, updateTelemetrySetting, toggleActModeForYoloMode, etc. Las solicitudes gRPC que llegan del webview terminan todas en algún método concreto del controller.
Motivación de diseño
- Controller no retiene el flujo LLM: Controller no se involucra en la llamada al LLM, ni en el parseo de bloques, ni en la ejecución de herramientas — todo eso vive en Task. Controller solo se encarga de «crear el Task, inyectarle dependencias, y recogerlo cuando termina». Esta separación permite testear Task en unidad sin tener que mockear Controller.
- Patrón fábrica para Task:
initTaskes la única entrada que crea un Task (initTask:231). Primero haceclearTaskpara evitar coexistencia, lee ajustes comoautoApprovalyshell integration timeout, adquiere eltask lock, y por último hacenew Task({...})inyectando todas las dependencias víaTaskParams. Task no instancia sus dependencias; todas le llegan desde Controller. postStateToWebviewcentraliza el push de estado: Controller no deja que Task hagapostMessagedirectamente, sino que le pasa como callbackpostStateToWebview: () => this.postStateToWebview()(postStateToWebview callback:311). Task solo modificaclineMessagesenmessageStateHandler; Controller se encarga de serializar el estado aExtensionStatey empujarlo al webview.- Polling temporizado de configuración remota:
startRemoteConfigTimerse arranca de forma asíncrona al final del constructor (startRemoteConfigTimer:114), tira una vez al instante y luego cada hora, para que las políticas de empresa (yoloModeAllowed, allowedMCPServers, etc.) puedan actualizarse en caliente. cancelTaskidempotente: el flagcancelInProgressevita entradas múltiples cuando el usuario machaca el botón de cancelar (cancelInProgress:428). El flujo de cancelación debeabortTask+ esperar a que el stream se detenga + reiniciarinitTaskdesde el historial, lo que puede tardar varios segundos; durante ese intervalo, la operación tiene que ser idempotente.- Inyección por callback en vez de herencia: Controller pasa
updateTaskHistory,reinitExistingTaskFromId,cancelTasky otros como funciones flecha dentro deTaskParams(TaskParams callbacks:310). Cuando Task invocathis.cancelTask(), recibe elthisde Controller. Es un acoplamiento más flojo que retener una referencia directa a Controller.
Archivos clave
class Controller:70— declaración de la clase. Campos:task?,mcpHub,accountService,authService,stateManager,workspaceManager.constructor:121— ensambla StateManager, AuthService, OcaAuthService, BannerService, McpHub; ejecutacleanupLegacyCheckpointsycheckCliInstallation.startRemoteConfigTimer:114— tira una vez de la configuración remota, y luego cada hora.initTask:231— entrada de la fábrica de Task: lee ajustes, adquiere el lock, hacenew Task, y llama astartTaskoresumeTaskFromHistory.tryAcquireTaskLockWithRetry:286— evita que la misma task corra en dos instancias de Cline a la vez; ante conflicto entre ventanas, lanza error.new Task:307— inyecta todas las dependencias en Task víaTaskParams; Task no instancia ningún servicio transversal.cancelTask:426— cancelación por fases: anti-reentrada →abortTask→ esperar stream → restaurar desde history oclearTask.postStateToWebview:846— llama agetStateToPostToWebviewpara construir el estado, y luego asendStateUpdatepara empujarlo al webview.getStateToPostToWebview:851— componeExtensionStatea partir de 30+stateKey, incluyendo clineMessages, taskHistory, apiConfiguration.clearTask:1016— limpia la caché de task settings, llamatask.abortTasky asignathis.task = undefinedpara que el GC lo recoja.dispose:168— limpia el timer de configuración remota, limpia task, y hacemcpHub.dispose.new Controller:19— WebviewProvider construye un Controller sincrónicamente; ambos están atados 1:1.
Flujo de datos
initTask es el método central de Controller. Su flujo es «leer ajustes → adquirir lock → new Task → arrancar»:
// apps/vscode/src/core/controller/index.ts
await this.clearTask() // ensures that an existing task doesn't exist before starting a new one
const autoApprovalSettings = this.stateManager.getGlobalSettingsKey("autoApprovalSettings")
const shellIntegrationTimeout = this.stateManager.getGlobalSettingsKey("shellIntegrationTimeout")
// ...más lectura de settings
const taskId = historyItem?.id || Date.now().toString()
// Acquire task lock
const lockResult: FolderLockWithRetryResult = await tryAcquireTaskLockWithRetry(taskId)
if (!lockResult.acquired && !lockResult.skipped) {
throw new Error(errorMessage) // Prevents task initialization
}
this.task = new Task({
controller: this,
mcpHub: this.mcpHub,
updateTaskHistory: (historyItem) => this.updateTaskHistory(historyItem),
postStateToWebview: () => this.postStateToWebview(),
reinitExistingTaskFromId: (taskId) => this.reinitExistingTaskFromId(taskId),
cancelTask: () => this.cancelTask(),
// ...más: shellIntegrationTimeout, terminalReuseEnabled, cwd, taskId, etc.
})
if (historyItem) {
this.task.resumeTaskFromHistory()
} else if (task || images || files) {
this.task.startTask(task, images, files)
}Este bloque está cerca de initTask body:247. Nótese que Task recibe controller: this como referencia, pero en la práctica solo usa unos pocos campos (controller.stateManager, controller.mcpHub, …) y las modificaciones de estado siempre se canalizan a través del callback postStateToWebview. Cuando Task termina, o cuando el usuario pulsa cancelar en el webview, Controller entra en cancelTask: primero abortTask para que el bucle recursivo se retire, luego pWaitFor a que el stream se detenga de verdad (pWaitFor stream stop:449), y finalmente construye una nueva instancia de Task a partir del historial (para que el usuario vea el botón de resume) o hace clearTask directo.
Límites y fallos
- No se obtiene el task lock: si
tryAcquireTaskLockWithRetrydevuelveacquired=falseyskipped=false, se lanza directamente (lock fail throw:288) y el Task no se crea.skipped=truees elfallbackpara el escenario de instancia única de VSCode, en el que se permite continuar. - Umbral de nuevo usuario: cuando el historial de tareas llega a 10 o más,
isNewUserse pone a falso (new user threshold:261), lo que dispara el colapso de la guía de onboarding en la UI. - Versionado de
autoApproval: cadainitTaskincrementa en 1 elautoApprovalSettings.version(autoApproval version bump:267), invalidando las configuraciones de auto-aprobación anteriores y forzando al usuario a confirmar de nuevo. cancelTaskcon timeout de 3 s:pWaitForespera a que el stream se detenga como máximo 3 segundos (cancel timeout:456); si expira, solo registra el error y no bloquea. Luego marca la task comoabandoned = truepara que ya no afecte a la UI.- Errores de persistencia en StateManager no interrumpen: el callback
onPersistenceErrorsolo registra el log (onPersistenceError:127); no llama areInitialize()(pondríaisInitialized=falsey rompería la task en curso), ni muestra advertencia (los datos están a salvo en memoria y la próxima persistencia debounced reintentará). - Orden de
dispose: eldisposede Controller primero limpia el timer de configuración remota, luegoclearTask, y por últimomcpHub.dispose(dispose:168). El orden no puede invertirse: si McpHub se disposea mientras task todavía lo referencia, se rompe.
Resumen
Controller es la pieza clave con la que Cline separa los «servicios compartidos entre tareas» del «estado de una tarea concreta». Él mismo no toca el flujo LLM; solo ensambla dependencias, fábrica Task y centraliza el push de estado. Para ver cómo webview y extensión enrutan los mensajes hacia Controller, ver /startup/webview-bridge; para ver cómo Task ejecuta su bucle recursivo, ver /agent-loop/task-class y /agent-loop/recursion.