06 · 终端UI-pi-tui
06 — 终端 UI (pi-tui)
问题
在终端中渲染一个交互式 Agent 界面面临几个挑战:
- 终端是顺序输出的设备,如何实现”局部更新”(只刷新变化的部分)?
- 如何处理复杂的键盘输入(组合键、修饰键、按键释放)?
- 如何渲染 Markdown、代码高亮、diff、图片?
- 如何让 UI 组件可组合且高效?
pi-tui 是一个独立的终端 UI 库,解决了这些问题。
渲染架构
差分渲染
pi-tui 的核心渲染策略是差分渲染(differential rendering):
1 | Frame N (当前屏幕) Frame N+1 (新屏幕) |
为什么不直接清屏重绘:终端清屏会闪烁,且大量输出消耗带宽。差分渲染只发送变化的字符,体验流畅。
Component 接口
每个 UI 组件实现 Component 接口:
1 | interface Component { |
render() 返回的 Line 是一行终端文本(包含内容和样式信息)。渲染引擎比较前后帧的 Line[],计算差异。
Container 组合
Container 是组合组件——它持有子组件列表,递归调用 render(),将子组件的输出按布局排列。
1 | Container (垂直布局) |
两种屏幕模式
pi-tui 提供两种屏幕实现:
TuiMainScreen — Inline 模式
packages/tui/src/tui-main-screen.ts(~24K)
与 shell 共享终端——Agent 输出在 shell 的正常输出流中。适合”对话式”交互:用户输入 prompt,Agent 回复,输出追加到终端历史。
特点:
- 不使用 alternate screen buffer
- 输出追加到终端滚动历史
- 适合
pi -p或简单交互
TuiAltScreen — 全屏模式
packages/tui/src/tui-alt-screen.ts(~62K)
使用终端的 alternate screen buffer——全屏接管,退出后恢复原屏幕。适合”应用式”交互:有固定布局(header、聊天区、编辑器、footer)。
特点:
- 使用 alternate screen buffer
- 固定布局,内容在区域内滚动
- 退出后恢复原终端
- 适合
pi交互模式
组件模型
packages/tui/src/components/(18 个组件文件)
| 组件 | 文件 | 说明 |
|---|---|---|
Editor |
editor.ts(84K) |
全功能文本编辑器:undo/redo、kill ring、词导航 |
Markdown |
markdown.ts(33K) |
Markdown 渲染 + 语法高亮 |
ScrollView |
scroll-view.ts |
可滚动视图区域 |
Input |
input.ts |
单行输入 |
SelectList |
select-list.ts |
选择列表 |
Box |
box.ts |
带边框容器 |
Text |
text.ts |
纯文本 |
TruncatedText |
truncated-text.ts |
截断文本 |
HStack/VStack |
水平/垂直堆叠 | |
Image |
image.ts |
终端图片显示 |
Loader |
loader.ts |
加载动画 |
Editor 组件
packages/tui/src/components/editor.ts(84K)— 仓库中最大的 TUI 文件:
- Undo/Redo:完整的编辑历史栈
- Kill Ring:Emacs 风格的剪切板环(多次 yank 循环粘贴)
- 词导航:向前/向后跳词、跳到行首/行尾
- 多行支持:光标跨行移动、选择、删除
这是 Pi 交互模式中用户输入 prompt 的编辑器——支持多行编辑、光标移动、历史导航。
Markdown 渲染
packages/tui/src/components/markdown.ts(33K):
- 解析 Markdown AST
- 语法高亮(代码块)
- 终端颜色映射
- 自适应宽度换行
输入处理
键盘解析
packages/tui/src/keys.ts(~45K):
支持 Kitty keyboard protocol——现代终端的增强键盘协议:
- 修饰键组合(Ctrl+Shift+Alt+ 任意键)
- 按键释放事件
- 按键重复检测
- 特殊键(F1-F12、方向键、Home/End/Page Up/Down)
旧版终端使用转义序列解析(\x1b[A = 上箭头等)。
输入缓冲
packages/tui/src/stdin-buffer.ts(~12K):
终端输入可能以不完整的块到达(特别是转义序列可能跨 chunk)。stdin-buffer 负责缓冲和分割输入,确保完整的按键事件被正确解析。
快捷键系统
packages/tui/src/keybindings.ts(~10K):
KeybindingsManager + TUI_KEYBINDINGS 管理快捷键映射。支持可配置的快捷键绑定,与 coding-agent 的 KeybindingsManager 集成。
终端能力
packages/tui/src/terminal.ts(~19K)
Terminal 接口抽象终端能力:
1 | interface Terminal { |
ProcessTerminal 是基于 Node.js process.stdout/process.stdin 的实现。
终端图片
packages/tui/src/terminal-image.ts(~22K):
支持三种终端图片协议:
- Kitty graphics protocol:现代终端的图片渲染协议
- iTerm2 inline images:iTerm2 的 base64 图片协议
- Sixel:老式图形协议(检测支持)
用户可以粘贴图片到 Pi 的输入中,图片会通过这些协议渲染。
LaTeX 渲染
packages/tui/src/latex.ts(~33K):将 LaTeX 公式渲染为终端可显示的形式。
工具函数
packages/tui/src/utils.ts(~37K):
visibleWidth():计算字符串的可见宽度(排除 ANSI 转义码)- ANSI 处理:解析、生成、过滤 ANSI 转义序列
- 文本换行:按可见宽度换行,保留 ANSI 样式
packages/tui/src/fuzzy.ts:模糊匹配,用于自动补全。
packages/tui/src/autocomplete.ts(~24K):AutocompleteProvider 和 CombinedAutocompleteProvider——用于 slash 命令、文件路径、模型名称等的自动补全。
布局引擎
packages/tui/src/layout.ts(~16K):
Box model 布局引擎,计算每个组件的位置和大小:
- 固定尺寸(
width/height) - 弹性尺寸(
flex) - 边距和填充(
padding/margin) - 对齐方式
与 Agent 的连接
pi-tui 是纯渲染库——它不知道 Agent 的存在。连接在 coding-agent 的 InteractiveMode 中建立:
1 | AgentSession (coding-agent) |
1 | ┌─ TuiMainScreen / TuiAltScreen ──────────────────────┐ |
学习要点
- 差分渲染是终端 UI 的核心优化:比较前后帧,只输出变化的字符,避免闪烁和带宽浪费
- Component 接口的
render() → Line[]设计:简单的返回值让差分比较容易实现,组件可自由组合 - 两种屏幕模式对应不同交互模式:Inline(对话式)vs AltScreen(应用式),选择取决于产品定位
- 键盘输入处理远比想象复杂:转义序列跨 chunk、修饰键组合、Kitty protocol、按键释放——需要专门的解析器和输入缓冲
- TUI 与 Agent 完全解耦:
pi-tui不知道 Agent 存在,连接在InteractiveMode中通过事件订阅建立。这让 TUI 库可以独立复用