Read OSS

Gemini CLI 的两张面孔:React/Ink 终端 UI 与可编程 SDK

中级

前置知识

  • 第 1 篇:架构与代码导航指南
  • 第 2 篇:智能体循环
  • React 基础知识
  • 理解异步可迭代对象(async iterables)

Gemini CLI 的两张面孔:React/Ink 终端 UI 与可编程 SDK

Gemini CLI 的架构有着清晰的 core/CLI 分层、类型化的事件流以及规范的 protocol 接口,这使得它能够在同一套后端之上提供两种截然不同的交互方式。交互式终端 UI 通过 React 在终端内渲染出功能丰富的应用界面,包含 context provider、键盘处理和流式内容展示;SDK 则将同一套引擎封装为可编程 API,方便嵌入其他工具。本文将逐一介绍这两种方式。

React/Ink 交互式 UI

Gemini CLI 的终端 UI 是一个完整的 React 应用,通过 Ink 渲染到终端输出。Ink 的核心思想是将 React 组件映射为终端字符界面。UI 采用懒加载策略——正如第 1 篇所介绍的,gemini.tsx 第 167–185 行中的 startInteractiveUI 会动态 import 这个较重的模块:

export async function startInteractiveUI(/* ... */) {
  const { startInteractiveUI: doStartUI } = await import('./interactiveCli.js');
  await doStartUI(config, settings, startupWarnings, workspaceRoot, ...);
}

这样可以让非交互路径保持快速启动。进入交互模式后,interactiveCli.tsx 中的 startInteractiveUI 负责启用鼠标事件、加载按键匹配器、补丁 console 输出、创建工作用的 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

Context Provider 层级结构

UI 使用深层嵌套的 React context provider 来管理状态。来看 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 是真正承担核心工作的组件。它体量庞大(数百行代码),负责管理以下内容:

  • 认证状态与 auth 命令处理
  • 通过 useGeminiStream hook 处理 Gemini 流式事件
  • 通过 useHistory 管理历史记录
  • slash 命令处理
  • 确认请求的响应逻辑
  • 配额管理与降级策略
  • UI 状态,如流式状态、输入焦点和工具操作

在渲染实际的 App 组件之前,AppContainer 还会再套上一层 context provider:AppContextUIStateContextUIActionsContextConfigContextToolActionsProvider

提示: 排查 UI 问题时,最常用到的是 ConfigContextUIStateContextConfigContext 提供核心的 Config 对象,而 UIStateContext 则持有可变状态,如 StreamingState、当前确认请求以及历史记录项。

从流式事件到 React 状态

连接智能体循环(第 2 篇)与 React UI 的桥梁是 useGeminiStream hook。它订阅来自 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

这个 hook 负责管理 StreamingState 枚举的状态流转:

  • IdleStreaming:发送消息时触发
  • StreamingIdle:响应完成时触发
  • StreamingCancelled:用户中断时触发

来自流的工具调用请求会被分发给 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

Agent Protocol 与 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() — 向 agent 发送数据(消息、elicitation 响应、配置更新、操作指令),返回用于关联的 streamId
  • subscribe() — 监听所有 agent 事件,返回取消订阅的函数。
  • abort() — 取消当前 agent 正在进行的活动。

AgentSessionAgentProtocol 基础上提供了更便捷的 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 循环来处理 agent 事件:

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 联合类型(18 种类型,详见第 2 篇)与 SDK 对外暴露的 AgentEvent 联合类型之间进行转换。后者采用了针对外部消费者优化的不同分类体系。

A2A Server 与 VS Code 插件

另有两个附加包基于 SDK 构建:

packages/a2a-server — 一个实验性的 Agent-to-Agent 协议服务器,通过 HTTP 对外暴露 Gemini CLI 的能力。其他 agent 可以通过标准化 API 发送 prompt、接收流式响应并调用工具。这一实现遵循 Google 的 A2A 协议规范,用于 agent 间的互联通信。

packages/vscode-ide-companion — 一个 VS Code 插件,提供与编辑器的深度集成。它通过 ideContextStore 将 IDE 上下文(打开的文件、光标位置、选中文本)传递给 Gemini CLI 后端。正如第 2 篇在 GeminiClient.getIdeContextParts() 中所介绍的,这些上下文会以结构化 JSON 的形式注入到对话中,让模型能够感知你在编辑器中正在查看的内容。

提示: 使用 SDK 开发时,建议优先使用 AgentSession.sendStream(),而不是手动调用 send() + subscribe()sendStream() 已经内置了流跟踪、按 streamId 过滤事件以及通过异步迭代器协议自动清理资源的逻辑。

系列总结

在这六篇文章中,我们完整地走过了 Gemini CLI 的整个代码库:

  1. 架构 — 7 个包组成的 monorepo、Config 核心对象,以及双事件系统
  2. 智能体循环 — GeminiClient → Turn → GeminiChat,涵盖流式事件、上下文压缩与循环检测
  3. 工具与 Scheduler — 工具定义的 builder 模式与事件驱动的编排机制
  4. 安全 — 策略引擎规则、平台沙箱,以及可插拔的安全检查器
  5. 可扩展性 — Hooks、skills、MCP、带 HMAC 完整性校验的扩展,以及模型路由策略
  6. UI 与 SDK — React/Ink 终端渲染与可编程的 AgentProtocol

贯穿始终的设计哲学是:通过清晰的契约实现层层抽象。ServerGeminiStreamEvent 联合类型将后端与任意前端连接起来;AgentLoopContext 接口约定了执行上下文的边界;ToolInvocation 生命周期标准化了工具的执行方式;MessageBus 则在 agent 的自主决策与用户控制之间扮演着调解者的角色。

无论你是要为 Gemini CLI 贡献代码、开发扩展,还是通过 SDK 将其嵌入自己的工具,理解这些层次结构都能帮助你建立清晰的全局视图,在庞大的代码库中自信地找到方向。