# TUI 应用层

> KimiTUI 协调器与控制器分工、反向 RPC 的模态协调、slash 命令注册表、流式渲染，以及只经 SDK 的引擎边界。

- 作者：David（道雾轩）
- 专栏：Kimi Code 源码解读（https://daiw.org/manual/kimi-code.md）
- 最后更新：2026-09-19
- 原文：https://daiw.org/manual/kimi-code/tui-app
- 转载与引用：请注明出处并附原文链接（https://daiw.org/about/copyright）

`apps/kimi-code/src/tui/` 是把 [pi-tui](https://daiw.org/manual/kimi-code/pi-tui) 组装成实际应用的一层。`apps/kimi-code/AGENTS.md` 给出了入口链：

```text
src/main.ts -> src/cli/commands.ts -> src/cli/run-shell.ts -> SDK KimiHarness -> src/tui/kimi-tui.ts
```

## 结构

```text
tui/
├── kimi-tui.ts          KimiTUI 协调器（4190 行）
├── tui-state.ts         全局 UI 状态
├── controllers/         控制器（按关注点拆分，15 个）
├── commands/            slash 命令的声明、解析与注册表
├── components/          chrome/ dialogs/ editor/ media/ messages/ panes/
├── reverse-rpc/         反向 RPC（审批、提问）
├── theme/               主题与颜色令牌
├── constant/ utils/     TUI 专用常量与工具
├── banner/              启动横幅
├── goal-queue-store.ts  待办目标队列
└── easter-eggs/         彩蛋
```

`KimiTUI` 被定位为**协调器**：持有状态、负责启停顺序、布局与编辑器接线、会话生命周期和 slash 命令分发，但不该堆积事件路由与渲染逻辑——那些归控制器。它从 v0.29.1 的 3085 行长到了 4190 行，可见这条约束需要持续维护。

`tui-state.ts:99` 在创建状态时决定用哪套渲染器：

```ts
const fullscreen = process.env['KIMI_CODE_TUI_FULL_SCREEN'] === '1';
const ui =
  fullscreen
    ? new TuiAltScreen(terminal, undefined, undefined, {
```

默认是主屏的 `TuiMainScreen`，环境变量打开后才用全屏的 `TuiAltScreen`；此时鼠标被接管，OSC 8 链接点击与 Windows 右键粘贴都改由应用自己处理。

## 控制器

`controllers/` 按关注点拆开，每个只管一件事：

| 控制器 | 职责 |
| --- | --- |
| `session-event-handler.ts`（1300 行） | 按事件类型分发 SDK 会话事件 |
| `streaming-ui.ts`（910 行） | **流式渲染** |
| `session-replay.ts`（899 行） | 恢复会话时的[回放](https://daiw.org/manual/kimi-code/records-replay)，复用实时渲染路径 |
| `editor-keyboard.ts` | 编辑器键盘、退出快捷键、外部编辑器 |
| `subagent-event-handler.ts` | [子代理](https://daiw.org/manual/kimi-code/subagents)事件与 swarm 进度 |
| `subagent-activity-store.ts` | 后台子代理的活动记录，供任务浏览器查看 |
| `tasks-browser.ts` | [后台任务](https://daiw.org/manual/kimi-code/background)浏览 |
| `auth-flow.ts` | 登录流程 |
| `clipboard-image-hint.ts` | 剪贴板出现新图片时提示可以粘贴 |
| `btw-panel.ts` | `/btw` 旁路提问面板 |

另有 `notify`、`cache-hint-controller`、`plugin-update-notifier`、`staging-leases`、`survey-controller` 等较新的控制器。

`session-event-handler.ts` 的核心是一张分发表（`:278` 起，节选）：

```ts
case 'turn.started': this.handleTurnBegin(event); break;
case 'turn.ended': this.handleTurnEnd(event, sendQueued); break;
// ...
case 'assistant.delta': this.handleAssistantDelta(event); break;
case 'hook.result': this.handleHookResult(event); break;
case 'thinking.delta': this.handleThinkingDelta(event); break;
case 'tool.call.started': this.handleToolCall(event); break;
```

子代理事件单独交给 `SubAgentEventHandler`，是因为它们要**挂到父工具卡片下折叠显示**——README 说子代理在 isolated contexts 里跑，就是为了让主对话保持干净，UI 上也不能把子代理的每次工具调用都平铺出来。

## 反向 RPC

```text
reverse-rpc/
├── approval/            审批：adapter / controller / handler
├── question/            提问：controller / handler
├── base-controller.ts   基于 Promise 的面板控制器基类
├── modal-coordinator.ts 模态协调
└── types.ts             面板数据形状
```

这是 SDK 审批与提问回调在 TUI 侧的落地——引擎请求[审批](https://daiw.org/manual/kimi-code/permission)或提问时，需要在终端弹出交互，再把用户的选择转回 SDK 响应。

协调分两层。`ReverseRpcController` 保证同一类请求一次只显示一个面板，其余按到达顺序排队——多个并行子代理同时要审批时正是这种情况；用户选了「本会话都允许」，队列里动作相同的审批会被自动放行。`ReverseRpcModalCoordinator` 再管审批与提问之间的互斥（`modal-coordinator.ts:68`）：

```ts
private show(entry: ReverseRpcModalEntry): void {
  const active = this.active;
  if (active === null) {
    this.active = entry;
    entry.show();
    return;
  }

  if (active.owner === entry.owner) {
    this.active = entry;
    entry.show();
    return;
  }
```

终端没有真正的窗口层级，只能靠协调器串行化，避免两个模态互相覆盖或界面错乱。

## slash 命令

`commands/registry.ts` 的 `BUILTIN_SLASH_COMMANDS` 列出 40 余个内置命令，每条带名字、别名、描述、优先级、可用时机（`always` 或 `idle-only`）与参数补全。命令清单能反映产品面：

```text
/login /logout /provider /model /effort   认证与模型
/settings /reload /experiments /theme      配置
/new /sessions /fork /title /undo /copy    会话操作
/permission /yolo /auto /plan              权限与计划模式
/goal /swarm /tower /tasks /btw            长程与协作
/mcp /plugins /add-dir                     扩展与工作区
/web /remote-control /usage /status        其它
```

动态命令另有两路：skill 生成 `/skill:<name>`（内置 skill 直接用名字，见 [Skills](https://daiw.org/manual/kimi-code/skills)），插件命令生成 `/<插件>:<命令>`（见[插件与市场](https://daiw.org/manual/kimi-code/plugins)）。按 `write-tui` 的约定，`commands/` 只负责声明、解析与类型，真正的执行在 `KimiTUI` 的命令处理段里分发。

## 流式渲染

`streaming-ui.ts` 是体验的关键。模型流式返回时要：

1. 增量追加文本，不重绘整屏（靠 pi-tui 的[差分渲染](https://daiw.org/manual/kimi-code/pi-tui)），刷新按 50ms 节流（`STREAMING_UI_FLUSH_MS`）。
2. 思考内容与正文分开展示。
3. 工具调用实时显示参数预览与进度。
4. 压缩、子代理、后台 agent 的状态同样在这里汇总进 transcript。

它与 pi-tui 的关系是：pi-tui 提供「怎么高效画」，`streaming-ui.ts` 决定「画什么、何时画」。

## 状态与主题

`tui-state.ts` 是唯一的全局 UI 状态形状；`goal-queue-store.ts` 把 `/goal next` 排队的[目标](https://daiw.org/manual/kimi-code/goal-plan)存进会话目录的 `upcoming-goals.json`。`theme/` 是颜色的唯一来源：`colors.ts` 的 `ColorPalette` 定义语义令牌，`theme-schema.json` 描述自定义主题，内置 skill `custom-theme` 让用户能对话式定制；CI 守卫禁止直接使用 `chalk.red` 这类具名颜色。

## 与引擎的边界

根 `AGENTS.md` 的硬约束：

> `apps/kimi-code`: the CLI / TUI application. It consumes core capabilities through `@moonshot-ai/kimi-code-sdk` and **must not depend directly on engine packages**.

`src/tui/` 下确实只认 SDK（`KimiHarness`、`Session` 与事件类型），没有直接 import 引擎包。这让 TUI 与引擎能独立演进，也让同一套 TUI 逻辑理论上可以对接远程引擎（比如通过 [kap-server](https://daiw.org/manual/kimi-code/kap-server)）。严格说，CLI 的 print 模式与配置校验（`src/cli/v2/`）、插件市场工具（`src/utils/plugin-marketplace.ts`）目前仍直接引用 `@moonshot-ai/agent-core-v2` 的导出，`kimi web` 则直接启动 kap-server。

`AGENTS.md` 还要求写 TUI 时用仓库自带的 skill：

> When writing or modifying its terminal UI, use the `write-tui` skill (`.agents/skills/write-tui/SKILL.md`).

**仓库把自己的 TUI 编写规范做成了 skill**——架构地图、新功能落点、测试位置、主题机制与对话框设计规范（`DESIGN.md`）都在里面，给 AI 助手看，也给人看。这是 agent-assisted 开发的一个有意思的实践。

第 4 部分结束。下一篇进入[minidb](https://daiw.org/manual/kimi-code/minidb)。
