Skip to content

Controller: ensamblado de dependencias y fábrica de Task

源码版本v4.0.10

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: initTask es la única entrada que crea un Task (initTask:231). Primero hace clearTask para evitar coexistencia, lee ajustes como autoApproval y shell integration timeout, adquiere el task lock, y por último hace new Task({...}) inyectando todas las dependencias vía TaskParams. Task no instancia sus dependencias; todas le llegan desde Controller.
  • postStateToWebview centraliza el push de estado: Controller no deja que Task haga postMessage directamente, sino que le pasa como callback postStateToWebview: () => this.postStateToWebview() (postStateToWebview callback:311). Task solo modifica clineMessages en messageStateHandler; Controller se encarga de serializar el estado a ExtensionState y empujarlo al webview.
  • Polling temporizado de configuración remota: startRemoteConfigTimer se 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.
  • cancelTask idempotente: el flag cancelInProgress evita entradas múltiples cuando el usuario machaca el botón de cancelar (cancelInProgress:428). El flujo de cancelación debe abortTask + esperar a que el stream se detenga + reiniciar initTask desde 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, cancelTask y otros como funciones flecha dentro de TaskParams (TaskParams callbacks:310). Cuando Task invoca this.cancelTask(), recibe el this de 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; ejecuta cleanupLegacyCheckpoints y checkCliInstallation.
  • 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, hace new Task, y llama a startTask o resumeTaskFromHistory.
  • 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ía TaskParams; Task no instancia ningún servicio transversal.
  • cancelTask:426 — cancelación por fases: anti-reentrada → abortTask → esperar stream → restaurar desde history o clearTask.
  • postStateToWebview:846 — llama a getStateToPostToWebview para construir el estado, y luego a sendStateUpdate para empujarlo al webview.
  • getStateToPostToWebview:851 — compone ExtensionState a partir de 30+ stateKey, incluyendo clineMessages, taskHistory, apiConfiguration.
  • clearTask:1016 — limpia la caché de task settings, llama task.abortTask y asigna this.task = undefined para que el GC lo recoja.
  • dispose:168 — limpia el timer de configuración remota, limpia task, y hace mcpHub.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»:

typescript
// 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 tryAcquireTaskLockWithRetry devuelve acquired=false y skipped=false, se lanza directamente (lock fail throw:288) y el Task no se crea. skipped=true es el fallback para 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, isNewUser se pone a falso (new user threshold:261), lo que dispara el colapso de la guía de onboarding en la UI.
  • Versionado de autoApproval: cada initTask incrementa en 1 el autoApprovalSettings.version (autoApproval version bump:267), invalidando las configuraciones de auto-aprobación anteriores y forzando al usuario a confirmar de nuevo.
  • cancelTask con timeout de 3 s: pWaitFor espera 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 como abandoned = true para que ya no afecte a la UI.
  • Errores de persistencia en StateManager no interrumpen: el callback onPersistenceError solo registra el log (onPersistenceError:127); no llama a reInitialize() (pondría isInitialized=false y 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: el dispose de Controller primero limpia el timer de configuración remota, luego clearTask, y por último mcpHub.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.

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