Part IV · Interface & Extensions

11 TUI 差分渲染 为什么值得手写 3.2 万行终端 UI

已发布包规模 32374 行内部依赖 0
§1 Mental Model 一句话心智模型

先记住这一句

agent 的输出是流式且频繁重排的:一段文字在生成中不断变长、工具结果随时插进来、滚动位置要保持。全量重绘会闪,所以 pi 自己写了一个只更新变化行的终端 UI 库。

3.2 万行手写终端 UI,是本仓库最容易被质疑的投入。判断它值不值,要先看它解决的问题有多特殊。

§2 Evidence 代码实证

它自称解决什么

packages/tui/README.md原文 L3、L8–L9(并置)
Minimal terminal UI framework with differential rendering and synchronized output
for flicker-free interactive CLI applications.

- **Differential Rendering**: Updates only changed lines or viewport rows
- **Application-owned Scrolling**: Alternate-screen viewport supports mouse, trackpad, …
两个关键词:differential rendering(只重画变化的行)与 synchronized output(终端的同步输出转义序列,让一帧原子地出现,不出现撕裂)。tui/README.md:8

零依赖

packages/tui 的 @earendil-works/* 内部依赖为空,也不依赖 React。它不知道 LLM、agent、会话的存在——第 01 章那张依赖图里唯一一个「叶子」级的大包。

这带来一个实际结果:它是这个仓库里最容易被单独拿走用的部分,而且 coding-agent 的无头模式(第 14 章)不需要 mock 掉任何 UI——直接不加载它就行。

还有原生模块

packages/tui/native/darwin 与 native/win32 各有一份 README——说明为了终端行为一致性,某些平台特性下沉到了原生层。这是「自己写 UI 库」成本的一部分:你要为每个平台的终端怪癖负责。

§3 Design 设计动机与权衡

为什么现成方案不够

Ink(React for CLI)之类的方案针对的是「表单、进度条、列表」这类结构化且更新局部的 UI。agent 的输出形态不同:

「只更新变化的行」+「一帧原子输出」正好对着这四条。用通用框架也能做,但你会花大量时间对抗它的重绘模型——而重绘模型正是这里唯一重要的东西。

代价

3.2 万行 + 两个平台的原生模块 + 终端兼容性的长尾(docs/terminal-setup.md、docs/tmux.md、docs/termux.md、docs/windows.md 四份文档专门讲这个)。这是一条只有在「终端体验就是产品」时才划算的路。

对照 第 15 章:pi 拒绝做权限系统的理由是「做不彻底不如不做」;这里的选择是反过来的——终端体验做不彻底也不如不做,所以做彻底。判据一致:看这件事是不是产品的核心。

§4 Limits 边界与失效条件
§5 Takeaway 拿走什么
§ Self-check 自测

给场景,判断 pi 会怎么做

场景你的 agent CLI 每秒几十次 token 级更新,用户还会往回滚动看历史。直接用 Ink(React for CLI)最先撞到的问题是什么?

看答案

B。pi 自研 TUI 的判据就是「重绘模型是不是你要解决的主要问题」。是——就自己写;不是——用现成的。

场景把 @earendil-works/pi-tui 装进自己的项目,它会带上什么依赖?

看答案

C。tui 是依赖图里唯一的「叶子」级大包,不知道 LLM 的存在——这也是它最容易被单独复用的原因。

基于 earendil-works/pi @ ac4ac9e(2026-08-07 快照)。
仓库 commit 速度约 428/月,行号引用仅对该 SHA 有效。