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