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 命令处理
- 通过
useGeminiStreamhook 处理 Gemini 流式事件 - 通过
useHistory管理历史记录 - slash 命令处理
- 确认请求的响应逻辑
- 配额管理与降级策略
- UI 状态,如流式状态、输入焦点和工具操作
在渲染实际的 App 组件之前,AppContainer 还会再套上一层 context provider:AppContext、UIStateContext、UIActionsContext、ConfigContext 和 ToolActionsProvider。
提示: 排查 UI 问题时,最常用到的是
ConfigContext和UIStateContext。ConfigContext提供核心的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 枚举的状态流转:
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。
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 正在进行的活动。
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 循环来处理 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 的整个代码库:
- 架构 — 7 个包组成的 monorepo、Config 核心对象,以及双事件系统
- 智能体循环 — GeminiClient → Turn → GeminiChat,涵盖流式事件、上下文压缩与循环检测
- 工具与 Scheduler — 工具定义的 builder 模式与事件驱动的编排机制
- 安全 — 策略引擎规则、平台沙箱,以及可插拔的安全检查器
- 可扩展性 — Hooks、skills、MCP、带 HMAC 完整性校验的扩展,以及模型路由策略
- UI 与 SDK — React/Ink 终端渲染与可编程的 AgentProtocol
贯穿始终的设计哲学是:通过清晰的契约实现层层抽象。ServerGeminiStreamEvent 联合类型将后端与任意前端连接起来;AgentLoopContext 接口约定了执行上下文的边界;ToolInvocation 生命周期标准化了工具的执行方式;MessageBus 则在 agent 的自主决策与用户控制之间扮演着调解者的角色。
无论你是要为 Gemini CLI 贡献代码、开发扩展,还是通过 SDK 将其嵌入自己的工具,理解这些层次结构都能帮助你建立清晰的全局视图,在庞大的代码库中自信地找到方向。