# Knowledge Interaction Surfaces Terminal UI

> 覆盖 Blade React + Ink TUI 的初始化、输入协议、消息与工具渲染、确认队列、Session 切换、流式缓冲和 Runtime 清理。进入时机：修改终端键盘行为、粘贴、Ctrl+C、审批与 问答、流式输出、Static 布局、会话恢复或 slash command 展示。 不包含：Headless/Print 输出（见 ../cli-bootstrap-headless-and-print/）、Web React 状态（见 ../web-client/）。 关键词：AppWrapper, BladeInterface, MessageArea, useCommandHandler, useAgent, useConfirmation, parseTerminalInput, Static, rawStreamRenderer。

- Skill: `echovic/knowledge-interaction-surfaces-terminal-ui` (Agent Skill)
- Install (CLI): `npx skillmds@latest add echovic/knowledge-interaction-surfaces-terminal-ui`
- Raw SKILL.md: https://api.skillmd.com/api/skills/echovic/knowledge-interaction-surfaces-terminal-ui/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: echovic (https://skillmd.com/u/echovic)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/echovic/knowledge-interaction-surfaces-terminal-ui

---


## Module Structure

TUI 以单个 vanilla Zustand Store 投影当前 Session，React Hook 只持有活动 Agent/Runtime
和输入交互生命周期；durable 消息、权限模式与恢复数据仍由 Session 服务管理。

### Directory Layout
- `packages/cli/src/ui/App.tsx` — 版本提示、Workspace Trust、资源与 Hook 初始化
- `packages/cli/src/ui/components/BladeInterface.tsx` — 主界面、阻塞弹窗与 Session 启动路由
- `packages/cli/src/ui/components/MessageArea.tsx` — 单 Static 根、流式块和 raw tail 渲染
- `packages/cli/src/ui/components/` — 输入、确认、问答、状态和工具展示组件
- `packages/cli/src/ui/hooks/` — Agent、命令、输入、确认、Ctrl+C 与缓冲生命周期
- `packages/cli/src/ui/input/terminalInput.ts` — bracketed paste 与终端控制序列解析
- `packages/cli/src/ui/utils/` — LoopEvent 投影、Session 激活、Markdown 和工具格式化
- `packages/cli/src/store/` — React 与非 React 代码共享的当前进程状态
- `packages/cli/src/slash-commands/` — TUI 内置与自定义命令路由

### Key Entry Points
- `AppWrapper` in `packages/cli/src/ui/App.tsx` — 完成信任判断后挂载主界面
- `BladeInterface` in `packages/cli/src/ui/components/BladeInterface.tsx` — 组合输入、弹窗、消息与状态区
- `useCommandHandler()` in `packages/cli/src/ui/hooks/useCommandHandler.ts` — 单轮命令、取消和恢复编排
- `useAgent()` in `packages/cli/src/ui/hooks/useAgent.ts` — Agent/SessionRuntime 的唯一 React 所有者
- `createLoopEventHandler()` in `packages/cli/src/ui/utils/loopEventHandler.ts` — LoopEvent 到 TUI Store 的投影

## Gotchas
- 取消顺序不可交换：先 `dismissAll()` 释放阻塞审批，再排空流式缓冲，再 abort，最后 finalize；先 abort 会把审批拒绝误判为普通 permission denial，晚到 `stream_end` 还可能重复提交 (`packages/cli/src/ui/hooks/useCommandHandler.ts`, `packages/cli/src/ui/hooks/useConfirmation.ts`)
- `createAbortController()` 会主动中止并替换已有 controller；命令下层必须复用外层 controller，否则 finally 的所有权检查失败并让 `isProcessing` 永久卡住 (`packages/cli/src/ui/hooks/useCommandHandler.ts`, `packages/cli/src/store/slices/commandSlice.ts`)
- `MessageArea` 只能有一个 Ink `Static` 根；条件挂载第二个 Static 曾让 Ink 保留已释放 Yoga 节点并在后续 commit 崩溃 (`packages/cli/src/ui/components/MessageArea.tsx`, `docs/testing/tui-single-static-root-ownership-evidence.md`, `git:c68977b7`)
- 高频 Markdown tail 绕过 React 直接写 stdout；在 Static 增长、finalize、清屏、resize 或历史展开前必须先清理 raw renderer，否则终端会残留或错位 (`packages/cli/src/ui/components/MessageArea.tsx`, `packages/cli/src/ui/utils/rawStreamRenderer.ts`)
- `clearCount` 是 Static 的唯一强制重挂边界，finalize 时还必须先 `eraseScreen + cursorTo(0,0)`；仅清屏不归位会在顶部留下大段空白 (`packages/cli/src/ui/components/MessageArea.tsx`)
- 一次 stdin 回调可能是完整 IME 文本或批量自动化输入，不能逐字符假设；bracketed paste marker 还可能被 Ink 去掉 ESC 或跨 chunk 到达 (`packages/cli/src/ui/input/terminalInput.ts`, `docs/reference/tui-terminal-input.md`)
- 未闭合 paste 会留在 parser buffer，超过消息字符预算后一直丢弃到 end marker；不要把半段 paste 当普通输入提交 (`packages/cli/src/ui/input/terminalInput.ts`)
- 第一次 Ctrl+C 在执行中只取消任务并显示二次退出提示，3 秒内第二次才走 GracefulShutdown；直接恢复 Ink 的默认 `exitOnCtrlC` 会跳过资源释放 (`packages/cli/src/ui/hooks/useCtrlCHandler.ts`, `packages/cli/src/blade.tsx`)
- Session 激活必须先完成 fork/load 和 UI-safe 转换，再 `cleanupAgent()`，最后一次性恢复新 Store；清理失败时不能提前切换界面身份 (`packages/cli/src/ui/utils/sessionActivation.ts`)
- fork 提示是 UI-only assistant message，不进入恢复的 model context；把 visible messages 重建为上下文会污染后续模型输入 (`packages/cli/src/ui/utils/sessionActivation.ts`, `packages/cli/src/ui/utils/sessionContext.ts`)
- 阻塞弹窗通过 `display="none"` 隐藏主界面而不卸载，目的是保留 Static 和输入状态；改成条件卸载会重复打印历史并丢失输入组件状态 (`packages/cli/src/ui/components/BladeInterface.tsx`)

## Architecture
- `AppWrapper` 先处理版本提示和 Workspace Trust，再解析可信资源与 Hooks；选择“安全继续”只跳过信任授予，不跳过应用初始化 (`packages/cli/src/ui/App.tsx`)
- `useAgent` 同时拥有 Agent 和 SessionRuntime 引用；Session ID 或 workspace 改变时先幂等销毁旧 Agent，再 dispose 整个 Runtime 边界 (`packages/cli/src/ui/hooks/useAgent.ts`, `git:959149a8`)
- TUI Store 不做持久化：会话写入 JSONL、配置写入 ConfigService；Store 只保存当前进程投影和渲染缓冲 (`packages/cli/src/store/vanilla.ts`)
- `createLoopEventHandler` 为每轮创建闭包，以 `streamFinalized` 隔离 abort、fallback 和正常 `stream_end`；跨轮复用 handler 会让上一轮终态污染下一轮 (`packages/cli/src/ui/utils/loopEventHandler.ts`)
- 流式内容有 Hook 层批缓冲、Store 外模块级 chunks、Markdown 增量缓存和 raw tail 四层状态，正常结束与取消都必须按所有权逐层排空或丢弃 (`packages/cli/src/ui/hooks/useStreamingBuffer.ts`, `packages/cli/src/store/slices/sessionSlice.ts`, `packages/cli/src/ui/utils/markdownIncremental.ts`)
- Confirmation Hook 把请求串成单活动项队列，并为每项绑定 AbortSignal；工具执行并不直接依赖某个具体 Ink 组件 (`packages/cli/src/ui/hooks/useConfirmation.ts`)

## Decisions
- React 与 Agent/工具共享 vanilla Zustand 实例，但只暴露 actions 和 selectors，以避免非 React 路径另建状态源 (`packages/cli/src/store/vanilla.ts`, `packages/cli/src/store/selectors/index.ts`)
- 已完成历史使用 Ink Static，最高频 tail 使用 raw renderer，是为兼顾原生 scrollback 与流式性能；这两条路径通过 message ID 和 finalize 标记去重 (`packages/cli/src/ui/components/MessageArea.tsx`)
- TUI 启用 bracketed paste 并显式关闭 focus reporting，退出时重复恢复终端模式，避免异常关闭把宿主 shell 留在特殊输入状态 (`packages/cli/src/ui/hooks/useTerminalInputModes.ts`, `docs/reference/tui-terminal-input.md`)

## Patterns
- Session 级模型、reasoning、service tier、verbosity 和 communication style 都先刷新 Runtime、再持久化 metadata；持久化失败会回滚 Runtime，避免界面与重启状态分叉 (`packages/cli/src/ui/hooks/useAgent.ts`)
- 恢复 Session 时同时保存 UI-safe messages 与原始 model context，后续只把恢复边界后的新 UI 消息追加到原始上下文 (`packages/cli/src/store/slices/sessionSlice.ts`, `packages/cli/src/ui/utils/sessionContext.ts`)
- 用户输入在 slash command、Hook 改写和 Agent 输入之间保留不同表示：Hook 可改写模型输入，但不能回写已经提交的用户可见消息 (`packages/cli/src/ui/hooks/useCommandHandler.ts`)
- 工具进度、Provider 恢复和 MCP 事件通过统一 LoopEvent handler 投影；新增事件必须保持 exhaustive switch，并避免把结构化输出保留工具渲染成普通工具卡 (`packages/cli/src/ui/utils/loopEventHandler.ts`)
- 主题、权限、Session 选择等弹窗共享焦点状态，主输入仅在没有阻塞弹窗时接管键盘；组件自己的 `useInput` 必须检查对应 FocusId (`packages/cli/src/ui/components/BladeInterface.tsx`, `packages/cli/src/ui/components/ConfirmationPrompt.tsx`)

## Dependencies
- TUI 使用 React、Ink、Yoga 与 Zustand；`ink-text-input` 只处理编辑行为，终端 framing 和批量输入由项目自己的 parser 补齐 (`packages/cli/src/ui/components/CustomTextInput.tsx`, `packages/cli/src/ui/input/terminalInput.ts`)

