Gemini CLI の二つの顔:React/Ink ターミナル UI とプログラマブル SDK
前提知識
- ›第1回:アーキテクチャとナビゲーションガイド
- ›第2回:エージェントループ
- ›React の基礎知識
- ›async iterable の理解
Gemini CLI の二つの顔:React/Ink ターミナル UI とプログラマブル SDK
Gemini CLI のアーキテクチャは、core と CLI の明確な分離、型付きイベントストリーム、そしてプロトコルインターフェースという設計原則のもとに成り立っています。この構造により、同一のバックエンドを基盤としながら、まったく異なる2つのインターフェースが実現されています。インタラクティブなターミナル UI は、コンテキストプロバイダー、キーボードハンドリング、ストリーミングコンテンツ表示を備えた本格的な React アプリケーションをターミナル上でレンダリングします。一方 SDK は、同じエンジンをプログラマブル API としてラップし、他のツールへの組み込みを可能にします。本記事では、この2つのインターフェースを詳しく見ていきましょう。
React/Ink インタラクティブ UI
Gemini CLI のターミナル UI は、React コンポーネントをターミナル出力にマッピングするライブラリ Ink を使った本格的な React アプリケーションです。UI はレイジーローディングで読み込まれます。第1回で触れたように、gemini.tsx の 167〜185 行目にある startInteractiveUI は、重量のあるモジュールを動的インポートで遅延読み込みしています。
export async function startInteractiveUI(/* ... */) {
const { startInteractiveUI: doStartUI } = await import('./interactiveCli.js');
await doStartUI(config, settings, startupWarnings, workspaceRoot, ...);
}
このアプローチにより、非インタラクティブパスの起動を高速に保てます。インタラクティブモードが起動すると、interactiveCli.tsx の startInteractiveUI がマウスイベントの有効化、キーマッチャーの読み込み、コンソール出力のパッチ適用、stdio ストリームの生成を行い、React ツリーをレンダリングします。
graph TD
subgraph "Terminal Setup"
ME[Enable mouse events]
KM[Load key matchers]
CP[Patch console]
WS[Create working stdio]
end
subgraph "React Tree"
AW[AppWrapper]
AW --> SC[SettingsContext.Provider]
SC --> KMP[KeyMatchersProvider]
KMP --> KP[KeypressProvider]
KP --> MP[MouseProvider]
MP --> TP[TerminalProvider]
TP --> SP[ScrollProvider]
SP --> OP[OverflowProvider]
OP --> SSP[SessionStatsProvider]
SSP --> VMP[VimModeProvider]
VMP --> AC[AppContainer]
end
ME --> AW
KM --> AW
CP --> AW
WS --> AW
コンテキストプロバイダーの階層構造
UI は深い階層の React コンテキストプロバイダーを使って状態を管理しています。interactiveCli.tsx の 100〜120 行目にある AppWrapper コンポーネントを見てみましょう。
classDiagram
class SettingsContext {
LoadedSettings
Multi-scope settings
}
class KeyMatchersProvider {
Key binding definitions
Custom shortcuts
}
class KeypressProvider {
Priority-based key handling
useKeypress hook
}
class MouseProvider {
Mouse event state
Click and scroll tracking
}
class TerminalProvider {
Terminal dimensions
Capability detection
}
class ScrollProvider {
Scroll position
Virtual scroll management
}
class OverflowProvider {
Content overflow detection
Truncation management
}
class SessionStatsProvider {
Token counts
Turn statistics
}
class VimModeProvider {
Vi keybinding mode
Normal/insert state
}
SettingsContext --> KeyMatchersProvider
KeyMatchersProvider --> KeypressProvider
KeypressProvider --> MouseProvider
MouseProvider --> TerminalProvider
TerminalProvider --> ScrollProvider
ScrollProvider --> OverflowProvider
OverflowProvider --> SessionStatsProvider
SessionStatsProvider --> VimModeProvider
この階層の最下層に位置する AppContainer コンポーネントが、実際の処理の中心です。数百行にも及ぶ大型コンポーネントで、以下の責務を担っています。
- 認証状態と認証コマンドの処理
- イベント処理のための Gemini ストリームフック(
useGeminiStream) useHistoryによるヒストリー管理- スラッシュコマンドの処理
- 確認リクエストのハンドリング
- クォータとフォールバックの管理
- ストリーミング状態、入力フォーカス、ツールアクションといった UI 状態
AppContainer はこれらすべてをさらに別のコンテキストプロバイダーでラップしてから、実際の App コンポーネントをレンダリングします。具体的には AppContext、UIStateContext、UIActionsContext、ConfigContext、ToolActionsProvider が使われています。
ヒント: UI の問題をデバッグする際は、
ConfigContextとUIStateContextが最もよく参照するコンテキストです。ConfigContextはコアのConfigオブジェクトを提供し、UIStateContextはStreamingState、現在の確認リクエスト、ヒストリーアイテムといったミュータブルな状態を保持しています。
ストリーミングイベントから React 状態へ
エージェントループ(第2回)と React UI をつなぐのが useGeminiStream フックです。このフックは GeminiClient.sendMessageStream() から ServerGeminiStreamEvent を受け取り、React の状態更新に変換します。
sequenceDiagram
participant User as User Input
participant AC as AppContainer
participant GS as useGeminiStream
participant GC as GeminiClient
participant UI as React Components
User->>AC: Submit prompt
AC->>GS: sendMessage(prompt)
GS->>GC: sendMessageStream()
loop For each event
GC-->>GS: ServerGeminiStreamEvent
alt Content event
GS->>UI: Update streaming text
else ToolCallRequest
GS->>UI: Show tool call in progress
else ToolCallConfirmation
GS->>UI: Show confirmation dialog
else Thought event
GS->>UI: Update thinking indicator
else Error event
GS->>UI: Display error
end
end
GC-->>GS: Finished event
GS->>UI: Set streaming state to idle
フックは StreamingState enum の状態遷移を管理しています。
- メッセージ送信時:
Idle→Streaming - レスポンス完了時:
Streaming→Idle - ユーザーが中断した時:
Streaming→Cancelled
ストリームから届いたツール呼び出しリクエストは Scheduler に渡されます(第3回参照)。Scheduler がユーザーの確認を必要とする場合は MessageBus 経由でイベントを発行し、UI は TOOL_CONFIRMATION_REQUEST イベントを受け取って適切な確認ダイアログを表示します。
非インタラクティブモード
stdin がパイプされているか --prompt フラグが使用されている場合、Gemini CLI は React を使わず非インタラクティブモードで動作します。packages/cli/src/nonInteractiveCli.ts の runNonInteractive() 関数は、よりシンプルなイベント処理パスを提供します。
flowchart TD
A[main() detects non-interactive] --> B[Read stdin if piped]
B --> C[Fire SessionStart hook]
C --> D[Log user prompt telemetry]
D --> E[runNonInteractive()]
E --> F[Consume ServerGeminiStreamEvent]
F --> G[Write Content to stdout]
F --> H[Auto-confirm tools per policy]
F --> I[Write errors to stderr]
G --> J[Exit on Finished]
非インタラクティブモードでは、gemini.tsx の 720〜754 行目にある出力リスナーが coreEvents を直接 stdout と stderr に振り分けます。React レンダリングもスクロール管理もキーハンドリングも一切なく、イベント駆動で出力するだけです。
確認ダイアログを表示する UI がないため、非インタラクティブモードはポリシーエンジンと承認モードに完全に依存します。YOLO モードではすべてのツールが自動承認されます。DEFAULT モードでは確認が必要なツールは自動的に拒否されます(確認できるユーザーがいないため)。これは MessageBus のリスナーチェックで制御されており、TOOL_CONFIRMATION_REQUEST リスナーが登録されていない場合、confirmed: false, requiresUserConfirmation: true をただちに返します。
エージェントプロトコルと SDK
packages/sdk/ の SDK は、AgentProtocol インターフェースを基盤としたプログラマブル API を提供します。
export interface AgentProtocol extends Trajectory {
send(payload: AgentSend): Promise<{ streamId: string | null }>;
subscribe(callback: (event: AgentEvent) => void): Unsubscribe;
abort(): Promise<void>;
readonly events: readonly AgentEvent[];
}
三つのメソッドがインターフェース全体の契約を定義しています。
send()— エージェントにデータを送信します(メッセージ、エリシテーションレスポンス、設定更新、アクション)。相関付けのためのstreamIdを返します。subscribe()— すべてのエージェントイベントをリッスンします。購読解除用の関数を返します。abort()— 現在のエージェント処理をキャンセルします。
AgentSession は AgentProtocol をより便利な API でラップし、AsyncIterable として sendStream() を追加しています。
async *sendStream(payload: AgentSend): AsyncIterable<AgentEvent> {
const result = await this._protocol.send(payload);
const streamId = result.streamId;
if (streamId === null) return;
yield* this.stream({ streamId });
}
これにより、for await...of ループでエージェントイベントを処理できます。
const session = agent.session();
for await (const event of session.sendStream({ message: [{ text: "Fix the bug" }] })) {
switch (event.type) {
case 'content': console.log(event.text); break;
case 'agent_end': console.log('Done'); break;
}
}
SDK のエントリーポイントである GeminiCliAgent はセッションを管理します。
const agent = new GeminiCliAgent({ cwd: '/path/to/project' });
const session = agent.session();
// 既存セッションを再開する場合:
const resumed = await agent.resumeSession(previousSessionId);
graph TD
subgraph SDK Package
GCA[GeminiCliAgent<br/>Session factory]
GCS[GeminiCliSession<br/>Session lifecycle]
end
subgraph Core Package
AP[AgentProtocol<br/>send/subscribe/abort]
AS[AgentSession<br/>AsyncIterable wrapper]
ET[Event Translator<br/>ServerGeminiStreamEvent → AgentEvent]
end
GCA --> GCS
GCS --> AP
AP --> AS
AS --> ET
Event Translator レイヤーは、内部の ServerGeminiStreamEvent ユニオン(第2回で触れた 18 種類)を、外部利用者向けに最適化された別の分類体系を持つ SDK の AgentEvent ユニオンに変換します。
A2A サーバーと VS Code 拡張機能
SDK の上にはさらに二つのパッケージが構築されています。
packages/a2a-server — Gemini CLI の機能を HTTP 経由で公開する実験的な Agent-to-Agent プロトコルサーバーです。他のエージェントは標準化された API を通じて、プロンプトの送信、ストリーミングレスポンスの受信、ツールの呼び出しを行えます。エージェント間通信のための Google の A2A プロトコル仕様に準拠しています。
packages/vscode-ide-companion — エディター連携を実現する VS Code 拡張機能です。開いているファイル、カーソル位置、選択テキストといった IDE のコンテキスト情報を ideContextStore 経由で Gemini CLI バックエンドに送信します。第2回の GeminiClient.getIdeContextParts() で見たように、このコンテキストは構造化 JSON として会話に挿入され、モデルがエディター上で何を見ているかを把握できるようになります。
ヒント: SDK を使って開発する際は、
send()とsubscribe()を手動で組み合わせるよりもAgentSession.sendStream()を使いましょう。sendStream()はストリームの追跡、streamIdによるイベントフィルタリング、async iterator プロトコルを通じた適切なクリーンアップをすべて自動で処理してくれます。
シリーズのまとめ
6回にわたって、Gemini CLI のコードベースを端から端まで追ってきました。
- アーキテクチャ — 7パッケージのモノレポ、Config ゴッドオブジェクト、二系統のイベントシステム
- エージェントループ — GeminiClient → Turn → GeminiChat のストリーミングイベント、圧縮、ループ検出
- ツールと Scheduler — ツール定義のビルダーパターンとイベント駆動のオーケストレーション
- セキュリティ — ポリシーエンジンのルール、プラットフォームサンドボックス、プラガブルな安全性チェッカー
- 拡張性 — フック、スキル、MCP、HMAC 整合性検証付き拡張機能、モデルルーティング戦略
- UI と SDK — React/Ink によるターミナルレンダリングとプログラマブルな AgentProtocol
一貫して貫かれている設計思想は、明確な契約による階層的な抽象化です。ServerGeminiStreamEvent ユニオンはバックエンドとあらゆるフロントエンドをつなぎます。AgentLoopContext インターフェースは実行コンテキストをスコープします。ToolInvocation ライフサイクルはツール実行を標準化します。そして MessageBus が、エージェントの自律的な判断とユーザーのコントロールを仲介します。
Gemini CLI へのコントリビュート、拡張機能の開発、SDK での組み込みのいずれにおいても、これらの層を理解していれば自信を持ってコードベースを歩き回れるはずです。