TUI 架构 · 一个 Elm 式的终端界面

进 Part 5,钻进那个约 55 万行(含测试)的终端 UI。这一章看 grok-pager 的整体架构:Elm 式的 Action→dispatch→Effect 三段式、一个精心排序的 biased 事件循环、专用读写线程,以及为什么「派发层不碰终端/网络/文件」是它最值得学的决定。

作者 David更新于 第 42 篇(共 60 篇)

TUI 架构 · 一个 Elm 式的终端界面

前面四部分讲的都是「引擎」。Part 5 钻进最外层——你真正看到、敲键的那个终端界面(xai-grok-pager + -render,两者 src/ 下的 Rust 代码合计约 55 万行,不含测试约 26.5 万行,基于 ratatui)。别以为 TUI 是薄壳,它的架构相当讲究。

Elm 式三段式

app/actions.rs 的模块注释写得很直白:event → dispatch → effect。三个核心类型:

  • Action(约 300 个变体):同步、无副作用的用户意图,由输入产生。SendPrompt、CancelTurn、SwitchModel……
  • Effect:异步副作用,丢进 JoinSet 跑。
  • TaskResult:异步任务的结果,包回成 Action::TaskComplete 再走一遍 dispatch。
图表加载中…

最值得学的决定:派发层 sans-IO

app/dispatch/ 的模块注释一句话点破:它做的是「Action → 状态改动 + Effect」,从不碰终端、网络、文件系统——因此「不用 tokio、不用终端就能完整测试」。所有真正的副作用被隔离在 effects/。这个「纯派发 + 隔离副作用」的分层,是让一个几百个 Action 的复杂 TUI 还能测的关键。ACP 事件(来自 Part 1 的 SessionActor)则不走 Action,由 acp_handler/ 直接转成 scrollback 变更,需要副作用时同样排成 Effect。

一个排序讲究的事件循环

event_loop.rs 的核心是一个 biased 的 tokio::select!,分支顺序本身就是设计:取消/退出 > 写线程回执与阻塞报告 > ACP(仅当输入队列空时才收,免得 token 洪流饿死按键)> 后台任务 > 进度 > 输入 > 各种定时器与配置/外观监听 > 语音 STT(最后,因为热麦会持续推送临时转写,突发时能塞满 128 槽的通道,绝不能饿死别的)。定时器分支用 match at { Some=>sleep_until, None=>pending() } 的惯用法,只在需要时武装。

两个专用线程也值得记:

  • 读线程:专门 poll + read 键盘鼠标,转发到 tokio 通道——select! 里别的分支赢了,通道的 recv future 被丢弃重建也不丢唤醒;而直接在 select! 里 poll crossterm 的 EventStream 不是 cancel-safe 的(crossterm 的已知 issue)。
  • 写线程:缓一帧、把字节送出去,把 PTY 背压和 tokio 循环隔开。

状态与视图

AppView 持有全部状态(约 220 个字段:活动视图、各 agent、滚动、通知服务、鉴权/信任……)。ActiveView 分 Welcome / Agent(id) / AgentDashboard;每个 agent 有自己的 AgentView(输入 + 绘制,各种 overlay 门控输入)。绘制节流靠 min_draw_interval,空帧直接跳过光标转义。

一个端到端的小例子是状态行([ui.status_line],默认关):底部一行显示模型、上下文占用、花费、目录,或者你自己脚本的输出。它的数据契约在独立的小 crate xai-grok-status-line 里:agent 把 StatusLineContext 经 ACP 推给客户端,acp_handler 把它存进 AgentView;command 模式跑脚本则完整走一遍三段式——Effect::RunStatusLineCommand 进 JoinSet,结果以 TaskResult 回来;定时刷新也只是 select! 里又一个按需武装的定时器分支。

和《从 LLM 到 Coding Agent》对照

教学最小实现的「界面」通常就是 print + readline。grok 的 TUI 是一个完整的 Elm 式应用:sans-IO 可测派发、饿死感知的事件循环、cancel-safe 的读线程、背压隔离的写线程。这是「命令行玩具」和「你愿意每天用的终端产品」之间的全部距离。下一章看它怎么把文字画到屏幕上。


上一篇:自定义模型 · 下一篇:渲染管线与 scrollback

本页目录