Controller: Abhängigkeitsmontage und Task-Factory
Verantwortung
Controller ist die Zwischenschicht zwischen extension.ts und Task. Nachdem activate() initialize() durchlaufen hat, ist webview.controller genau diese Instanz (class Controller:70). Sie hält die Task-Instanz (this.task), verwaltet die langlebigen Dienste StateManager, AuthService, OcaAuthService, ClineAccountService, BannerService und McpHub und bündelt Querfunktionen wie Task-History, Telemetry und Remote-Konfiguration. In einem Satz: Controller ist „Task-Factory plus Container für shared Services über Task-Grenzen hinweg".
Die nach außen angebotenen Methoden lassen sich grob in drei Gruppen teilen. Erstens die Task-Lebensdauer: initTask, cancelTask, clearTask, reinitExistingTaskFromId. Zweitens die Zustandssynchronisation: postStateToWebview und getStateToPostToWebview verpacken clineMessages, taskHistory, apiConfiguration und viele weitere Felder des laufenden Task in ein ExtensionState und schicken es an die webview. Drittens Einstellungen und Konto: handleSignOut, setUserInfo, updateTelemetrySetting, toggleActModeForYoloMode und weitere. Alle gRPC-Anfragen aus der webview landen schließlich auf einer konkreten Controller-Methode.
Entwurfsmotivation
- Controller hält keinen LLM-Stream: Controller kümmert sich nicht um LLM-Aufrufe, nicht um Block-Parsing und nicht um Werkzeugausführung – all das lebt in Task. Controller ist ausschließlich für „Task erzeugen, Abhängigkeiten bereitstellen, Task nach Abschluss abräumen" zuständig. Diese Schichtung erlaubt es, Task in Unit-Tests zu prüfen, ohne Controller mocken zu müssen.
- Task-Factory-Muster:
initTaskist der einzige Ort, an dem ein Task erzeugt wird (initTask:231). Zuerst wirdclearTaskaufgerufen, um Parallelität zu vermeiden, danach autoApproval, Shell-Integration-Timeout und weitere Einstellungen gelesen, dann der task lock erworben, und schließlichnew Task({...})mit allen Abhängigkeiten überTaskParamsinjiziert. Task erzeugt seine Abhängigkeiten nicht selbst, sondern erhält sie ausschließlich von Controller. - postStateToWebview als zentrale Zustandspush-Methode: Controller erlaubt Task nicht, direkt postMessage aufzurufen. Stattdessen wird
postStateToWebview: () => this.postStateToWebview()als Callback an Task übergeben (postStateToWebview callback:311). Task ändert ausschließlich clineMessages immessageStateHandler; Controller ist für die Serialisierung inExtensionStateund den Versand an die webview verantwortlich. - Remote-Konfiguration per Timer:
startRemoteConfigTimerwird am Ende des Konstruktors asynchron gestartet (startRemoteConfigTimer:114); sofort einmal und danach stündlich, sodass Unternehmensrichtlinien (yoloModeAllowed, allowedMCPServers usw.) hot aktualisiert werden können. - cancelTask verhindert Wiederholung: Das Flag
cancelInProgressverhindert, dass bei mehrfachem Klick auf Abbrechen der Code mehrfach eintritt (cancelInProgress:428). Der AbbruchlaufabortTask+ Warten auf Stream-Stopp + erneutesinitTaskaus der History kann Sekunden dauern; in dieser Zeit muss der Vorgang idempotent bleiben. - Callback-Injektion statt Vererbung: Controller steckt
updateTaskHistory,reinitExistingTaskFromId,cancelTaskund weitere Methoden als Arrow-Funktionen inTaskParams(TaskParams callbacks:310). Wenn Task diese überthis.cancelTask()aufruft, istthisan Controller gebunden. Das ist lockerer gekoppelt als eine direkte Controller-Referenz in Task.
Schlüsseldateien
class Controller:70— Klassendeklaration; Felder umfassentask?,mcpHub,accountService,authService,stateManager,workspaceManager.constructor:121— Montiert StateManager, AuthService, OcaAuthService, BannerService, McpHub; ruftcleanupLegacyCheckpointsundcheckCliInstallationauf.startRemoteConfigTimer:114— Holt sofort einmal die Remote-Konfiguration, danach stündlich.initTask:231— Task-Factory-Einstieg; liest Einstellungen, übernimmt lock, instanziiert Task und ruft startTask bzw. resumeTaskFromHistory auf.tryAcquireTaskLockWithRetry:286— Verhindert, dass derselbe Task in zwei Cline-Instanzen gleichzeitig läuft; bei fensterübergreifendem Konflikt wird ein Fehler geworfen.new Task:307— Injiziert alle Abhängigkeiten überTaskParams; Task selbst instanziiert keinen Quer-Service.cancelTask:426— Abbruch in Phasen: Reentrancy-Schutz → abortTask → Stream-Stopp abwarten → aus History wiederherstellen oder clearTask.postStateToWebview:846— RuftgetStateToPostToWebviewauf und schickt den Zustand persendStateUpdatean die webview.getStateToPostToWebview:851— Setzt über 30 stateKeys zumExtensionStatezusammen, darunter clineMessages, taskHistory, apiConfiguration.clearTask:1016— Leert den task settings cache, rufttask.abortTaskauf und setztthis.task = undefined, damit GC aufräumen kann.dispose:168— Leert den Remote-Config-Timer, dann Task, dannmcpHub.dispose.new Controller:19— WebviewProvider konstruiert synchron einen Controller; beide sind 1:1 verknüpft.
Datenfluss
initTask ist die zentrale Methode des Controllers. Der Ablauf lautet „Einstellungen lesen → lock erwerben → new Task → starten":
// 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")
// ...more settings reads
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(),
// ...plus shellIntegrationTimeout, terminalReuseEnabled, cwd, taskId usw.
})
if (historyItem) {
this.task.resumeTaskFromHistory()
} else if (task || images || files) {
this.task.startTask(task, images, files)
}Dieser Block liegt in der Nähe von initTask body:247. Achtung: Task erhält zwar eine controller: this-Referenz, nutzt daraus aber nur wenige Felder wie controller.stateManager und controller.mcpHub; Zustandsänderungen laufen stets über den postStateToWebview-Callback. Nach Abschluss ruft Task cancelTask auf, oder der Benutzer klickt in der webview auf Abbrechen – Controller betritt dann cancelTask: zuerst abortTask, damit sich die Rekursionsschleife selbst beendet; danach pWaitFor, um darauf zu warten, dass der Stream wirklich stoppt (pWaitFor stream stop:449); schließlich wird aus der History eine neue Task-Instanz auf der UI wiederhergestellt (damit der Benutzer den Resume-Button sieht), oder es folgt ein direktes clearTask.
Grenzen und Fehler
- task lock nicht erhältlich: Gibt
tryAcquireTaskLockWithRetryacquired=falseundskipped=falsezurück, wird direkt ein Fehler geworfen (lock fail throw:288); Task wird nicht erzeugt.skipped=trueist der Rückfallfall für die VSCode-Single-Instance und erlaubt das Fortsetzen. - new-user-Schwelle: Ab zehn Einträgen in der Task-History wird
isNewUserauf false gesetzt (new user threshold:261); das blendet die Onboarding-Hinweise in der UI aus. - autoApproval-Versionsinkrement: Bei jedem
initTaskwirdautoApprovalSettings.versionum 1 erhöht (autoApproval version bump:267), sodass alte Auto-Approval-Konfigurationen ungültig werden und der Benutzer erneut bestätigen muss. - cancelTask 3-Sekunden-Timeout:
pWaitForwartet maximal 3 Sekunden auf Stream-Stopp (cancel timeout:456); bei Timeout wird nur ein Fehler geloggt, nicht blockiert; der Task erhältabandoned = true, sodass er die UI nicht weiter beeinflusst. - StateManager-Persistenzfehler brechen nicht ab: Der
onPersistenceError-Callback protokolliert nur (onPersistenceError:127); er ruft nichtreInitialize()auf (das würdeisInitialized=falsesetzen und den laufenden Task kollabieren lassen) und zeigt keine Warnung (die Daten sind im Speicher sicher, die nächste debounced-Persistenz versucht es erneut). - dispose-Reihenfolge: Controller dispose räumt zuerst den Remote-Config-Timer ab, dann
clearTask, dannmcpHub.dispose(dispose:168). Diese Reihenfolge darf nicht vertauscht werden, sonst wird McpHub freigegeben, während ein Task noch darauf verweist, und der Code stürzt ab.
Zusammenfassung
Controller ist Clines zentraler Trennpunkt zwischen „Shared Services über Task-Grenzen hinweg" und „Zustandsautomat eines einzelnen Task". Er selbst berührt den LLM-Stream nicht, sondern montiert nur Abhängigkeiten, fabriziert Tasks und bündelt Zustands-Push. Um weiterzulesen, wie webview und Extension Nachrichten an Controller senden, siehe /startup/webview-bridge; wie Task selbst die Rekursionsschleife betreibt, siehe /agent-loop/task-class und /agent-loop/recursion.
Siehe offizielle Dokumentation: Cline-Dokumentation · README