Skip to content

Controller : assemblage des dépendances et fabrique de Task

源码版本v4.0.10

Responsabilités

Controller est la couche intermédiaire entre extension.ts et Task. Après que activate() a exécuté initialize(), c'est lui que l'on récupère sous webview.controller (class Controller:70). Il porte l'instance Task (this.task), gère les services à longue durée de vie StateManager, AuthService, OcaAuthService, ClineAccountService, BannerService et McpHub, et rassemble les transverses task history, telemetry, configuration distante. En une phrase : Controller est la « fabrique de Task + conteneur de services partagés entre tâches ».

Les méthodes qu'il expose se répartissent grosso modo en trois familles. D'abord le cycle de vie des tâches : initTask, cancelTask, clearTask, reinitExistingTaskFromId. Ensuite la synchronisation d'état : postStateToWebview et getStateToPostToWebview, qui empalettent clineMessages, taskHistory, apiConfiguration et tout un paquet d'autres états du task courant dans un ExtensionState poussé vers la webview. Enfin, paramètres et compte : handleSignOut, setUserInfo, updateTelemetrySetting, toggleActModeForYoloMode, etc. Les requêtes gRPC émises par la webview finissent toutes par atterrir sur une méthode précise du controller.

Motivation de conception

  • Controller ne porte pas le flux LLM : Controller ne gère ni l'appel LLM, ni le parsing des blocks, ni l'exécution des outils — tout cela vit dans Task. Controller se borne à « créer Task, fournir ses dépendances, récupérer Task une fois terminé ». Cette séparation permet de tester Task en isolation sans avoir à mocker Controller.
  • Patron fabrique pour Task : initTask est le seul point d'entrée de création de Task (initTask:231). Il commence par clearTask pour éviter la coexistence, lit les paramètres autoApproval et shell integration timeout, acquiert le task lock, puis new Task({...}) injecte toutes les dépendances via TaskParams. Task ne new pas ses propres dépendances, il ne reçoit que ce que Controller lui passe.
  • postStateToWebview centralise la poussée d'état : Controller ne laisse pas Task appeler postMessage directement ; il passe postStateToWebview: () => this.postStateToWebview() comme callback à Task (postStateToWebview callback:311). Task se contente de modifier clineMessages dans messageStateHandler, et Controller sérialise l'état en ExtensionState pour la webview.
  • Tirage périodique de la configuration distante : startRemoteConfigTimer est lancé de façon asynchrone à la fin du constructeur (startRemoteConfigTimer:114), tire une fois immédiatement puis toutes les heures, afin que les stratégies d'entreprise (yoloModeAllowed, allowedMCPServers, etc.) puissent se mettre à jour à chaud.
  • cancelTask anti-double : le flag cancelInProgress évite de rentrer plusieurs fois dans la procédure quand l'utilisateur martèle le bouton d'annulation (cancelInProgress:428). Le flux d'annulation doit abortTask + attendre l'arrêt du stream + réinitier un task depuis l'historique, ce qui peut prendre plusieurs secondes — pendant ce temps il faut être idempotent.
  • Injection de callbacks plutôt qu'héritage : Controller glisse updateTaskHistory, reinitExistingTaskFromId, cancelTask sous forme de fonctions fléchées dans TaskParams (TaskParams callbacks:310). Quand Task appelle this.cancelTask(), il récupère en réalité le this de Controller. Couplage plus lâche que de faire porter une référence Controller à Task.

Fichiers clés

  • class Controller:70 — déclaration de la classe, champs task?, mcpHub, accountService, authService, stateManager, workspaceManager.
  • constructor:121 — assemble StateManager, AuthService, OcaAuthService, BannerService, McpHub, exécute cleanupLegacyCheckpoints et checkCliInstallation.
  • startRemoteConfigTimer:114 — tire une fois la configuration distante, puis toutes les heures.
  • initTask:231 — entrée de la fabrique Task : lit les settings, prend le lock, new Task, appelle startTask ou resumeTaskFromHistory.
  • tryAcquireTaskLockWithRetry:286 — empêche deux instances Cline de lancer le même task en parallèle, lève une erreur en cas de conflit cross-fenêtre.
  • new Task:307 — injecte toutes les dépendances via TaskParams, Task ne new lui-même aucun service transverse.
  • cancelTask:426 — annulation par étapes : anti-réentrée → abortTask → attendre l'arrêt du stream → restoration depuis l'historique ou clearTask.
  • postStateToWebview:846 — appelle getStateToPostToWebview pour construire l'état, puis sendStateUpdate pour le pousser à la webview.
  • getStateToPostToWebview:851 — assemble 30+ stateKeys dans ExtensionState, dont clineMessages, taskHistory, apiConfiguration.
  • clearTask:1016 — vide le cache des task settings, appelle task.abortTask, met this.task = undefined pour permettre le GC.
  • dispose:168 — stoppe le remote config timer, clear task, mcpHub.dispose.
  • new Controller:19 — WebviewProvider new Controller à la construction, les deux sont liés 1:1.

Flux de données

initTask est la méthode centrale de Controller, son flux est « lire les settings → prendre le lock → new Task → démarrer » :

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")
// ...更多 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(),
    // ...还有 shellIntegrationTimeout、terminalReuseEnabled、cwd、taskId 等
})

if (historyItem) {
    this.task.resumeTaskFromHistory()
} else if (task || images || files) {
    this.task.startTask(task, images, files)
}

Ce bloc se trouve près de initTask body:247. À noter : Task reçoit controller: this en référence, mais n'utilise en pratique que quelques champs comme controller.stateManager et controller.mcpHub, et toute modification d'état passe par le callback postStateToWebview. Une fois Task terminé, il appelle cancelTask, ou l'utilisateur clique sur annuler dans la webview, et Controller entre dans cancelTask : d'abord abortTask pour faire sortir la boucle récursive, puis pWaitFor attend que le stream soit réellement arrêté (pWaitFor stream stop:449), et enfin on restaure depuis l'historique une nouvelle instance Task exposée à l'UI (pour que l'utilisateur voie le bouton resume), ou l'on clearTask directement.

Limites et échecs

  • task lock non acquis : quand tryAcquireTaskLockWithRetry renvoie acquired=false et skipped=false, on lève directement une erreur (lock fail throw:288), Task n'est pas créé. skipped=true est le repli pour le scénario mono-instance VSCode, qui autorise la poursuite.
  • Seuil new user : quand l'historique de tâches atteint 10 entrées, isNewUser passe à faux (new user threshold:261), ce qui replie l'onboarding dans l'UI.
  • Incrémentation du numéro de version autoApproval : chaque initTask incrémente autoApprovalSettings.version de 1 (autoApproval version bump:267), ce qui invalide les anciennes configurations d'auto-approbation et force l'utilisateur à reconfirmer.
  • Timeout de cancelTask à 3 secondes : pWaitFor attend au plus 3 s l'arrêt du stream (cancel timeout:456). En cas de timeout, on log une error sans bloquer, et l'on marque le task abandoned = true pour qu'il n'affecte plus l'UI.
  • Les erreurs de persistance StateManager n'interrompent pas : le callback onPersistenceError se contente de logger (onPersistenceError:127). Il n'appelle pas reInitialize() (cela mettrait isInitialized=false et casserait le task en cours), n'affiche pas non plus de warning (les données sont sûres en mémoire, la prochaine persistence debounced réessaiera).
  • Ordre du dispose : le dispose de Controller stoppe d'abord le remote config timer, puis clearTask, puis mcpHub.dispose (dispose:168). L'ordre ne doit pas être inversé, sinon McpHub se retrouve disposed alors qu'un task l'utilise encore et crash.

Résumé

Controller est ce qui permet à Cline de séparer « services partagés entre tâches » et « machine à états d'une tâche ». Il ne touche pas au flux LLM ; il se borne à assembler les dépendances, à factory les Task, et à pousser l'état de façon centralisée. Pour voir comment les messages de la webview sont acheminés vers Controller, voir /startup/webview-bridge ; pour voir comment Task lui-même exécute sa boucle récursive, voir /agent-loop/task-class et /agent-loop/recursion.

Voir la documentation officielle : documentation Cline · README.