Files
deepseek-harness/.agents/notes/implemented/testing/2026-07-18-tui-terminal-state-snapshots.md
T
Turtle f290a8b851 refactor(cli)!: one shared base config with per-surface overlays
`dsh` shipped two config trees that were 43 rows the same: apps/cli/cordis.yml
composed web as 74 flat rows, while the TUI booted examples/tui-agent/cordis.yml
whose single `@deepseek-ai/dsh-tui-demo` row mounted twelve plugins behind a
twenty-key pass-through Config. Neither file was what its location claimed —
apps/cli hardcoded the "example" as the product default and the "demo" bundle
was the application — and every capability change had to be made twice.

- apps/cli/base.cordis.yml holds the 43 shared rows; tui.cordis.yml and
  web.cordis.yml are patch lists stating only what differs per surface
- overlays apply as SIBLING patch lists at one include level, because include
  patches never cross an include boundary. Precedence: base < surface <
  (--config | personal ~/.dsh/config.yaml) < launcher flag/profile patches
- `--config` now applies an overlay INSTEAD OF the personal one, so a demo or
  test tree never inherits the user's route; new `--config-replace` boots a file
  as the entire tree (the old `--config` behaviour). Both survive /resume
- vendor/include: index each `insert`ed row as it is added so a later patch can
  configure or disable it. Upstream built the id index once before the patch
  loop, leaving every surface-only row — the whole TUI front door — silently
  unpatchable from user config. Logged as local modification 8
- session identity moves to dsh-agent-loop's CONFIGURED_AGENT_IDENTITIES_KEY;
  dsh-tui's MAIN_SESSION_ID_KEY is deleted (only the bundle read it)
- delete examples/tui-agent, examples/cordis-agent, packages/examples/tui-demo;
  TUI tests → apps/cli/tests, cordis e2e → packages/cordis/tool-cordis/tests,
  examples/code-mode survives as an overlay leaf
- `dsh web` gains --config, threaded into AppCLIEntry as an extra overlay

Three latent defects surfaced and are fixed here: the TUI captured the optional
sessionQuery service once at construction and could permanently disable /resume
when it won the mount race; the session-store root silently reverted to a
project-local ./.sessions; --config-replace was dropped by the resume handoff.

Verified by booting each tree through the real Loader (TUI 55 entries, web 75,
zero unsettled) rather than reading YAML. All eight terminal snapshots replay
byte-identically; 14/14 PTY smoke, 112/112 snapshots, 25/25 doc-sync, hygiene
and lint clean.
2026-07-29 21:15:42 +08:00

8.7 KiB
Raw Blame History

Agent Note: Snapshot semantic terminal state for the TUI

Status: implemented

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 TUI therefore needs a deterministic, reviewable representation of terminal state, recorded model journeys that execute the real downstream stack, and a smaller test at the real process and PTY boundary.

Decision

TUI coverage has four complementary 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.
  3. examples/tui-agent/tests/tui.snapshot.ts replays committed JSONL session logs through the production agent loop and real tools, then compares the resulting semantic terminal state.
  4. examples/tui-agent/tests/tui-keyless-smoke.e2e.ts boots the real Loader composition in a PTY, drives a scripted conversation through streaming and ask_user_question, and verifies startup, input, exit, failure reporting, and terminal restoration.

The runnable TUI is the shipped apps/cli composition: the shared base.cordis.yml plus the tui.cordis.yml overlay, which owns the interactive coding backends, tools, and front door. TUI snapshots and PTY tests live in apps/cli/tests/. The redundant-agent removal owns this consolidation.

Recorded-session replay

Each example-level scenario directory owns session.jsonl, optional child logs session.<n>.jsonl, and terminal.expected.txt. The primary log supplies user-authored user/message prompts and the recorded assistant/chunk sequence. dsh-llm-replay derives one model-call script per session, binds child logs to fresh child sessions, and is the only mocked boundary. The agent loop, bash and filesystem implementations, Code Mode worker, subagent provider, workflow worker, Cordis tools, presenters, and TUI are production implementations.

The suite rejects a journey when its tool-call sequence differs, an expected event count is missing, a tool result is an error, a turn ends in error, a workflow lifecycle is incomplete, or the live child-session count differs from the fixture set. These assertions prevent an attractive terminal expected output from hiding a failed or bypassed production path.

The live-model fixtures use DSH_SNAPSHOT=record; record mode rewrites their primary and child JSONL logs and terminal expected outputs. The deterministic Cordis toolchain keeps an authored complete JSONL script because reliably coercing a live model through five exact tool boundaries and two children is not a stable recording contract. DSH_SNAPSHOT=refresh replays every committed script keylessly and rewrites only derived terminal expected outputs. Plain replay compares without writing, and unknown mode values fail loud.

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

Layer Scenario Contract pinned
Recorded journey Multi-turn conversation Recorded reasoning/text chunks, two input turns, retained history, token totals, and idle editor state
Recorded journey Todo plan Real todo_write execution, result card, and persistent plan rendering
Recorded journey Bash terminal card Real local executor output, description, exit status, and completed terminal card
Recorded journey Parallel filesystem reads Two calls from one assistant message, real file contents, ordering, and separate completed cards
Recorded journey Code Mode Real run_code worker execution, two tool/code-dispatch events, captured program output, and completed card
Recorded journey Dynamic workflow Real workflow worker, phase lifecycle, replayed child session, structured return value, and completed card
Recorded journey Cordis dynamic toolchain Real mount, Code Mode inspect, direct subagent, workflow child, unmount, and all production presenters
Transient state Streaming and pending advanced calls In-flight reasoning/text plus pending Code Mode, workflow, and Cordis cards that disappear from completed logs
Transient state Cards, interaction, layout, failure, and shutdown Collapsed/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. TUI scenarios own terminal expected outputs while using 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

  • Completed advanced snapshots now fail when the real Code Mode, workflow, subagent, filesystem, bash, or Cordis path breaks, rather than accepting a fabricated result event.
  • 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 the PTY smoke.
  • Expected outputs deliberately encode wrapping and viewport behavior at fixed sizes. Intentional layout changes use keyless refresh, while model-journey changes use record mode and review both JSONL and terminal diffs.