Skip to content

Controller: Abhängigkeitsmontage und Task-Factory ​

源码版本v4.0.10

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: initTask ist der einzige Ort, an dem ein Task erzeugt wird (initTask:231). Zuerst wird clearTask aufgerufen, um Parallelität zu vermeiden, danach autoApproval, Shell-Integration-Timeout und weitere Einstellungen gelesen, dann der task lock erworben, und schließlich new Task({...}) mit allen Abhängigkeiten über TaskParams injiziert. 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 im messageStateHandler; Controller ist für die Serialisierung in ExtensionState und den Versand an die webview verantwortlich.
  • Remote-Konfiguration per Timer: startRemoteConfigTimer wird 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 cancelInProgress verhindert, dass bei mehrfachem Klick auf Abbrechen der Code mehrfach eintritt (cancelInProgress:428). Der Abbruchlauf abortTask + Warten auf Stream-Stopp + erneutes initTask aus der History kann Sekunden dauern; in dieser Zeit muss der Vorgang idempotent bleiben.
  • Callback-Injektion statt Vererbung: Controller steckt updateTaskHistory, reinitExistingTaskFromId, cancelTask und weitere Methoden als Arrow-Funktionen in TaskParams (TaskParams callbacks:310). Wenn Task diese über this.cancelTask() aufruft, ist this an Controller gebunden. Das ist lockerer gekoppelt als eine direkte Controller-Referenz in Task.

Schlüsseldateien ​

  • class Controller:70 — Klassendeklaration; Felder umfassen task?, mcpHub, accountService, authService, stateManager, workspaceManager.
  • constructor:121 — Montiert StateManager, AuthService, OcaAuthService, BannerService, McpHub; ruft cleanupLegacyCheckpoints und checkCliInstallation auf.
  • 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 über TaskParams; Task selbst instanziiert keinen Quer-Service.
  • cancelTask:426 — Abbruch in Phasen: Reentrancy-Schutz → abortTask → Stream-Stopp abwarten → aus History wiederherstellen oder clearTask.
  • postStateToWebview:846 — Ruft getStateToPostToWebview auf und schickt den Zustand per sendStateUpdate an die webview.
  • getStateToPostToWebview:851 — Setzt über 30 stateKeys zum ExtensionState zusammen, darunter clineMessages, taskHistory, apiConfiguration.
  • clearTask:1016 — Leert den task settings cache, ruft task.abortTask auf und setzt this.task = undefined, damit GC aufräumen kann.
  • dispose:168 — Leert den Remote-Config-Timer, dann Task, dann mcpHub.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":

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")
// ...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 tryAcquireTaskLockWithRetry acquired=false und skipped=false zurück, wird direkt ein Fehler geworfen (lock fail throw:288); Task wird nicht erzeugt. skipped=true ist 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 isNewUser auf false gesetzt (new user threshold:261); das blendet die Onboarding-Hinweise in der UI aus.
  • autoApproval-Versionsinkrement: Bei jedem initTask wird autoApprovalSettings.version um 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: pWaitFor wartet maximal 3 Sekunden auf Stream-Stopp (cancel timeout:456); bei Timeout wird nur ein Fehler geloggt, nicht blockiert; der Task erhält abandoned = true, sodass er die UI nicht weiter beeinflusst.
  • StateManager-Persistenzfehler brechen nicht ab: Der onPersistenceError-Callback protokolliert nur (onPersistenceError:127); er ruft nicht reInitialize() auf (das würde isInitialized=false setzen 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, dann mcpHub.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