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