DSH / Atlas
2026-07-18implementedtestingarchived 2026-08-04

Snapshot semantic terminal state for the TUI

TUI 语义终端状态快照

The TUI is a stateful renderer. Its user-visible result depends on ANSI parsing, differential frames, wrapping, scrollback, viewport position, terminal width, focus, cursor state, and each tool's presentation intent. Unit tests that collect `Terminal.write()` fragments can prove event handling, but they cannot prove the final screen a terminal displays. The same screen may also be emitted through different write frag

English

Problem

The TUI is a stateful renderer. Its user-visible result depends on ANSI parsing, differential frames, wrapping, scrollback, viewport position, terminal width, focus, cursor state, and each tool's presentation intent. Unit tests that collect Terminal.write() fragments can prove event handling, but they cannot prove the final screen a terminal displays. The same screen may also be emitted through different write fragments, so pinning those fragments creates false regressions.

Component-line snapshots stop before ANSI reaches a terminal and miss cursor movement, clearing, styling, overlay composition, and reflow. Raster screenshots include font and platform rendering noise that is unrelated to the TUI contract. A completed flow built by directly appending plausible session events has another blind spot: it proves the renderer accepts those shapes, not that the production agent loop and tool implementations produce them.

The reusable TUI therefore needs a deterministic, reviewable representation of terminal state. A product deployment that ships it additionally needs recorded model journeys through the assembled stack and a smaller test at the real process and PTY boundary.

Decision

Reusable TUI coverage has two complementary package layers:

  1. packages/ui/tui/tests/tui.spec.ts tests event mapping, input routing, disposal, and error behavior directly.
  2. packages/ui/tui/tests/tui.snapshot.ts mounts the production TUI against a headless terminal emulator for transient states that a completed session log cannot retain: in-flight streaming, pending tool calls, overlays, expansion, compaction reflow, errors, and shutdown.

The explicit-config entrypoint decision removed the product TUI composition, recorded application journeys, and PTY suite. A deployment shipping a terminal front door owns those assembled-application layers; package tests do not claim that product coverage.

Removed application replay

The deleted application suite gave each scenario session.jsonl, optional child logs session.<n>.jsonl, and terminal.expected.txt. The primary log supplied user-authored user/message prompts and the recorded assistant/chunk sequence. dsh-llm-replay derived one model-call script per session and was the only mocked boundary; the agent loop, tools, workers, presenters, and TUI were production implementations.

That suite rejected a journey when its tool-call sequence differed, an expected event count was missing, a tool result was an error, a turn ended in error, a workflow lifecycle was incomplete, or the live child-session count differed from the fixture set. These checks remain the acceptance pattern for any future terminal deployment; they are no longer shipped fixtures.

The removed recording workflow used DSH_SNAPSHOT=record for model journeys and DSH_SNAPSHOT=refresh for derived terminal output. Removing the product entrypoint also removed those modes from the repository snapshot lane; reusable TUI snapshots are authored directly from package scenarios.

Semantic terminal projection

The package-local HeadlessTerminal implements the same pi-tui Terminal interface as the process terminal and feeds every ANSI write into the pinned @xterm/headless parser. Snapshot code waits for synchronized frames to quiesce before reading state. The streaming checkpoint freezes the loader interval while allowing real wall-clock delay across one animation tick, so it pins semantic status rather than whichever spinner glyph the scheduler happened to render.

Each expected output projects dimensions, active-buffer and viewport coordinates, lifecycle and cursor state, rows, wrap markers, and non-default style ranges into text. Scroll-heavy cards capture the used buffer; overlays capture the visible viewport. Text and style remain separate so a reviewer can distinguish content changes from presentation changes without decoding ANSI bytes.

Every checkpoint enforces theme independence across the complete terminal state: no RGB colors, no palette entries beyond ANSI 0–15, and no explicit background colors. Reverse video remains valid for selection because it uses terminal defaults. Both suites own closed inventories that reject missing scenarios, missing checkpoints, and orphaned expected output files.

Required scenario matrix

LayerScenarioContract pinned
Transient stateStreaming and pending advanced callsIn-flight reasoning/text plus pending Code Mode, workflow, and Cordis cards that disappear from completed logs
Transient stateCards, interaction, layout, failure, and shutdownCollapsed/expanded card families, question validation, compaction replacement, resize reflow, help/errors, cursor restoration, and terminal stop

Alternatives considered

  • Snapshot raw terminal writes — rejected because differential rendering may change write boundaries without changing the screen, while cursor and clear sequences are unreadable in review.
  • Snapshot component render lines before terminal output — rejected because it does not test ANSI parsing, cursor movement, overlays, viewport behavior, or independent components in one frame.
  • Build every completed flow by appending session events — rejected because a hand-authored event sequence can drift from the agent loop, tool execution, child-session binding, or worker behavior while its presentation test stays green. Direct event construction remains limited to transient renderer states.
  • Reuse ACP stdout expected outputs as the TUI oracle — rejected because a recorded model journey is transport-neutral but its presentation is not. A terminal deployment owns its expected output while it may reuse the same JSONL replay vocabulary.
  • Commit raster screenshots — rejected because fonts, glyph metrics, antialiasing, and host terminal themes make them platform-sensitive and make semantic style changes difficult to review.
  • Use only PTY end-to-end tests — rejected because raw PTY output is a stream of historical drawing operations, not queryable final state. PTY tests retain the real Loader/input/teardown boundary, while the emulator owns broad state coverage.

Consequences

  • Package snapshots fail when TUI event mapping or presentation breaks; they do not substitute for an assembled application's tool-path transcript.
  • TUI visual regressions produce readable cell-and-style diffs, while JSONL fixtures retain the exact model chunks that made the production path execute.
  • The emulator uses xterm's proposed buffer API. An xterm upgrade requires rerunning and reviewing the semantic projection; terminal-specific behavior still needs a PTY smoke owned by the deployment that ships it.
  • Expected outputs deliberately encode wrapping and viewport behavior at fixed sizes. Intentional layout changes update and review the package semantic snapshots.

中文

问题

TUI 是有状态的渲染器。用户最终看到的结果取决于 ANSI 解析、差分帧、换行、回滚缓冲、视口位置、终端宽度、焦点、光标状态,以及各工具的呈现意图。收集 Terminal.write() 片段的单元测试可以验证事件处理,却无法验证终端最终显示的画面。同一画面也可能由不同的写入片段产生,因此固定这些片段会制造误报。

组件行快照止于 ANSI 进入终端之前,无法覆盖光标移动、清屏、样式、浮层组合和重排。栅格截图会带入与 TUI 契约无关的字体和平台渲染噪声。直接追加看似合理的会话事件来构造完整流程还存在另一处盲区:这种测试只能证明渲染器接受这些数据形态,无法证明生产环境的 agent loop(智能体循环)和工具实现会生成这些事件。

因此,可复用 TUI 需要确定、便于评审的终端状态表示。交付它的产品部署还需要通过组装后的技术栈运行已录制模型流程,并保留一项范围更小、覆盖真实进程与 PTY 边界的测试。

决策

可复用 TUI 的覆盖分为两个互补的包级层次:

  1. packages/ui/tui/tests/tui.spec.ts 直接测试事件映射、输入路由、资源释放和错误行为。
  2. packages/ui/tui/tests/tui.snapshot.ts 将生产 TUI 挂载到无界面终端模拟器,覆盖完整会话日志无法保留的瞬态:进行中的流式输出、待完成工具调用、浮层、展开状态、压缩重排、错误和关闭过程。

显式配置入口决策移除了产品 TUI 组合、已录制应用流程和 PTY 测试套件。交付终端入口的部署负责这些组装应用层;包测试不声称提供产品覆盖。

已移除的应用回放

已删除的应用测试套件为每个场景提供 session.jsonl、可选的子会话日志 session.<n>.jsonl,以及 terminal.expected.txt。主日志提供用户来源的 user/message 提示词和已录制的 assistant/chunk 序列。dsh-llm-replay 为每个会话派生一份模型调用脚本,并且是测试中唯一的 mock 边界;agent loop、工具、worker、呈现器和 TUI 都使用生产实现。

如果工具调用顺序不符、预期事件数量不足、工具结果报错、轮次以错误结束、工作流生命周期不完整,或者实时子会话数量与 fixture(测试前置数据)集合不一致,该测试套件都会拒绝流程。这些检查仍是未来任何终端部署的验收模式;它们已不再作为 fixture 交付。

已移除的录制工作流使用 DSH_SNAPSHOT=record 录制模型流程,使用 DSH_SNAPSHOT=refresh 更新派生的终端输出。移除产品入口时也从仓库快照通道中移除了这些模式;可复用 TUI 快照直接由包级场景编写。

语义终端投影

包内的 HeadlessTerminal 实现与进程终端相同的 pi-tui Terminal 接口,并把每次 ANSI 写入交给固定版本的 @xterm/headless 解析器。读取状态前,快照代码会等待同步帧稳定。流式输出检查点会冻结 loader 的 interval,同时保留跨过一次动画 tick 的真实墙钟等待,从而固定语义状态,而非调度器碰巧渲染出的某个加载动画字形。

每份预期输出把终端尺寸、活动缓冲区和视口坐标、生命周期与光标状态、各行、换行标记以及非默认样式区间投影为文本。滚动内容较多的卡片捕获已使用缓冲区;浮层捕获可见视口。文本和样式相互分离,评审人无需解码 ANSI 字节即可区分内容变化与呈现变化。

每个检查点还会对完整终端状态强制执行主题无关性:禁止 RGB 颜色、禁止 ANSI 0–15 以外的调色板项,也禁止显式背景色。选择行使用终端默认色进行反显,因此仍然有效。两套测试都拥有封闭清单,会拒绝缺失的场景、缺失的检查点和遗留预期输出文件。

必需场景矩阵

层次场景固定的契约
瞬态流式输出与待完成高级调用进行中的推理和文本,以及完整日志中不会保留的待完成 Code Mode、工作流和 Cordis 卡片
瞬态卡片、交互、布局、失败和关闭折叠与展开的卡片族、问题校验、压缩替换、尺寸重排、帮助与错误、光标恢复和终端停止

曾考虑的替代方案

  • 快照原始终端写入:不予采纳,因为差分渲染可能在画面不变时改变写入边界,而且光标与清屏序列难以评审。
  • 快照进入终端输出之前的组件渲染行:不予采纳,因为它无法测试 ANSI 解析、光标移动、浮层、视口行为,也无法测试独立组件在同一帧中的相互作用。
  • 通过追加会话事件构造所有完整流程:不予采纳,因为人工编写的事件序列可能与 agent loop、工具执行、子会话绑定或 worker 行为发生偏差,但呈现测试仍然保持绿色。直接构造事件只用于渲染器瞬态。
  • 复用 ACP stdout 预期输出作为 TUI 判定依据:不予采纳,因为已录制模型流程与传输方式无关,其呈现方式却并非如此。终端部署拥有自己的预期输出,同时可以复用同一套 JSONL 回放词汇。
  • 提交栅格截图:不予采纳,因为字体、字形度量、抗锯齿和宿主终端主题会使结果依赖平台,也会增加语义样式变更的评审难度。
  • 只使用 PTY 端到端测试:不予采纳,因为原始 PTY 输出是一系列历史绘制操作,而不是可查询的最终状态。PTY 测试保留真实 Loader、输入与清理边界,模拟器负责广泛的状态覆盖。

后果

  • 当 TUI 事件映射或呈现损坏时,包快照会失败;它们不能代替组装应用的工具路径 transcript。
  • TUI 视觉回归会产生便于阅读的单元格和样式 diff,而 JSONL fixture 会保留触发生产路径的确切模型分片。
  • 模拟器使用 xterm 的拟议缓冲区 API。升级 xterm 时必须重新运行并评审语义投影;终端特有行为仍需由交付该终端的部署所拥有的 PTY 冒烟测试覆盖。
  • 预期输出有意固定指定尺寸下的换行与视口行为。预期布局变更会更新并评审包级语义快照。