前置:Phase 0 地图已通
目标:本机跑起 CLI;能指出交互与 headless 各自从哪进main(),以及 UI 如何摸到GeminiClient;
对后端同学:把终端 UI 当「前端」略读,搞清为什么有无 UI,然后尽快进 core
官方对照:
CONTRIBUTING.md(本地开发)docs/local-development.md(可选:OTel 追 loop)docs/cli/headless.md(无 UI / 可编程接口)docs/cli/cli-reference.md(常用 flag)- 安装文档:geminicli.com/docs/get-started/installation
| 顺序 | 文件 | 看什么 |
|---|---|---|
| 1 | packages/cli/index.ts |
进程入口 / supervisor:memory、relaunch、吞 pty 竞态;再加载真正的 main |
| 2 | packages/cli/src/gemini.tsx |
export async function main():参数、settings、auth、选交互/无 UI |
| 3 | packages/cli/src/interactiveCli.tsx |
ink.render(...) 挂上交互 UI;知入口即可 |
| 4 | packages/cli/src/nonInteractiveCli.ts |
headless:一条 prompt 跑完退出 |
| 5 | packages/cli/src/ui/hooks/useGeminiStream.ts |
搜 submitQuery / sendMessageStream——UI 如何调用 core |
浏览:scripts/start.js(根 npm start 调它)、packages/cli/package.json 的 "bin": { "gemini": ... }。
可选扫一眼(不必深读):ui/contexts/KeypressContext.tsx(setRawMode + 解析按键)。
cd /Users/qychen98/Projects/gemini-cli
# 建议 Node 20
node -v # 应对齐 .nvmrc
npm install
npm run build
npm start首次运行通常会:
- 选主题(交互模式)
- 做 Google 账号 / API key 等认证(按你本机环境)
- 进入对话提示符
常用用法(开发态把参数透传给 gemini):
npm start # 交互
npm start -- -p "summarize README.md" # 无 UI,跑完退出
npm run debug # node --inspect-brk安装后的用户命令同理:gemini / gemini -p "..." / echo "..." | gemini。
只验证「能启动、能发出一轮请求」即可;本阶段不要求改代码。
npm start
→ scripts/start.js # 检查 build、设 DEV、spawn node packages/cli
→ packages/cli/index.ts # 进程壳(见下)
→ import main from ./src/gemini.js
→ main()
生产:@google/gemini-cli 的 bin.gemini → dist/index.js(同源)。
index.ts 做什么(不是业务 main):
- 轻量父进程 / 重量子进程:默认先 spawn 自己,避免一上来 import Ink/React 拖慢启动(~1.5s)
- 按机器内存调
--max-old-space-size(可用 settingsadvanced.autoConfigureMemory关掉) - 子进程以
RELAUNCH_EXIT_CODE(199)退出时循环 relaunch(更新等场景) - 吞掉
node-pty已知 resize/EBADF竞态;其它 uncaught → stderr + exit - 真正业务在子进程分支:动态加载
gemini.tsx的main()
心智:index.ts = bootstrap / process supervisor;gemini.tsx = 应用入口。
在 gemini.tsx 的 main() 里(概念上):
main()
→ 解析 argv / settings / workspace
→ 认证与 config 初始化(构造 core 侧 Config / GeminiClient 等)
→ 若交互:startInteractiveUI / interactiveCli
→ 若无 UI(`-p`、piped stdin、非 TTY 等):runNonInteractive(...)
对照 mini 的 run/hello_world.py:
| mini | Gemini CLI |
|---|---|
脚本里 DefaultAgent(...).run(task) |
main() 拼好 config + client,再进 UI 或 headless |
| 无独立 UI 层 | Ink 是默认体验,但 loop 不在 React 里实现 |
交互路径的关键一跳在 useGeminiStream.ts:
用户回车
→ submitQuery(...)
→ geminiClient.sendMessageStream(request, signal, prompt_id, ...)
→ for await (event of stream) 更新 UI
工具执行不在 sendMessageStream 内部「偷偷跑完」就结束——CLI 会消费 ToolCallRequest 等事件,经 useToolScheduler(包装 core Scheduler)执行,再把结果送回下一轮。细节见 Phase 2 / 3。
后端同学可把这一层当成 前端:展示 + 采集输入;agent 核心不在这里。
packages/cli(Ink / 按键 / 渲染) ≈ 前端
packages/core(client / tools / loop)≈ 后端
交互能力的根基是操作系统 + Node 进程,Ink 只是本项目的 TUI 框架选型:
1. TTY(终端设备)
stdin/stdout 连着真实终端(process.stdin.isTTY)
→ 才能 raw mode、光标控制、全屏重绘
pipe / 重定向时 isTTY=false → 走 headless
2. 常驻进程
不退出,一直听键盘、持续写屏
3. Raw mode + 按键解析
setRawMode(true):按键立刻进进程(非「回车才送一行」)
KeypressContext 把 ESC 序列解析成 enter / 方向键 / …
InputPrompt + text-buffer 维护输入框
4. Ink = 终端版 React(本仓库的 UI 实现)
ink.render(<App />) → ANSI 字符画;状态变了就重绘
可选 alternate buffer(独立全屏,退出还原)
一句话:TTY + raw mode + 常驻进程 = 能交互;Ink = Gemini CLI 用来组织「画屏 / 组件状态」的库。
旁注(不必深究):Claude Code 也是 React/Ink 路线(深度定制);Codex CLI 当前主路径是 Rust + Ratatui——共同点是 TUI,不是都必须 Ink。
Agent 不只要给人聊,还要给脚本 / CI / 其它程序当「一次函数调」。
| 场景 | 交互 UI 的问题 | 无 UI |
|---|---|---|
| 管道 | cat log | gemini 没有完整 TTY |
读 stdin,跑完 exit |
| CI / 自动化 | 没人点确认、画不了全屏 | gemini -p "..." + exit code |
| 下游解析 | ANSI 难 parse | --output-format json / stream-json |
| 嵌入 | UI 绑死进程 | 同一套 core;再往上是 SDK |
官方叫法:Headless mode —— programmatic interface。
无 UI 不是阉割版,而是第二种客户端:仍走 core loop,只是不挂 Ink。
有 UI ≈ Web 控制台(给人)
无 UI ≈ HTTP API / 批处理入口(给机器)
core ≈ 两边共用的业务服务
| 形态 | 入口文件 | 典型场景 |
|---|---|---|
| 交互 | interactiveCli.tsx + Ink hooks |
日常 npm start / gemini |
| 无 UI | nonInteractiveCli.ts |
-p、pipe、脚本、CI |
| SDK(本阶段只知道存在) | packages/sdk GeminiCliAgent |
嵌入别的程序(Phase 5 可选) |
验收时至少亲手跑过 交互;有余力再用一条非交互命令感受 headless(例如 npm start -- -p "hello")。
用一句话填空:
mini: hello_world 拼装 Agent/Model/Env,然后 agent.run(task)
Gemini: gemini.tsx 拼装 Config/Client/…,然后 ______ 或 ______
参考答案:interactive UI(submitQuery → sendMessageStream) / runNonInteractive。
再填一句(心智):
终端 Ink 层对我而言相当于 ______;我该深挖的是 ______。
参考答案:前端 / 展示层;core 的 loop 与扩展挂载点(tools / MCP / GEMINI.md / hooks)。
-
npm install+npm run build+npm start能起来 - 能指出进程入口是
packages/cli/index.ts(supervisor),业务main在gemini.tsx - 能说出交互 vs
nonInteractiveCli的分流,以及无 UI 存在的原因(脚本 / CI / pipe / 可编程输出) - 能用三层解释「为什么终端能交互」:TTY → raw mode → Ink(并知道 Ink ≠ 交互根基)
- 能在
useGeminiStream.ts里找到sendMessageStream调用点 - 明白:改 loop / 扩展主要去 core,不是去 Ink 组件
跑通之后请记住:
接下来重点是扩展面(tools / MCP / GEMINI.md / hooks / policy / SDK),不是继续读 Ink。
Phase 2 只花半天到一天摸清 loop 形状与挂载点,然后尽快进 Phase 3–4。
→ PHASE2-AGENT-LOOP.md:轻读 GeminiClient / Turn,标出扩展挂载点。