Controller : assemblage des dépendances et fabrique de Task
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 :
initTaskest le seul point d'entrée de création de Task (initTask:231). Il commence parclearTaskpour éviter la coexistence, lit les paramètres autoApproval et shell integration timeout, acquiert le task lock, puisnew Task({...})injecte toutes les dépendances viaTaskParams. 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 dansmessageStateHandler, et Controller sérialise l'état enExtensionStatepour la webview. - Tirage périodique de la configuration distante :
startRemoteConfigTimerest 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 doitabortTask+ 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,cancelTasksous forme de fonctions fléchées dansTaskParams(TaskParams callbacks:310). Quand Task appellethis.cancelTask(), il récupère en réalité lethisde 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, champstask?,mcpHub,accountService,authService,stateManager,workspaceManager.constructor:121— assemble StateManager, AuthService, OcaAuthService, BannerService, McpHub, exécutecleanupLegacyCheckpointsetcheckCliInstallation.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 viaTaskParams, 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— appellegetStateToPostToWebviewpour construire l'état, puissendStateUpdatepour le pousser à la webview.getStateToPostToWebview:851— assemble 30+ stateKeys dansExtensionState, dont clineMessages, taskHistory, apiConfiguration.clearTask:1016— vide le cache des task settings, appelletask.abortTask, metthis.task = undefinedpour 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 » :
// 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
tryAcquireTaskLockWithRetryrenvoieacquired=falseetskipped=false, on lève directement une erreur (lock fail throw:288), Task n'est pas créé.skipped=trueest 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,
isNewUserpasse à faux (new user threshold:261), ce qui replie l'onboarding dans l'UI. - Incrémentation du numéro de version autoApproval : chaque
initTaskincrémenteautoApprovalSettings.versionde 1 (autoApproval version bump:267), ce qui invalide les anciennes configurations d'auto-approbation et force l'utilisateur à reconfirmer. - Timeout de cancelTask à 3 secondes :
pWaitForattend 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 taskabandoned = truepour qu'il n'affecte plus l'UI. - Les erreurs de persistance StateManager n'interrompent pas : le callback
onPersistenceErrorse contente de logger (onPersistenceError:127). Il n'appelle pasreInitialize()(cela mettraitisInitialized=falseet 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, puismcpHub.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.