13. U2 总结
前面十二篇把 U2 阶段的每个模块都逐函数拆了一遍。这一篇做个收束:U2 整体交付了什么、对应哪些计划单元、做了哪些关键技术决策,以及刻意留到后面的部分。
U2 对应主屏计划 2026-06-25-003 的 U2(静态主屏组件)与 U3(输入框输入与换行),以及主题计划 2026-06-29-001 的 U2(半行背景块原语)与 U3(接入背景块与主题)。它由两个 commit 叠加完成:dd15b678(构建交互式主屏)和 5432e018(迁移到 Jotai 并拆分模块)。
到目前为止完成了什么
U1 的 App 还只是一个静态空壳。U2 把它变成了一个真正能交互的主屏:
- 可组合的主屏布局:Header(logo + 版本)、正文转录区、cwd 行、输入框、状态栏五块,由
HomeScreenView纵向组合。 - 底部吸附布局:cwd、输入框、状态栏在任意终端尺寸下都吸在底部,正文与 cwd 之间恰好一个空行,靠
homeScreenAtoms里的布局与 spacer 计算实现。 - 正文转录与滚动:
bodyRows把类型化条目编译成渲染行,BodyPane处理换行、滚动窗口和滚动条,支持滚轮与 PageUp/PageDown。 - 可输入可提交的输入框:
composerAtoms持有状态,PromptComposer/下的几个聚焦模块分别负责输入、可视文本、光标、渲染;支持换行、Unicode 光标、64 KiB 限长、提交快照。 - Git 感知的 cwd:
gitStatus解析 porcelain 输出,cwd 行显示~相对路径加分支/改动标记。 - 内部主题与半行背景块:
themeConfig集中语义令牌,半行▄/▀glyph 给用户消息块和输入框做无缝背景。
关键技术决策
-
共享状态用 Jotai,局部编辑状态仍可内聚。主屏有十几个被多组件读取的派生值,全用 props 会让叶子组件参数爆炸。重构把它们收进 atoms,组件按需订阅。这也是第二个 commit 的主线。
-
纯函数 helper 与展示组件分离。
bodyRows、layout、cwdLine、backgroundBlock等放在src/libs/tui/下,src/components/只留.tsx。布局、换行、Git 解析这些容易出错的逻辑被隔离成可单测的纯函数。 -
按 200 行约定拆分文件。
HomeScreen和PromptComposer都从单文件拆成了 colocated 文件夹(index + 子模块),符合tui/AGENTS.md的文件大小约定。输入框尤其明显:状态、输入、可视文本、光标、帧渲染各自成文件。 -
背景渲染是内部原语,且 fallback-aware。半行块只用在消息块和输入框这两个局部,没有去刷整屏 canvas(早期尝试会露行缝)。
shouldRenderBackground被保留成变量而非写死,给将来按终端能力降级留了接口。所有“含义”都用非颜色标记(ERROR:、•、❯、*/+/%、滚动条字形)承载,去色后依然可读。 -
手动光标定位必须随布局同步验证。光标用 Ink cursor API 手动算,
y要叠加 composerTop、背景 padding 和 Ink 行原点偏移。任何改动 body 高度、spacer、换行或背景行的改动,都要重新确认光标落在当前文本行上。 -
鼠标与 Git 都要稳健降级。Git 状态读取整段
try/catch加超时,失败就不显示而不是崩溃;鼠标上报只在 TTY 下开关,非 TTY 跳过。
刻意留到后面的部分
U2 只搭了可交互的前端外壳,没有接任何后端行为。提交 prompt 目前只是把它追加进正文,onPromptSubmit 默认是 noop。主题也没有任何用户可见配置(没有 /theme、没有持久化、没有自定义主题文件)。这些都按计划留给后续单元:
- 主屏计划的 U4–U8:Rust JSON-RPC stdio 后端、协议、受控启动器、后端客户端、提交/ACK 接线。
- 之后的 U9–U13:SQLite 会话存储与
/resume、独立可执行文件、跨平台分发与 Release 流水线。
换句话说,U2 把“看得见、摸得着”的主屏先做扎实——布局稳、输入顺、转录清楚——为后面接入真正的 agent 循环打好 UI 地基。