Skip to content

Controller: 依存組み立てと Task ファクトリ

源码版本v4.0.10

役割

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 のファクトリ + タスクをまたぐ共有サービスコンテナ」である。

对外に晒すメソッドは大きく三種類に分かれる。一つはタスクライフサイクル:initTaskcancelTaskclearTaskreinitExistingTaskFromId。一つは状態同期:postStateToWebviewgetStateToPostToWebview。これら二つは現在の task の clineMessages、taskHistory、apiConfiguration などの大きな塊の状態を ExtensionState にまとめて webview に送る。もう一つは設定とアカウント:handleSignOutsetUserInfoupdateTelemetrySettingtoggleActModeForYoloMode など。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 は updateTaskHistoryreinitExistingTaskFromIdcancelTask などのメソッドをアロー関数として TaskParams に詰め込む (TaskParams callbacks:310)。Task が this.cancelTask() を呼ぶと Controller の this が得られる。Task に Controller 参照を持たせるよりも結合度が下がる。

主要ファイル

  • class Controller:70 — クラス宣言。フィールドは task?mcpHubaccountServiceauthServicestateManagerworkspaceManager など。
  • constructor:121 — StateManager、AuthService、OcaAuthService、BannerService、McpHub を組み立て、cleanupLegacyCheckpointscheckCliInstallation を実行する。
  • 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:846getStateToPostToWebview で状態を組み立て、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 → 起動」である:

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")
// ...更多 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.stateManagercontroller.mcpHub などの少数のフィールドだけであり、state 変更は postStateToWebview コールバックを経由する。Task が終わると cancelTask を呼ぶか、ユーザーが webview でキャンセルを押すと Controller は cancelTask に入る。まず abortTask で再帰ループを自ら終了させ、次に pWaitFor で stream が本当に止まるのを待ち (pWaitFor stream stop:449)、最後に history から新しい Task インスタンスを復元して UI に載せる (resume ボタンを見せる) か、直接 clearTask する。

境界と失敗

  • task lock が取得できない:tryAcquireTaskLockWithRetryacquired=false かつ skipped=false を返した時は直接例外を投げ (lock fail throw:288)、Task は作られない。skipped=true は VSCode の単一インスタンス場合のフォールバックで、続行を許可する。
  • new user 閾値:タスク履歴が 10 件以上で isNewUser を false にする (new user threshold:261)。UI の初心者ガイドを折りたたむトリガーになる。
  • autoApproval バージョン番号のインクリメント:毎回 initTaskautoApprovalSettings.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