# 第四阶段：TUI -- 终端用户界面

> 对应源文件：`packages/tui/src/`

## 概述

`pi-tui` 是一个**从零构建的终端 UI 框架**，不依赖 blessed、ink 等第三方库。它为 pi 提供了交互式终端界面，包括：

- 差异化渲染（只更新变化的行）
- 组件系统（Component 接口）
- 覆盖层系统（Overlay，用于对话框）
- 键盘输入处理（含 Kitty 协议支持）
- 自动补全
- 终端内图片显示（iTerm2/Kitty 协议）

## 核心概念

### Component 接口

所有 UI 元素必须实现 `Component` 接口：

```typescript
interface Component {
  render(width: number): string[];   // 渲染为文本行数组
  handleInput?(data: string): void;  // 可选：处理键盘输入
  invalidate(): void;                // 清除缓存，强制重绘
}
```

这是一个极简的设计：
- `render()` 接收终端宽度，返回一个字符串数组（每个元素 = 一行）
- 字符串包含 ANSI 转义序列（颜色、加粗等）
- 没有虚拟 DOM、没有状态树 -- 就是字符串

### Container

`Container` 是最基本的容器组件，按顺序排列子组件：

```typescript
class Container implements Component {
  children: Component[] = [];
  addChild(component: Component): void;
  removeChild(component: Component): void;
  render(width: number): string[] {
    // 依次渲染每个子组件，合并行数组
    return this.children.flatMap(child => child.render(width));
  }
}
```

### TUI 类

`TUI` 继承自 `Container`，是整个终端 UI 的根节点。它增加了：

| 功能 | 说明 |
|------|------|
| **差异化渲染** | 比较上一帧和当前帧，只重写变化的行 |
| **焦点管理** | 跟踪哪个组件接收键盘输入 |
| **Overlay 系统** | 在主内容上显示模态对话框 |
| **硬件光标** | 通过 `CURSOR_MARKER` 定位光标（用于 IME 输入法） |
| **节流渲染** | 最小 16ms 间隔，避免过度刷新 |

### 渲染循环

```
组件状态变化 → requestRender()
  ↓ 节流（≥16ms 间隔）
scheduleRender()
  ↓
doRender()
  1. 渲染所有子组件 → 得到 string[]
  2. 渲染所有可见 Overlay → 合成到 string[]
  3. 与上一帧比较
  4. 只重写变化的行（光标定位 + 清除 + 写入）
  5. 处理硬件光标位置（CURSOR_MARKER）
```

差异化渲染的关键：光标在终端中是有"位置"的，通过 ANSI 转义序列移动光标到变化的行，然后用清行+写入替换内容。这避免了全屏重绘的闪烁问题。

---

## Overlay 系统

Overlay 是一个浮动在主内容上方的组件，用于：
- 选择对话框（`/model`、`/theme`）
- 确认对话框（bash 权限确认）
- 文本输入对话框
- 扩展的自定义 UI

```typescript
// 显示 overlay
const handle = tui.showOverlay(component, {
  width: "80%",           // 宽度为终端宽度的 80%
  maxHeight: "50%",       // 最高占终端的一半
  anchor: "center",       // 居中
  margin: 2,              // 距边缘 2 行/列
});

// 控制 overlay
handle.setHidden(true);   // 临时隐藏
handle.focus();           // 获取焦点
handle.hide();            // 永久关闭
```

Overlay 合成的原理：在主内容的 string[] 之上，按 row/col 位置"覆盖"overlay 的内容。实际上是字符串级别的拼接操作（按列裁剪、替换），而不是真正的图层叠加。

---

## 键盘输入处理

`keys.ts`（44KB）处理各种终端的键盘输入编码：

```
原始输入 → parseKey() → KeyId 标准化
```

键盘输入在不同终端中有不同编码：
- 普通终端：`\x1b[A`（上箭头）
- Kitty 协议：`\x1b[1;1A` 或 CSI u 序列
- xterm modifyOtherKeys：另一套编码

`keys.ts` 把所有这些统一为 `KeyId`（如 `"up"`, `"ctrl+c"`, `"shift+enter"`）。

`matchesKey()` 函数用于在组件中检查按键：

```typescript
handleInput(data: string) {
  if (matchesKey(data, "ctrl+c")) {
    this.abort();
  } else if (matchesKey(data, "enter")) {
    this.submit();
  }
}
```

---

## 自动补全

`autocomplete.ts` 实现了多来源的自动补全：

```
输入 "/" → 触发命令补全
输入 "@" → 触发文件路径补全
输入 "model:" → 触发模型名补全
```

自动补全系统是可扩展的，扩展可以通过 `ctx.ui.addAutocompleteProvider()` 添加自己的补全源。

---

## 终端图片

`terminal-image.ts` 支持在终端中显示图片，使用两种协议：
- **iTerm2 内联图片协议**：base64 编码图片嵌入 OSC 序列
- **Kitty 图形协议**：更高级的图片传输

自动检测终端能力，回退到文字描述。

---

## 与 coding-agent 的集成

coding-agent 的 `InteractiveMode` 使用 TUI 组织界面布局：

```
┌───────────────────────────────────────────────┐
│ Header（模型信息、version）                     │
├───────────────────────────────────────────────┤
│ Chat 区域                                      │
│  user: 帮我读取 main.ts                        │
│  assistant: 好的，让我读取这个文件...             │
│  ┌─ read main.ts ──────────────────────────┐  │
│  │ 1  import { readFile } from "fs";       │  │
│  │ 2  ...                                  │  │
│  └─────────────────────────────────────────┘  │
│  assistant: 这个文件是...                      │
├───────────────────────────────────────────────┤
│ Widget 区域（扩展自定义）                       │
├───────────────────────────────────────────────┤
│ Editor（多行输入框 + 自动补全）                  │
├───────────────────────────────────────────────┤
│ Footer（模型名 | token 数 | 快捷键 | 状态）    │
└───────────────────────────────────────────────┘
```

---

## 小结

```
TUI 包
├── 核心
│   ├── Component 接口     → render(width) → string[]
│   ├── Container          → 垂直排列子组件
│   └── TUI 类             → 差异化渲染 + 焦点管理
│
├── Overlay 系统
│   ├── 浮动对话框
│   ├── 可配置位置和大小
│   └── 焦点栈管理
│
├── 键盘处理
│   ├── 多终端协议支持
│   ├── matchesKey() 统一检查
│   └── 可配置快捷键
│
├── 其他功能
│   ├── 自动补全（多来源）
│   ├── 终端图片（iTerm2/Kitty）
│   └── Undo/Redo 栈
│
└── 设计特点
    ├── 零依赖（不用 blessed/ink）
    ├── 极简接口（Component 只有 2 个方法）
    └── 增量渲染（只更新变化的行）
```
