mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-05-02 05:31:02 +00:00
docs: harden tui optimization design
This commit is contained in:
parent
88efd775db
commit
e2be1bd548
7 changed files with 1549 additions and 145 deletions
393
docs/design/tui-optimization/04-gemini-cli-research.md
Normal file
393
docs/design/tui-optimization/04-gemini-cli-research.md
Normal file
|
|
@ -0,0 +1,393 @@
|
|||
# Gemini CLI 源码调研:TUI 架构、性能与交互
|
||||
|
||||
> 调研对象:`/Users/gawain/Documents/codebase/opensource/gemini-cli`
|
||||
> 目标:从启动、渲染、防闪烁、滚动、Markdown、MCP、终端协议等维度提炼可迁移经验,并标注不应直接照搬的部分。
|
||||
|
||||
**事实边界**:除特别注明的外部终端资料外,本文件结论均基于本地源码树当前状态。上游若发生重构、文件移动或行为变更,需要重新核对后再引用到实施文档中。
|
||||
|
||||
## 1. 结论摘要
|
||||
|
||||
Gemini CLI 的 TUI 并不是“靠一个点优化起来”的,而是把优化拆成了四层:
|
||||
|
||||
1. **启动层**:入口尽量把重模块推迟到需要时再加载,例如 `packages/cli/src/gemini.tsx` 动态 `import('./interactiveCli.js')`
|
||||
2. **渲染模式层**:同一套 UI 支持 `alternateBuffer`、`terminalBuffer`、`renderProcess`、`incrementalRendering` 等多种模式
|
||||
3. **交互/长会话层**:在 alternate/terminal buffer 模式下使用 `ScrollableList` / `VirtualizedList`,并通过 `ResizeObserver`、`StaticRender`、批量滚动维持稳定性
|
||||
4. **观测层**:有 `startupProfiler`、`onRender` profiling、`useFlickerDetector()` 等现成观察点
|
||||
|
||||
但 Gemini CLI 也并非全线领先:
|
||||
|
||||
- Markdown 仍是**自定义正则解析器**,不是成熟 AST parser
|
||||
- 代码高亮仍是**同步 lowlight/common**,不是懒加载 grammar
|
||||
- `refreshStatic()` 仍然存在 `clearTerminal` 路径
|
||||
- `config.initialize()` 仍在 React mount 后执行,启动口径依然需要额外 instrumentation
|
||||
|
||||
因此,对 qwen-code 来说,Gemini 更像是“**渲染模式、滚动和观测层的强参考实现**”,而不是 Markdown / parser 架构的最终答案。
|
||||
|
||||
## 2. 关键文件地图
|
||||
|
||||
| 维度 | 文件 |
|
||||
| --- | --- |
|
||||
| 入口与启动 | `packages/cli/src/gemini.tsx` |
|
||||
| 交互式 UI render | `packages/cli/src/interactiveCli.tsx` |
|
||||
| 主状态容器 | `packages/cli/src/ui/AppContainer.tsx` |
|
||||
| 主内容区 | `packages/cli/src/ui/components/MainContent.tsx` |
|
||||
| 流式输出处理 | `packages/cli/src/ui/hooks/useGeminiStream.ts` |
|
||||
| 闪烁观测 | `packages/cli/src/ui/hooks/useFlickerDetector.ts` |
|
||||
| 虚拟滚动 | `packages/cli/src/ui/components/shared/VirtualizedList.tsx` |
|
||||
| 滚动容器 | `packages/cli/src/ui/components/shared/ScrollableList.tsx` |
|
||||
| Markdown | `packages/cli/src/ui/utils/MarkdownDisplay.tsx` |
|
||||
| 代码高亮 | `packages/cli/src/ui/utils/CodeColorizer.tsx` |
|
||||
| 表格渲染 | `packages/cli/src/ui/utils/TableRenderer.tsx` |
|
||||
| 终端能力检测 | `packages/cli/src/ui/utils/terminalCapabilityManager.ts` |
|
||||
| MCP 状态展示 | `packages/cli/src/ui/hooks/useMcpStatus.ts` |
|
||||
|
||||
## 3. 启动与初始化路径
|
||||
|
||||
### 3.1 入口把 React/Ink 推迟到真正需要时
|
||||
|
||||
`packages/cli/src/gemini.tsx` 的一个关键动作,是**主入口不直接顶层导入完整交互 UI**。它在确认要进入交互模式后,才动态导入 `interactiveCli.js`。这类拆分直接减少了冷启动阶段的 JS 解析和模块求值成本。
|
||||
|
||||
对 qwen-code 的启发:
|
||||
|
||||
- 可以把 `Ink`、`AppContainer`、大型 UI util 从 CLI 主入口延后加载
|
||||
- 如果未来引入 `marked`、更重的高亮或图像能力,必须维持这种“主路径瘦身”策略,否则 bundle 体积很快反噬首屏时间
|
||||
|
||||
### 3.2 pre-render 初始化与 post-render 初始化明确分层
|
||||
|
||||
Gemini CLI 在 render 前会做一部分初始化,例如:
|
||||
|
||||
- `loadSettings()`
|
||||
- `initializeApp(config, settings)`,内部执行 auth、theme 校验、IDE 背景连接等
|
||||
- `startupProfiler.start('cli_startup')`
|
||||
|
||||
但**真正的 `config.initialize()` 并不在 render 前完成**。它在 `packages/cli/src/ui/AppContainer.tsx` 中通过 effect 执行:
|
||||
|
||||
- 检查 `config.isInitialized()`
|
||||
- `await config.initialize()`
|
||||
- `setConfigInitialized(true)`
|
||||
- 然后再 `startupProfiler.flush(config)`
|
||||
|
||||
这说明 Gemini 也把“首屏出现”和“完整初始化完成”拆开了。对 qwen-code 的直接启示有两条:
|
||||
|
||||
1. 当前文档必须明确区分 `first_paint` 与 `config_initialize_end`
|
||||
2. “把启动测快”不能只盯着 render 前 profiler,因为 render 后的 `config.initialize()` 可能才是长尾
|
||||
|
||||
### 3.3 render 选项是 Gemini 的重要分水岭
|
||||
|
||||
`packages/cli/src/interactiveCli.tsx` 向 Ink render 传入了多组开关:
|
||||
|
||||
- `alternateBuffer`
|
||||
- `terminalBuffer`
|
||||
- `renderProcess`
|
||||
- `incrementalRendering`
|
||||
- `standardReactLayoutTiming`
|
||||
|
||||
其中 `incrementalRendering` 不是全局无条件开启,而是要求:
|
||||
|
||||
- settings 未显式关闭
|
||||
- 使用 alternate buffer
|
||||
- 非 shpool 场景
|
||||
|
||||
这说明 Gemini 团队已经接受一个现实:**不同终端、不同会话模式,最优渲染策略并不相同**。qwen-code 当前文档也应改成同样的思路,不再把“单一渲染模式”当作默认前提。
|
||||
|
||||
### 3.4 终端能力检测前置且覆盖输入/颜色/协议
|
||||
|
||||
`packages/cli/src/ui/utils/terminalCapabilityManager.ts` 做的事情比“看几个 env var”更激进,它会主动 query 终端能力:
|
||||
|
||||
- Kitty keyboard 协议
|
||||
- `OSC 11` 背景色查询
|
||||
- 终端 name/version
|
||||
- 设备属性 sentinel
|
||||
- `modifyOtherKeys`
|
||||
|
||||
并在退出时恢复:
|
||||
|
||||
- kitty keyboard / modifyOtherKeys
|
||||
- bracketed paste
|
||||
- mouse modes
|
||||
|
||||
对 qwen-code 的启发:
|
||||
|
||||
- Theme 自动选择不能只依赖 `TERM` / `COLORTERM`
|
||||
- 输入协议升级不应只靠静态 allowlist
|
||||
- 终端能力检测应视为基础设施,而不是散落在主题或 keybinding 里的临时逻辑
|
||||
|
||||
## 4. 渲染模式与防闪烁策略
|
||||
|
||||
### 4.1 Gemini 已经把“不同输出模式”产品化
|
||||
|
||||
Gemini 不是只依赖 `<Static>`。它在 render 选项和 UI 组件层形成了一个模式矩阵:
|
||||
|
||||
| 模式 | 主要特征 | 适用价值 |
|
||||
| --- | --- | --- |
|
||||
| main screen | 依赖 Ink 标准流式区域 | 兼容性最好,但最容易闪烁 |
|
||||
| alternate buffer | 全屏会话、滚动交互更自然 | 适合长对话和复制模式 |
|
||||
| terminal buffer | 支持更强滚动/回看语义 | 适合稳定 scrollback 场景 |
|
||||
| render process | 把 render 工作转移到独立过程 | 为高负载场景预留余地 |
|
||||
| incremental rendering | 配合特定 buffer 模式降低全量刷新 | 直接面向防闪烁 |
|
||||
|
||||
这给 qwen-code 的直接建议是:**文档不要只讨论某个优化点,而要把“渲染模式开关”本身列为一等设计对象**。
|
||||
|
||||
### 4.2 渐进转 Static 已经是 Gemini 的核心流式优化
|
||||
|
||||
`packages/cli/src/ui/hooks/useGeminiStream.ts` 中最值得借鉴的实现,是通过 `findLastSafeSplitPoint()` 把流式内容分成:
|
||||
|
||||
- 已稳定部分:写入 history / Static
|
||||
- 尾部未稳定部分:保留在 pending 区域
|
||||
|
||||
它在源码注释里明确把这件事定义为:
|
||||
|
||||
- 提升性能
|
||||
- 尽量把内容挪进 `<Static />`
|
||||
- 减少 re-render 和 flickering
|
||||
|
||||
这与 qwen-code 当前文档的方向一致,但 Gemini 给了两个更具体的经验:
|
||||
|
||||
1. **边界必须是 Markdown-safe 的**,不能只按字符数切
|
||||
2. 这不是最终解法,只是减轻动态区域压力的中层方案
|
||||
|
||||
### 4.3 Gemini 已经有 flicker observability,而不只是“肉眼觉得闪”
|
||||
|
||||
`packages/cli/src/ui/hooks/useFlickerDetector.ts` 每次 render 后都会:
|
||||
|
||||
- `measureElement(rootUiRef.current)`
|
||||
- 比较渲染高度与终端高度
|
||||
- 在 `constrainHeight` 为真且高度越界时:
|
||||
- `recordFlickerFrame(config)`
|
||||
- `appEvents.emit(AppEvent.Flicker)`
|
||||
|
||||
这意味着 Gemini 已经把“渲染超出终端高度”视为可记录的 bug 信号,而不是仅靠用户主观反馈。
|
||||
|
||||
对 qwen-code 的建议非常直接:
|
||||
|
||||
- 先补 `flicker frame`、`clearTerminal count`、`writes/sec`
|
||||
- 再谈具体优化优先级
|
||||
|
||||
### 4.4 `refreshStatic()` 仍然是 Gemini 的已知弱点
|
||||
|
||||
Gemini 的 `refreshStatic()` 逻辑并不完美。`packages/cli/src/ui/AppContainer.tsx` 中:
|
||||
|
||||
- 如果当前不在 alternate buffer 且没启用 terminal buffer
|
||||
- 就会 `stdout.write(ansiEscapes.clearTerminal)`
|
||||
- 然后增加 `historyRemountKey`
|
||||
|
||||
同时它会在这些场景反复触发:
|
||||
|
||||
- banner 变化
|
||||
- editor 关闭
|
||||
- width resize(300ms debounce)
|
||||
- 若干 UI 状态切换
|
||||
|
||||
这说明 Gemini 虽然在滚动/模式层做得很强,但**main-screen 的静态区刷新仍然有整屏清除代价**。这也是 qwen-code 不应照搬的点。
|
||||
|
||||
## 5. 滚动、长会话与交互
|
||||
|
||||
### 5.1 `MainContent` 已经有两套内容呈现路径
|
||||
|
||||
`packages/cli/src/ui/components/MainContent.tsx` 中,Gemini 会根据模式选择:
|
||||
|
||||
- main-screen 路径:`<Static>` + pending 区域
|
||||
- alternate/terminal buffer 路径:`<ScrollableList>`
|
||||
|
||||
并在 terminal buffer 模式下使用:
|
||||
|
||||
- `renderStatic`
|
||||
- `isStaticItem`
|
||||
- `overflowToBackbuffer`
|
||||
|
||||
这说明 Gemini 已经把“长会话滚动”和“普通消息流式输出”拆成两类场景处理,而不是逼同一个组件兼容全部模式。
|
||||
|
||||
### 5.2 `VirtualizedList` 是重量级实现,不是简单 windowing
|
||||
|
||||
`packages/cli/src/ui/components/shared/VirtualizedList.tsx` 有几个很值得记录的实现细节:
|
||||
|
||||
- 使用 `ResizeObserver` 同时观察容器尺寸和 item 高度
|
||||
- 为每个 item 维护实际高度缓存,结合 `estimatedItemHeight()` 计算 offsets
|
||||
- 维护 `scrollAnchor`
|
||||
- 维护 `isStickingToBottom`
|
||||
- 使用 `useBatchedScroll()` 处理 scrollTop 更新
|
||||
- 支持 `StaticRender`
|
||||
- 支持 `overflowToBackbuffer`
|
||||
- 支持 `stableScrollback`
|
||||
- 支持 `copyModeEnabled`
|
||||
|
||||
这不是“只渲染可见窗口”那么简单,而是在处理:
|
||||
|
||||
- 动态高度 item
|
||||
- 贴底行为
|
||||
- scrollback 稳定性
|
||||
- 复制模式
|
||||
- backbuffer 输出
|
||||
|
||||
对 qwen-code 的含义是:如果未来要做虚拟滚动,**至少要先明确是要解决哪一组问题**。一个只做 `slice(visibleRange)` 的轻量实现,无法直接覆盖长会话中的 sticky bottom、tool 输出和 copy mode 需求。
|
||||
|
||||
### 5.3 `ScrollableList` 是可复用的交互容器抽象
|
||||
|
||||
Gemini 将滚动行为和虚拟化行为分层:
|
||||
|
||||
- `ScrollableList` 负责交互语义与外层容器
|
||||
- `VirtualizedList` 负责 item 级测量和窗口化
|
||||
|
||||
这是 qwen-code 当前文档值得吸收的结构性建议:**不要把虚拟滚动逻辑塞进 `MainContent` 本体**,否则后续 tool 输出、prompt 历史、selection list 都会复制同一套复杂逻辑。
|
||||
|
||||
## 6. Markdown、代码高亮、表格与主题
|
||||
|
||||
### 6.1 Markdown 仍是正则解析器
|
||||
|
||||
Gemini 的 `packages/cli/src/ui/utils/MarkdownDisplay.tsx` 仍在使用逐行正则:
|
||||
|
||||
- `headerRegex`
|
||||
- `codeFenceRegex`
|
||||
- `ulItemRegex`
|
||||
- `olItemRegex`
|
||||
- `tableRowRegex`
|
||||
- `tableSeparatorRegex`
|
||||
|
||||
并没有看到类似 Claude 的 token cache / AST parser 架构。
|
||||
|
||||
所以对 qwen-code 来说,Gemini 在这一层的价值主要是:
|
||||
|
||||
- 证明“即便滚动和 buffer 做得很好,parser 依然可能成为短板”
|
||||
- 说明不能把“我们像 Gemini 一样”当成 Markdown 方向的充分论据
|
||||
|
||||
### 6.2 代码高亮仍是同步 lowlight/common
|
||||
|
||||
`packages/cli/src/ui/utils/CodeColorizer.tsx` 里:
|
||||
|
||||
- `createLowlight(common)`
|
||||
- `colorizeCode()` 同步返回 ReactNode
|
||||
- 没语言时仍可能走 `highlightAuto()`
|
||||
|
||||
也就是说,Gemini 并没有解决“高亮 grammar 过重”和“同步 render 路径无法懒加载”的根本矛盾。qwen-code 当前文档对这点的判断是正确的,应保留。
|
||||
|
||||
### 6.3 表格渲染已经相当成熟
|
||||
|
||||
`packages/cli/src/ui/utils/TableRenderer.tsx` 体现出另一个现实:Gemini 对表格渲染已经投入了不少工程量,尤其在:
|
||||
|
||||
- ANSI 宽度处理
|
||||
- CJK 宽度处理
|
||||
- wrap / alignment
|
||||
- 窄屏 fallback
|
||||
|
||||
因此 qwen-code 的建议也应继续保持:表格不是当前阶段的最大架构机会,除非能拿出新的可复现缺陷。
|
||||
|
||||
### 6.4 主题检测与终端背景联动值得借鉴
|
||||
|
||||
Gemini 不只是提供主题,还会根据终端背景查询进行联动。相关链路包含:
|
||||
|
||||
- `terminalCapabilityManager` 的 `OSC 11` 背景查询
|
||||
- `useTerminalTheme`
|
||||
- 对“重复相同背景报告不重复刷新”的测试保护
|
||||
|
||||
对 qwen-code 的启示:
|
||||
|
||||
- 主题选择应当有“自动能力检测 + 避免重复刷新”的设计
|
||||
- 不要让主题探测本身成为触发 `refreshStatic()` 的噪声源
|
||||
|
||||
## 7. MCP、工具可用性与启动后状态
|
||||
|
||||
### 7.1 MCP 状态已经是事件驱动的
|
||||
|
||||
`packages/cli/src/ui/hooks/useMcpStatus.ts` 通过:
|
||||
|
||||
- `coreEvents.on(CoreEvent.McpClientUpdate, onChange)`
|
||||
- 读取 `config.getMcpClientManager().getDiscoveryState()`
|
||||
- 读取 server count
|
||||
|
||||
向 UI 暴露:
|
||||
|
||||
- `discoveryState`
|
||||
- `mcpServerCount`
|
||||
- `isMcpReady`
|
||||
|
||||
这意味着 Gemini 在 UI 层已经有了“渐进状态展示”的基础设施,而不是所有状态都绑在一次性 init promise 上。
|
||||
|
||||
### 7.2 用户体验上已经允许“先进入,再等待 MCP”
|
||||
|
||||
`packages/cli/src/ui/AppContainer.tsx` 里存在非常明确的提示:
|
||||
|
||||
> Waiting for MCP servers to initialize... Slash commands are still available and prompts will be queued.
|
||||
|
||||
这说明 Gemini 的产品决策很明确:
|
||||
|
||||
- UI 可以先起来
|
||||
- slash commands 先可用
|
||||
- prompt 可排队
|
||||
- MCP 不必阻塞整个会话出现
|
||||
|
||||
qwen-code 文档应该把这一点提升为显式建议,而不是只讨论内部 discover 时序。
|
||||
|
||||
### 7.3 但 Gemini 仍然存在“启动口径错位”问题
|
||||
|
||||
虽然 UI 可以先起来,但 `config.initialize()` 仍是 post-render 执行,这意味着:
|
||||
|
||||
- 不补 instrumentation,就无法精确知道“用户能看到 UI”与“工具真正可用”的时间差
|
||||
- 如果只看 render 前 profile,会低估慢 MCP server 对整体体验的影响
|
||||
|
||||
所以 Gemini 提供了“可渐进可用”的产品经验,但并没有替代 qwen-code 当前文档里对 Phase 0 observability 的要求。
|
||||
|
||||
## 8. 对 qwen-code 的可执行建议
|
||||
|
||||
### 8.1 值得直接吸收的部分
|
||||
|
||||
1. **入口延迟加载交互 UI**
|
||||
- 把重 UI 模块从主入口挪到动态 import
|
||||
2. **把渲染模式开关提升为正式设计**
|
||||
- `alternateBuffer`
|
||||
- `terminalBuffer`
|
||||
- `incrementalRendering`
|
||||
3. **引入 flicker observability**
|
||||
- 参考 `useFlickerDetector()`
|
||||
4. **在全屏/alternate 模式优先落地滚动容器**
|
||||
- 先从 `ScrollableList` 风格抽象做起
|
||||
5. **MCP 状态用事件流驱动 UI**
|
||||
- 不要把“工具是否可用”缩减成一个布尔初始化结果
|
||||
|
||||
### 8.2 需要“改造后再借鉴”的部分
|
||||
|
||||
1. **渐进转 Static**
|
||||
- 可以沿用 `findLastSafeSplitPoint()` 思路
|
||||
- 但要补 height-aware threshold、flush cadence、tool/thought 边界策略
|
||||
2. **终端主题自动检测**
|
||||
- 可借鉴 `OSC 11`
|
||||
- 但必须先明确 qwen-code 的主题/refreshStatic 回路
|
||||
3. **虚拟滚动**
|
||||
- 先限制在 fullscreen / alternate buffer 场景
|
||||
- 避免直接把复杂度灌回 main-screen 基本路径
|
||||
|
||||
### 8.3 不建议直接照搬的部分
|
||||
|
||||
1. `refreshStatic()` 的 `clearTerminal` 路径
|
||||
2. 正则 Markdown parser
|
||||
3. 同步 `lowlight(common)` 高亮架构
|
||||
|
||||
## 9. 如何在 qwen-code 中使用这份调研
|
||||
|
||||
这份调研最适合用来指导三类决策:
|
||||
|
||||
1. **是否要先做“模式分层”而不是继续堆单点 patch**
|
||||
2. **是否要把长会话滚动和主屏普通输出拆成不同路径**
|
||||
3. **哪些 Gemini 做得不错,但不应被误认为 parser 或高亮架构的终局**
|
||||
|
||||
在实际实施时,应与以下文档配套阅读:
|
||||
|
||||
- `00-overview.md`:看总路线和阶段优先级
|
||||
- `02-screen-flickering.md`:看闪烁治理如何吸收 Gemini 的中层策略
|
||||
- `06-implementation-rollout-checklist.md`:看这些建议何时能进入灰度
|
||||
4. 把 post-render `config.initialize()` 当成“已解决启动问题”
|
||||
|
||||
## 9. 对现有 TUI 优化文档的具体修订要求
|
||||
|
||||
本调研对现有设计文档提出三项硬性修订:
|
||||
|
||||
1. `01-performance.md`
|
||||
- 增加“Gemini 的入口延迟加载、post-render config.initialize、事件化 MCP 状态”分析
|
||||
- 把“渐进式 MCP 可用性”从纯内部时序问题升级成用户体验设计
|
||||
2. `02-screen-flickering.md`
|
||||
- 增加 alternate/terminal buffer、incrementalRendering、flicker detector、`ScrollableList` / `VirtualizedList` 相关结论
|
||||
3. `03-rendering-extensibility.md`
|
||||
- 明确写出:Gemini 在滚动与模式层领先,但在 Markdown / parser / highlighter 架构上并没有比 qwen-code 更先进
|
||||
|
||||
## 10. 一句话判断
|
||||
|
||||
Gemini CLI 最值得 qwen-code 借鉴的,不是它的 Markdown 或高亮,而是它把 **“不同终端模式、长会话滚动、MCP 渐进状态、闪烁观测”** 做成了系统设计。这些能力一旦补进 qwen-code,现有优化文档会从“点状 patch 清单”升级成真正的 TUI 架构方案。
|
||||
Loading…
Add table
Add a link
Reference in a new issue