Controller: 依存組み立てと Task ファクトリ
役割
Controller は extension.ts と Task の間の中間レイヤである。activate() が initialize() を走らせた後に得られる webview.controller がこれ (class Controller:70)。Task インスタンス (this.task) を保持し、StateManager、AuthService、OcaAuthService、ClineAccountService、BannerService、McpHub といった長寿命サービスを管理し、task history、telemetry、リモート設定などの横断機能を一箇所に集約する。一言で言えば、Controller は「Task のファクトリ + タスクをまたぐ共有サービスコンテナ」である。
对外に晒すメソッドは大きく三種類に分かれる。一つはタスクライフサイクル:initTask、cancelTask、clearTask、reinitExistingTaskFromId。一つは状態同期:postStateToWebview、getStateToPostToWebview。これら二つは現在の task の clineMessages、taskHistory、apiConfiguration などの大きな塊の状態を ExtensionState にまとめて webview に送る。もう一つは設定とアカウント:handleSignOut、setUserInfo、updateTelemetrySetting、toggleActModeForYoloMode など。webview が送ってくる gRPC リクエストは最終的にすべて Controller 上の具体的なメソッドに落ちる。
設計動機
- Controller は LLM ストリームを持たない:Controller は LLM 呼び出しも block 解析もツール実行も関与せず、それらは Task の中にある。Controller は「Task を作り、Task に依存を提供し、Task が終わったら回収する」ことだけを担う。このレイヤ分けにより、Task を単体テストする時に Controller をモックしなくて済む。
- Task ファクトリパターン:
initTaskが Task を作る唯一の入口 (initTask:231)。先にclearTaskで並存を防ぎ、autoApproval、shell integration timeout などの設定を読み、task lock を取得してからnew Task({...})ですべての依存をTaskParams経由で注入する。Task 自身は依存を new せず、すべて Controller から受け取る。 - postStateToWebview で状態プッシュを一元化:Controller は Task に直接 postMessage させるのではなく、
postStateToWebview: () => this.postStateToWebview()をコールバックとして Task に渡す (postStateToWebview callback:311)。これにより Task はmessageStateHandlerの clineMessages を変更するだけで、状態をExtensionStateに直列化して webview に送るのは Controller の役割となる。 - リモート設定の定期的な取得:
startRemoteConfigTimerがコンストラクタの最後で非同期に起動する (startRemoteConfigTimer:114)。即座に一度取得し、その後一時間おきに取得することで、企業ポリシー (yoloModeAllowed、allowedMCPServers 等) をホットアップデートできる。 - cancelTask の重複防止:
cancelInProgressフラグで、ユーザーがキャンセルボタンを連打しても複数回入るのを防ぐ (cancelInProgress:428)。キャンセル処理はabortTask+ stream 停止待ち + history からの再 initTask を伴い、数秒かかる可能性があるため、その間は冪等でなければならない。 - 継承ではなくコールバック注入:Controller は
updateTaskHistory、reinitExistingTaskFromId、cancelTaskなどのメソッドをアロー関数としてTaskParamsに詰め込む (TaskParams callbacks:310)。Task がthis.cancelTask()を呼ぶと Controller のthisが得られる。Task に Controller 参照を持たせるよりも結合度が下がる。
主要ファイル
class Controller:70— クラス宣言。フィールドはtask?、mcpHub、accountService、authService、stateManager、workspaceManagerなど。constructor:121— StateManager、AuthService、OcaAuthService、BannerService、McpHub を組み立て、cleanupLegacyCheckpoints、checkCliInstallationを実行する。startRemoteConfigTimer:114— リモート設定を即座に一度取得し、以降一時間おきに取得する。initTask:231— Task ファクトリの入口。設定を読み、lock を取得し、new Task して startTask または resumeTaskFromHistory を呼ぶ。tryAcquireTaskLockWithRetry:286— 同じ task が二つの Cline インスタンスで同時に走るのを防ぐ。ウィンドウ間で衝突した時はエラーにする。new Task:307— すべての依存をTaskParams経由で Task に注入する。Task 自身は横断サービスを一つも new しない。cancelTask:426— 段階的キャンセル。重入防止 → abortTask → stream 停止待ち → history からの復元または clearTask。postStateToWebview:846—getStateToPostToWebviewで状態を組み立て、sendStateUpdateで webview に送る。getStateToPostToWebview:851— 30 以上の stateKey をExtensionStateにまとめる。clineMessages、taskHistory、apiConfiguration 等を含む。clearTask:1016— task settings cache を消し、task.abortTaskを呼び、this.task = undefinedで GC に回収させる。dispose:168— remote config timer を消し、task を消し、mcpHub.dispose を呼ぶ。new Controller:19— WebviewProvider 構築時に同期的に new Controller を呼び、両者は 1:1 で紐付く。
データフロー
initTask は Controller の最も中心となるメソッドで、フローは「設定読み出し → lock 取得 → new Task → 起動」である:
// 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)
}この部分は initTask body:247 の付近にある。Task は controller: this の参照を受け取るが、実際に使うのは controller.stateManager や controller.mcpHub などの少数のフィールドだけであり、state 変更は postStateToWebview コールバックを経由する。Task が終わると cancelTask を呼ぶか、ユーザーが webview でキャンセルを押すと Controller は cancelTask に入る。まず abortTask で再帰ループを自ら終了させ、次に pWaitFor で stream が本当に止まるのを待ち (pWaitFor stream stop:449)、最後に history から新しい Task インスタンスを復元して UI に載せる (resume ボタンを見せる) か、直接 clearTask する。
境界と失敗
- task lock が取得できない:
tryAcquireTaskLockWithRetryがacquired=falseかつskipped=falseを返した時は直接例外を投げ (lock fail throw:288)、Task は作られない。skipped=trueは VSCode の単一インスタンス場合のフォールバックで、続行を許可する。 - new user 閾値:タスク履歴が 10 件以上で
isNewUserを false にする (new user threshold:261)。UI の初心者ガイドを折りたたむトリガーになる。 - autoApproval バージョン番号のインクリメント:毎回
initTaskでautoApprovalSettings.versionを +1 する (autoApproval version bump:267)。古いバージョンの自動承認設定を無効にし、ユーザーに再確認を促す。 - cancelTask の 3 秒タイムアウト:
pWaitForが stream 停止を最大 3 秒待つ (cancel timeout:456)。タイムアウト時は error を記録するだけでブロックせず、task にabandoned = trueを付けて UI への影響を消す。 - StateManager の永続化エラーは処理を止めない:
onPersistenceErrorコールバックはログを記録するだけ (onPersistenceError:127)。reInitialize()は呼ばない (呼ぶとisInitialized=falseで走行中の task を壊す) し、警告も出さない (データはメモリに安全で、次回の debounced persistence が再試行する)。 - dispose の順序:Controller の dispose は remote config timer を消してから
clearTaskし、最後にmcpHub.disposeする (dispose:168)。順序を逆にすると McpHub がまだ task から参照されている時に dispose されてクラッシュする。
まとめ
Controller は Cline が「タスクをまたぐ共有サービス」と「単一タスクの状態機械」を分ける鍵である。Controller 自身は LLM ストリームに触れず、依存の組み立て、Task のファクトリ化、状態プッシュの一元化だけを担う。webview と拡張の間でメッセージを Controller にどう届けるかは /startup/webview-bridge に、Task 自身が再帰ループをどう回すかは /agent-loop/task-class と /agent-loop/recursion に続く。
公式資料: Cline ドキュメント · README