test(tui): snapshot semantic terminal state

This commit is contained in:
Tianyi Cui
2026-07-18 22:31:04 +08:00
parent 864b7bfd65
commit 8e366c3071
34 changed files with 2437 additions and 107 deletions
+1
View File
@@ -209,6 +209,7 @@ Generated by `pnpm run gen-rfc-index` from the RFC tree — never edit by hand;
| [Single-source the acp-agent replay config](implemented/testing/2026-07-04-single-source-acp-replay-config.md) | 2026-07-04 |
| [Pin request-header content in one snapshot scenario](implemented/testing/2026-07-06-pin-request-header-content-in-one-scenario.md) | 2026-07-06 |
| [Extract the ACP snapshot suite into a support package](implemented/testing/2026-07-08-shared-acp-snapshot-package.md) | 2026-07-08 |
| [Snapshot semantic terminal state for the TUI](implemented/testing/2026-07-18-tui-terminal-state-snapshots.md) | 2026-07-18 |
## Rejected
@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
2026-07-18-tui-terminal-state-snapshots.md: 225efe8de95973ecae3f9bf73308e7ae879dddbb
2026-07-18-tui-terminal-state-snapshots.zh.md: b7399a31b0abf41905f495a1ea3b4ecd0c858526
@@ -0,0 +1,57 @@
# RFC: Snapshot semantic terminal state for the TUI
Status: implemented
English | [中文](2026-07-18-tui-terminal-state-snapshots.zh.md)
## 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. The TUI therefore needs a deterministic, reviewable representation of terminal state plus a smaller test at the real process and PTY boundary.
## Decision
TUI coverage has three complementary layers:
1. `tui.spec.ts` tests event mapping, input routing, disposal, and error behavior directly.
2. `tui.snapshot.ts` mounts the production TUI against a headless terminal emulator and compares semantic terminal-state goldens.
3. `tui-keyless-smoke.e2e.ts` boots the real Loader composition in a PTY, drives a complete scripted conversation through streaming and `ask_user_question`, exits through `/exit`, and verifies terminal teardown. The production coding-agent configuration also retains its banner/exit and startup-failure PTY cases.
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. A snapshot waits for pi-tui's synchronized-output end marker before reading state. This makes a checkpoint represent a completed frame rather than a timer-dependent write prefix.
Each golden projects terminal state into text: dimensions, active-buffer and viewport coordinates, lifecycle and cursor state, rows, wrap markers, and non-default style ranges. 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 also enforces theme independence across the complete terminal state: no RGB colors, no palette entries beyond ANSI 015, and no explicit background colors. Reverse video remains valid for selection because it uses terminal defaults. The suite owns a closed checkpoint list: its type rejects undeclared names, and its inventory checks reject missing checkpoints and orphaned `.golden.txt` files.
### Required scenario matrix
| Area | Representative checkpoints | Contract pinned |
|---|---|---|
| Conversation | replay, streaming, completion | Resumed Markdown and reasoning, live deltas, plans, token usage, and max-token completion |
| Code Mode | pending and completed `run_code` | The production Code Mode registry and presenter, source program, captured logs, and result |
| Dynamic workflows | pending and completed `workflow` | The production workflow presenter, metadata, phases, parallel agents, script, and structured result |
| Cordis tools | pending and completed inspect/mount/unmount | The production `cordis_inspect`, `cordis_mount`, and `cordis_unmount` presenters and lifecycle results |
| Advanced tool cards | collapsed and expanded | Terminal, diff, generic, subagent, background-task, and skill card shapes plus output truncation |
| Interaction | question and validation | Constrained multi-select overlay composition, focus, scrolling, selection, and validation errors |
| Surface and layout | before compaction, narrow replacement, wide replacement | Surface replacement removes retired content; resize reflows the surviving surface without resurrection |
| Failure and shutdown | errors/help and disposed terminal | Help and unknown commands, live/turn error de-duplication, interruption, cursor restoration, and terminal stop |
The explicitly model-facing advanced cases use the real `ToolRegistry` configuration and the production Code Mode, workflow, and Cordis tool presenters. Synthetic presenter fixtures are limited to the generic card-shape matrix, where the TUI's input contract is the presenter view itself. Session events remain the driver so replay, streaming, result arrival, surface replacement, and lifecycle ordering exercise the same projection path as production.
The TUI suite is included by `vitest.snapshot.config.ts`, so `pnpm run test:snapshot` compares it keylessly. `pnpm run test:snapshot:refresh` rewrites its derived terminal goldens without contacting a model; `test:snapshot:record` remains meaningful for suites whose transcript source requires recording. Both refresh paths still compare the resulting files in the same run.
## 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 the interaction between independent components in one frame.
- **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.
- **Copy pi-tui's unpublished virtual-terminal test helper** — rejected because the installed package does not export that helper. A small adapter around the public `@xterm/headless` API keeps the dependency explicit and the projection owned by this package.
## Consequences
- TUI visual regressions produce readable cell-and-style diffs, and the required matrix makes advanced features first-class rather than incidental coverage.
- The test dependency is pinned to the xterm version used by pi-tui. The adapter uses xterm's proposed buffer API, so an xterm upgrade requires rerunning and reviewing the semantic projection.
- The emulator models ANSI terminal state but cannot prove behavior unique to every terminal implementation. The real PTY conversation covers process selection, keyboard input, user interaction, and teardown without duplicating the full matrix.
- Goldens deliberately encode wrapping and viewport behavior at fixed sizes. Intentional layout changes update them through the keyless refresh command and receive ordinary snapshot review.
@@ -0,0 +1,57 @@
# RFC: TUI 语义终端状态快照
Status: implemented
[English](2026-07-18-tui-terminal-state-snapshots.md) | 中文
## 问题
TUI 是有状态的渲染器。用户最终看到的结果取决于 ANSI 解析、差分帧、换行、回滚缓冲、视口位置、终端宽度、焦点、光标状态,以及各工具的呈现意图。收集 `Terminal.write()` 片段的单元测试可以验证事件处理,却无法验证终端最终显示的画面。同一画面也可能由不同的写入片段产生,因此固定这些片段会制造误报。
组件行快照止于 ANSI 进入终端之前,无法覆盖光标移动、清屏、样式、浮层组合和重排。栅格截图会带入与 TUI 契约无关的字体和平台渲染噪声。因此,TUI 既需要一种确定、便于评审的终端状态表示,也需要一项范围更小、覆盖真实进程与 PTY 边界的测试。
## 决策
TUI 覆盖分为三个互补层次:
1. `tui.spec.ts` 直接测试事件映射、输入路由、资源释放和错误行为。
2. `tui.snapshot.ts` 将生产 TUI 挂载到无界面终端模拟器,并比较语义终端状态金标。
3. `tui-keyless-smoke.e2e.ts` 在 PTY 中启动真实 Loader 组合,驱动一段完整的脚本化会话,使其依次经过流式输出和 `ask_user_question`,再通过 `/exit` 退出并验证终端清理。生产 coding-agent 配置还保留欢迎信息与退出,以及启动失败两类 PTY 场景。
包内的 `HeadlessTerminal` 实现与进程终端相同的 pi-tui `Terminal` 接口,并把每次 ANSI 写入交给固定版本的 `@xterm/headless` 解析器。快照会等待 pi-tui 的同步输出结束标记,再读取状态。因此,每个检查点表示已经完成的帧,而不是依赖计时的写入前缀。
每份金标把终端状态投影为文本:尺寸、活动缓冲区和视口坐标、生命周期与光标状态、各行、换行标记,以及非默认样式区间。滚动内容较多的卡片捕获已使用缓冲区;浮层捕获可见视口。文本和样式相互分离,评审人无需解码 ANSI 字节即可区分内容变化与呈现变化。
每个检查点还会对完整终端状态强制执行主题无关性:禁止 RGB 颜色、禁止 ANSI 0–15 以外的调色板项,也禁止显式背景色。选择行使用终端默认色进行反显,因此仍然有效。测试套件拥有封闭的检查点清单:类型会拒绝未声明的名称,清单检查会拒绝缺失的检查点和遗留的 `.golden.txt` 文件。
### 必需场景矩阵
| 范围 | 代表性检查点 | 固定的契约 |
|---|---|---|
| 会话 | 回放、流式输出、完成 | 恢复后的 Markdown 与推理、实时增量、计划、token 用量,以及达到 token 上限时的完成状态 |
| Code Mode | `run_code` 待完成与已完成 | 生产 Code Mode 注册表与呈现器、源程序、捕获日志和结果 |
| 动态工作流 | `workflow` 待完成与已完成 | 生产工作流呈现器、元数据、阶段、并行 agent、脚本和结构化结果 |
| Cordis 工具 | inspect/mount/unmount 待完成与已完成 | 生产 `cordis_inspect``cordis_mount``cordis_unmount` 呈现器及其生命周期结果 |
| 高级工具卡片 | 折叠与展开 | 终端、diff、通用、subagent、后台任务和 skill 卡片形态,以及输出截断 |
| 交互 | 问题与校验 | 受限多选浮层的组合、焦点、滚动、选择和校验错误 |
| 表层与布局 | 压缩前、窄幅替换、宽幅替换 | 表层替换会移除退役内容;调整尺寸只会重排保留的表层,不会让旧内容重新出现 |
| 失败与关闭 | 错误与帮助、终端已释放 | 帮助与未知命令、实时错误和轮次错误去重、中断、光标恢复及终端停止 |
面向模型的高级场景明确使用真实 `ToolRegistry` 配置,以及生产 Code Mode、工作流和 Cordis 工具呈现器。只有通用卡片形态矩阵使用合成呈现器 fixture;在这里,呈现器视图本身就是 TUI 的输入契约。测试仍由会话事件驱动,因此回放、流式输出、结果到达、表层替换和生命周期顺序都会经过与生产环境相同的投影路径。
`vitest.snapshot.config.ts` 会包含 TUI 测试套件,因此 `pnpm run test:snapshot` 可以无密钥比较快照。`pnpm run test:snapshot:refresh` 会重写从终端状态派生的金标,而不会联系模型;对于 transcript(文本记录)来源需要录制的测试套件,`test:snapshot:record` 仍有其原有含义。两条刷新路径都会在同一次运行中继续比较生成后的文件。
## 曾考虑的替代方案
- **快照原始终端写入**:不予采纳,因为差分渲染可能在画面不变时改变写入边界,而且光标与清屏序列难以评审。
- **快照进入终端输出之前的组件渲染行**:不予采纳,因为它无法测试 ANSI 解析、光标移动、浮层、视口行为,也无法测试独立组件在同一帧中的相互作用。
- **提交栅格截图**:不予采纳,因为字体、字形度量、抗锯齿和宿主终端主题会使结果依赖平台,也会增加语义样式变更的评审难度。
- **只使用 PTY 端到端测试**:不予采纳,因为原始 PTY 输出是一系列历史绘制操作,而不是可查询的最终状态。PTY 测试保留真实 Loader、输入与清理边界,模拟器负责广泛的状态覆盖。
- **复制 pi-tui 未发布的虚拟终端测试 helper**:不予采纳,因为已安装的包并未导出该 helper。围绕公开 `@xterm/headless` API 编写小型适配器,可以显式声明依赖,并让本包拥有状态投影。
## 后果
- TUI 视觉回归会产生便于阅读的单元格和样式 diff;必需场景矩阵也让高级功能成为一等测试对象,而不是偶然覆盖。
- 测试依赖固定到 pi-tui 使用的 xterm 版本。适配器使用 xterm 的拟议缓冲区 API,因此升级 xterm 时必须重新运行并评审语义投影。
- 模拟器可以建模 ANSI 终端状态,但无法证明每种终端实现独有的行为。真实 PTY 会话覆盖进程选择、键盘输入、用户交互和清理,无需复制完整矩阵。
- 金标有意固定指定尺寸下的换行与视口行为。布局的预期变更通过无密钥刷新命令更新,并接受常规快照评审。
+2 -2
View File
@@ -7,7 +7,7 @@ How this repo tests, tier by tier, and the rules that keep a green suite meaning
- **Unit** (`pnpm run test`): vitest over `packages|examples/*/tests/**/*.spec.ts`, colocated with what they test. Every registry gets an HMR-safety test (dispose the contributing fiber, assert cleanup). Prefer edge cases, error paths, event ordering, concurrency races, and permanent contract regressions (see `packages/core/agent-loop/tests/contract-regressions.spec.ts`).
- **Coverage gate** (`pnpm run test:coverage`): the gating run, per-file 100% on `packages/*/*/src`. An uncovered line is often dead code the gate is correctly flagging for deletion, not a missing test to bolt on. Line coverage is necessary, never sufficient — it proves lines ran, not that the feature works as shipped.
- **Real-API e2e** (`pnpm run test:e2e`): with-key tests against live provider APIs — the DeepSeek model plus provider-specific smokes that gate on their own keys (`EXA_API_KEY`, `PERPLEXITY_API_KEY`, …); each suite self-skips without its key so keyless CI stays green ([real-API e2e RFC](rfc/implemented/testing/2026-06-19-real-api-e2e-ci.md)).
- **Snapshot** (`pnpm run test:snapshot`): boots the real example subprocess, replays a recorded session keyless, diffs normalized stdout + the re-persisted log against committed goldens ([snapshot RFC](rfc/implemented/testing/2026-06-19-acp-snapshot-tests.md)). Use `pnpm run test:snapshot:record` when the model transcript should change; use `pnpm run test:snapshot:refresh` when the committed transcript is still the right mock LLM input and replay goldens need keyless rewrite. Review the golden diff. System-prompt/tool-schema content is pinned by ONE scenario (`text-turn`) and tokenized in every other fixture, so a prompt or schema edit churns one committed line ([pinned-header RFC](rfc/implemented/testing/2026-07-06-pin-request-header-content-in-one-scenario.md)).
- **Snapshot** (`pnpm run test:snapshot`): transport-specific keyless goldens cover external presentation. ACP suites boot the real example subprocess, replay a recorded session, and diff normalized stdout plus the re-persisted log ([ACP snapshot RFC](rfc/implemented/testing/2026-06-19-acp-snapshot-tests.md)); the TUI suite parses real ANSI output into semantic terminal-state goldens and retains a real PTY conversation at the process boundary ([TUI snapshot RFC](rfc/implemented/testing/2026-07-18-tui-terminal-state-snapshots.md)). Use `pnpm run test:snapshot:record` when a model transcript must change and `pnpm run test:snapshot:refresh` when the committed transcript or scripted events remain correct; review every golden diff. System-prompt/tool-schema content is pinned by ONE ACP scenario (`text-turn`) and tokenized in every other fixture, so a prompt or schema edit churns one committed line ([pinned-header RFC](rfc/implemented/testing/2026-07-06-pin-request-header-content-in-one-scenario.md)).
## The with-key policy: inference is cheap here
@@ -35,4 +35,4 @@ An e2e assertion re-runs the command or re-reads the file externally; a keyword
## When a snapshot test is required
Any change affecting the editor-facing transcript or end-to-end agent UX — the ACP bridge, the loop's observable output, tool presentation — adds or updates a scenario in the owning example's snapshot suite (`examples/<name>/tests/snapshots/`, a scenario table over the [`dsh-acp-snapshot`](../packages/support/acp-snapshot/README.md) suite factory; `examples/acp-agent` is the primary suite), or states in the PR why none applies. New capability seams, lifecycle shapes, or transcript surfaces name their coverage at every tier at plan time and verify the harness can express it — a harness gap is scheduled work, not a mid-build surprise.
Any change affecting an editor-facing transcript or end-to-end agent UX adds or updates a scenario in the owning snapshot suite, or states in the PR why none applies. ACP surfaces use `examples/<name>/tests/snapshots/`, a scenario table over the [`dsh-acp-snapshot`](../packages/support/acp-snapshot/README.md) suite factory (`examples/acp-agent` is primary); interactive-terminal presentation uses the semantic TUI matrix, with a PTY case when input, Loader selection, or terminal teardown changes. New capability seams, lifecycle shapes, or transcript surfaces name their coverage at every tier at plan time and verify the harness can express it — a harness gap is scheduled work, not a mid-build surprise.
@@ -0,0 +1,60 @@
import type { Context } from 'cordis'
import type { GenerateOptions, StreamChunk } from '@deepseek-ai/dsh-llm'
import { CallId, LlmAdapter } from '@deepseek-ai/dsh-llm'
const INITIAL_TEXT = 'I need one decision before I continue.'
const FINAL_TEXT = 'Decision received. Scripted TUI run complete.'
function textChunks(text: string): StreamChunk[] {
return [
{ type: 'block-start', index: 0, blockType: 'text' },
...Array.from(text, (char): StreamChunk => ({ type: 'text-delta', index: 0, text: char })),
{ type: 'block-end', index: 0, block: { type: 'text', text } },
{ type: 'usage', usage: { inputTokens: 20, outputTokens: text.length } },
{ type: 'finish', reason: { kind: 'stop' } },
]
}
/** Keyless two-step adapter for the real-PTY TUI conversation test. */
class ScriptedTuiAdapter extends LlmAdapter {
async * stream(options: GenerateOptions): AsyncIterable<StreamChunk> {
const hasToolResult = options.messages.at(-1)?.content.some(block => block.type === 'tool-result') ?? false
if (hasToolResult) {
for (const chunk of textChunks(FINAL_TEXT)) yield chunk
return
}
const args = JSON.stringify({
questions: [{
id: 'mode',
header: 'Execution mode',
question: 'How should the scripted run proceed?',
options: [
{ label: 'Safe', description: 'Use the guarded path.' },
{ label: 'Fast', description: 'Use the shorter path.' },
],
}],
})
const callId = CallId('call-ask-mode')
yield { type: 'block-start', index: 0, blockType: 'text' }
for (const char of INITIAL_TEXT) yield { type: 'text-delta', index: 0, text: char }
yield { type: 'block-end', index: 0, block: { type: 'text', text: INITIAL_TEXT } }
yield { type: 'block-start', index: 1, blockType: 'tool-call' }
yield { type: 'tool-call-delta', index: 1, id: callId, name: 'ask_user_question', argumentsDelta: args }
yield {
type: 'block-end',
index: 1,
block: { type: 'tool-call', id: callId, name: 'ask_user_question', arguments: args },
}
yield { type: 'usage', usage: { inputTokens: 20, outputTokens: 10 } }
yield { type: 'finish', reason: { kind: 'tool-calls' } }
}
}
export const name = 'tui-scripted-llm'
export const inject = ['llm']
/** Register the network-free adapter used by the PTY fixture. */
export function apply(ctx: Context): void {
ctx.llm.registerAdapter(['tui-scripted'], new ScriptedTuiAdapter())
}
@@ -0,0 +1,27 @@
# Real Loader composition for the keyless conversational PTY test. The app
# bundle supplies the production agent/TUI/user-question stack; only the model
# is scripted so the terminal interaction is deterministic and network-free.
- id: scripted-llm
name: './tui-scripted-llm.ts'
- id: bash
name: '@deepseek-ai/dsh-bash-local'
- id: fs-local
name: '@deepseek-ai/dsh-fs-local'
config:
cwd: !!js process.cwd()
- id: stdio-agent
name: '@deepseek-ai/dsh-stdio-demo'
config:
provider: tui-scripted
model: tui-scripted-model
persistenceRoot: './.sessions'
workspaceContext:
maxBytes: 65536
welcome: 'scripted TUI ready.'
ui:
mode: tui
tui:
showReasoning: true
@@ -4,31 +4,32 @@ import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { describe, expect, it } from 'vitest'
import { LOADER_SMOKE_TEST_TIMEOUT_MS } from '@deepseek-ai/dsh-loader-smoke'
import { LOADER_SMOKE_TEST_TIMEOUT_MS, resolveExampleLaunch } from '@deepseek-ai/dsh-loader-smoke'
const binScript = fileURLToPath(new URL('../../../packages/examples/stdio-demo/src/bin.ts', import.meta.url))
const configPath = fileURLToPath(new URL('../cordis.yml', import.meta.url))
const scriptedConfigPath = fileURLToPath(new URL('./fixtures/tui-scripted.cordis.yml', import.meta.url))
const tsconfigPath = fileURLToPath(new URL('../../../tsconfig.json', import.meta.url))
const tsxLoader = fileURLToPath(import.meta.resolve('tsx'))
const PTY_DRIVER = String.raw`
import errno, os, pty, select, signal, sys, time
node, tsx_loader, bin_script, config_path, tsconfig_path, cwd, resume_session_id = sys.argv[1:]
import errno, json, os, pty, select, signal, sys, time
node, launch_args_json, launch_env_json, cwd, resume_session_id, scenario = sys.argv[1:]
env = os.environ.copy()
env.update(json.loads(launch_env_json))
env.update({
"DEEPSEEK_API_KEY": "keyless-tui-no-call",
"DSH_HOME": os.path.join(cwd, ".dsh"),
"DSH_AGENTS_HOME": os.path.join(cwd, ".agents"),
"TSX_TSCONFIG_PATH": tsconfig_path,
"COLUMNS": "100",
"LINES": "30",
})
if resume_session_id:
env["RESUME_SESSION_ID"] = resume_session_id
pid, fd = pty.fork()
if pid == 0:
os.chdir(cwd)
os.execvpe(node, [node, "--expose-internals", "--import", tsx_loader, bin_script, config_path], env)
os.execvpe(node, [node, *json.loads(launch_args_json)], env)
output = bytearray()
answered_question = False
sent_prompt = False
sent_exit = False
deadline = time.monotonic() + 25
status = None
@@ -43,7 +44,16 @@ while time.monotonic() < deadline:
chunk = b""
if chunk:
output.extend(chunk)
if not sent_exit and b"agent REPL ready." in output:
if scenario == "conversation" and not sent_prompt and b"scripted TUI ready." in output:
os.write(fd, b"exercise the TUI\r")
sent_prompt = True
if scenario == "conversation" and sent_prompt and not answered_question and b"How should the scripted run proceed?" in output:
os.write(fd, b"\r")
answered_question = True
if scenario == "conversation" and answered_question and not sent_exit and b"Decision received. Scripted TUI run complete." in output:
os.write(fd, b"/exit\r")
sent_exit = True
if scenario == "boot" and not sent_exit and b"agent REPL ready." in output:
os.write(fd, b"/exit\r")
sent_exit = True
waited, candidate = os.waitpid(pid, os.WNOHANG)
@@ -55,13 +65,26 @@ if status is None:
os.kill(pid, signal.SIGKILL)
_, status = os.waitpid(pid, 0)
sys.stdout.buffer.write(output)
if resume_session_id:
if scenario == "resume-failure":
if b'ui-tui: agent "main" failed to start:' not in output:
sys.stderr.write("TUI did not render the startup failure before timeout\n")
sys.exit(126)
if not os.WIFEXITED(status) or os.WEXITSTATUS(status) != 1:
sys.stderr.write("TUI startup failure did not exit with status 1\n")
sys.exit(127)
elif scenario == "conversation":
if not sent_prompt:
sys.stderr.write("TUI did not render the scripted welcome marker before timeout\n")
sys.exit(128)
if not answered_question:
sys.stderr.write("TUI did not render the user-question dialog before timeout\n")
sys.exit(129)
if not sent_exit:
sys.stderr.write("TUI did not finish the scripted tool round-trip before timeout\n")
sys.exit(130)
if not os.WIFEXITED(status) or os.WEXITSTATUS(status) != 0:
sys.stderr.write("TUI scripted conversation did not exit cleanly\n")
sys.exit(131)
else:
if not sent_exit:
sys.stderr.write("TUI did not render its welcome marker before timeout\n")
@@ -71,20 +94,36 @@ else:
sys.exit(125)
`
async function runTuiLoaderSmoke(resumeSessionId = ''): Promise<string> {
interface TuiLoaderSmokeOptions {
config?: string
resumeSessionId?: string
scenario?: 'boot' | 'conversation' | 'resume-failure'
}
async function runTuiLoaderSmoke(options: TuiLoaderSmokeOptions = {}): Promise<string> {
const cwd = await mkdtemp(join(tmpdir(), 'coding-tui-smoke-'))
try {
const launch = resolveExampleLaunch({
srcBin: binScript,
configArgs: [options.config ?? configPath],
tsconfigPath,
exposeInternals: true,
env: {
DEEPSEEK_API_KEY: 'keyless-tui-no-call',
DSH_HOME: join(cwd, '.dsh'),
DSH_AGENTS_HOME: join(cwd, '.agents'),
},
})
return await new Promise((resolve, reject) => {
const child = spawn('python3', [
'-c',
PTY_DRIVER,
process.execPath,
tsxLoader,
binScript,
configPath,
tsconfigPath,
launch.command,
JSON.stringify(launch.args),
JSON.stringify(launch.env),
cwd,
resumeSessionId,
options.resumeSessionId ?? '',
options.scenario ?? 'boot',
], { stdio: ['ignore', 'pipe', 'pipe'] })
let stdout = ''
let stderr = ''
@@ -108,10 +147,20 @@ describe('coding-agent TUI keyless smoke (real Loader tree in a PTY)', () => {
const output = await runTuiLoaderSmoke()
expect(output).toContain('DEEPSEEK')
expect(output).toContain('agent REPL ready.')
expect(output).toContain('\u001B[?2004l')
}, LOADER_SMOKE_TEST_TIMEOUT_MS)
it('streams a response, answers a user-question dialog, completes the tool round-trip, and exits cleanly', async () => {
const output = await runTuiLoaderSmoke({ config: scriptedConfigPath, scenario: 'conversation' })
expect(output).toContain('I need one decision before I continue.')
expect(output).toContain('How should the scripted run proceed?')
expect(output).toContain('Safe')
expect(output).toContain('Decision received. Scripted TUI run complete.')
expect(output).toContain('\u001B[?2004l')
}, LOADER_SMOKE_TEST_TIMEOUT_MS)
it('prints a config-resume failure and exits instead of leaving a blank terminal', async () => {
const output = await runTuiLoaderSmoke('missing-session')
const output = await runTuiLoaderSmoke({ resumeSessionId: 'missing-session', scenario: 'resume-failure' })
expect(output).toContain('ui-tui: agent "main" failed to start:')
expect(output).toContain('missing-session')
}, LOADER_SMOKE_TEST_TIMEOUT_MS)
+5
View File
@@ -10,6 +10,7 @@
"examples": {
"entry": [
"echo-agent/src/*.ts",
"coding-agent/tests/fixtures/tui-scripted-llm.ts",
"*/tests/**/*.e2e.ts",
"*/tests/**/*.snapshot.ts"
],
@@ -113,6 +114,10 @@
"entry": ["tests/**/*.spec.ts"],
"project": ["src/**/*.ts", "tests/**/*.ts"]
},
"packages/ui/tui": {
"entry": ["tests/**/*.spec.ts", "tests/**/*.snapshot.ts"],
"project": ["src/**/*.ts", "tests/**/*.ts"]
},
"packages/examples/jsonrpc-demo": {
"project": ["src/**/*.ts"]
},
+5
View File
@@ -38,8 +38,13 @@
"@deepseek-ai/dsh-agent": "workspace:^",
"@deepseek-ai/dsh-llm": "workspace:^",
"@deepseek-ai/dsh-session": "workspace:^",
"@deepseek-ai/dsh-system-prompt": "workspace:^",
"@deepseek-ai/dsh-tool-cordis": "workspace:^",
"@deepseek-ai/dsh-tool-workflow": "workspace:^",
"@deepseek-ai/dsh-tools": "workspace:^",
"@deepseek-ai/dsh-user-interaction": "workspace:^",
"@deepseek-ai/dsh-workflow": "workspace:^",
"@xterm/headless": "5.5.0",
"cordis": "^4.0.0-rc.6"
}
}
+130
View File
@@ -0,0 +1,130 @@
import { Context } from 'cordis'
import type { Terminal } from '@earendil-works/pi-tui'
import AgentRegistry, { AgentId, type Agent, type AgentStatus } from '@deepseek-ai/dsh-agent'
import type { ContentBlock } from '@deepseek-ai/dsh-llm'
import SessionStore, { SessionId, type Session } from '@deepseek-ai/dsh-session'
import type { ToolDefinition } from '@deepseek-ai/dsh-tools'
import UserInteractionService from '@deepseek-ai/dsh-user-interaction'
import { createTuiChat, type Config } from '../src/index.ts'
interface FakeAgent extends Agent {
status: AgentStatus
sent: ContentBlock[][]
steered: ContentBlock[][]
cancelled: string[]
}
export interface TuiHarnessOptions {
status?: AgentStatus
config?: Config
tools?: Record<string, ToolDefinition>
configureContext?: (ctx: Context) => Promise<void>
beforeMount?: (session: Session) => void
cwd?: string | null
}
export interface TuiHarness<TerminalType extends Terminal, Exit extends (code: number) => void> {
ctx: Context
session: Session
agent: FakeAgent
terminal: TerminalType
exit: Exit
controller: ReturnType<typeof createTuiChat>
}
/**
* Compose the production TUI around an in-memory session and controllable agent.
* @param terminal - Terminal boundary driven by the test.
* @param exit - Process-exit observer.
* @param options - Initial session, agent, tool, and TUI configuration.
* @returns The mounted TUI and every boundary the test may drive or inspect.
*/
export async function createTuiTestHarness<TerminalType extends Terminal, Exit extends (code: number) => void>(
terminal: TerminalType,
exit: Exit,
options: TuiHarnessOptions = {},
): Promise<TuiHarness<TerminalType, Exit>> {
const ctx = new Context()
await ctx.plugin(SessionStore)
await ctx.plugin(AgentRegistry)
await ctx.plugin(UserInteractionService)
if (options.configureContext === undefined) {
const tools = options.tools ?? {}
ctx.provide('tools', {
get(name: string) {
return tools[name]
},
} as never)
} else {
await options.configureContext(ctx)
}
const session = ctx.sessions.create(
SessionId('main-session'),
options.cwd === null ? undefined : { meta: { cwd: options.cwd ?? '/workspace' } },
)
options.beforeMount?.(session)
const sent: ContentBlock[][] = []
const steered: ContentBlock[][] = []
const cancelled: string[] = []
const agent: FakeAgent = {
id: AgentId('main'),
options: { model: 'deepseek-v4-flash' },
session,
status: options.status ?? 'idle',
ctx,
sent,
steered,
cancelled,
send(content) {
sent.push(content)
},
steer(content) {
steered.push(content)
},
inject() {},
cancel(reason) {
cancelled.push(reason ?? '')
},
whenIdle() {
return Promise.resolve()
},
}
ctx.agents.register(agent)
const controller = createTuiChat(ctx, Object.assign({
welcome: 'Coding agent ready.',
agent: 'main',
color: false,
}, options.config), { terminal, exit })
return { ctx, session, agent, terminal, exit, controller }
}
/** Dispose the mounted TUI before its owning Cordis context. */
export async function disposeTuiTestHarness(
setup: Pick<TuiHarness<Terminal, (code: number) => void>, 'controller' | 'ctx'>,
): Promise<void> {
await setup.controller.dispose()
await setup.ctx.fiber.dispose()
}
/** Append a production-shaped user message to the active session surface. */
export function appendUser(session: Session, text: string): void {
session.append('user/message', {
content: [{ type: 'text', text }],
source: { kind: 'user' },
}, { surfaceOp: 'append' })
}
/** Append a production-shaped assistant message to the active session surface. */
export function appendAssistant(
session: Session,
content: ContentBlock[],
usage?: { inputTokens: number; outputTokens: number },
): void {
session.append('assistant/message', {
turn: 1,
step: 0,
provenance: { provider: 'mock', model: 'deepseek-v4-flash' },
content,
...usage === undefined ? {} : { usage },
}, { surfaceOp: 'append' })
}
+318
View File
@@ -0,0 +1,318 @@
import type { Terminal } from '@earendil-works/pi-tui'
import { Terminal as XtermTerminal, type IBufferCell } from '@xterm/headless'
const FRAME_END = '\x1b[?2026l'
const FRAME_TIMEOUT_MS = 2_000
const ANSI_COLORS = [
'black',
'red',
'green',
'yellow',
'blue',
'magenta',
'cyan',
'white',
'bright-black',
'bright-red',
'bright-green',
'bright-yellow',
'bright-blue',
'bright-magenta',
'bright-cyan',
'bright-white',
] as const
interface FrameWaiter {
target: number
resolve: () => void
reject: (error: Error) => void
timer: ReturnType<typeof setTimeout>
}
interface RowSnapshot {
text: string
wrapped: boolean
styles: string[]
}
export interface TerminalSnapshotOptions {
/** Include the whole active buffer instead of only the visible viewport. */
includeScrollback?: boolean
}
function occurrenceCount(value: string, needle: string): number {
let count = 0
let offset = 0
while (true) {
const match = value.indexOf(needle, offset)
if (match < 0) return count
count += 1
offset = match + needle.length
}
}
function colorLabel(cell: IBufferCell, kind: 'fg' | 'bg'): string | undefined {
const isDefault = kind === 'fg' ? cell.isFgDefault() : cell.isBgDefault()
if (isDefault) return undefined
const isRgb = kind === 'fg' ? cell.isFgRGB() : cell.isBgRGB()
const value = kind === 'fg' ? cell.getFgColor() : cell.getBgColor()
if (isRgb) return `${kind}=#${value.toString(16).padStart(6, '0')}`
const name = ANSI_COLORS[value]
return `${kind}=${name ?? `ansi-${value}`}`
}
function styleLabel(cell: IBufferCell): string {
const labels = [
colorLabel(cell, 'fg'),
colorLabel(cell, 'bg'),
cell.isBold() !== 0 ? 'bold' : undefined,
cell.isDim() !== 0 ? 'dim' : undefined,
cell.isItalic() !== 0 ? 'italic' : undefined,
cell.isUnderline() !== 0 ? 'underline' : undefined,
cell.isBlink() !== 0 ? 'blink' : undefined,
cell.isInverse() !== 0 ? 'inverse' : undefined,
cell.isInvisible() !== 0 ? 'invisible' : undefined,
cell.isStrikethrough() !== 0 ? 'strike' : undefined,
cell.isOverline() !== 0 ? 'overline' : undefined,
].filter((label): label is string => label !== undefined)
return labels.join(' ')
}
function snapshotRow(terminal: XtermTerminal, row: number): RowSnapshot {
const line = terminal.buffer.active.getLine(row)
if (line === undefined) return { text: '', wrapped: false, styles: [] }
const styles: string[] = []
let activeStyle = ''
let activeStart = 0
for (let column = 0; column <= terminal.cols; column++) {
const cell = column < terminal.cols ? line.getCell(column) : undefined
const style = cell === undefined ? '' : styleLabel(cell)
if (style === activeStyle) continue
if (activeStyle !== '') styles.push(`${activeStart}-${column - 1} ${activeStyle}`)
activeStyle = style
activeStart = column
}
return {
text: line.translateToString(true),
wrapped: line.isWrapped,
styles,
}
}
function renderRows(rows: readonly RowSnapshot[], firstRow: number): string[] {
const rendered: string[] = []
let blankStart: number | undefined
const flushBlanks = (end: number): void => {
if (blankStart === undefined) return
rendered.push(blankStart === end ? `${blankStart}| <blank>` : `${blankStart}-${end}| <blank>`)
blankStart = undefined
}
for (let index = 0; index < rows.length; index++) {
const absoluteRow = firstRow + index
const row = rows[index] as RowSnapshot
if (row.text === '' && row.styles.length === 0 && !row.wrapped) {
blankStart ??= absoluteRow
continue
}
flushBlanks(absoluteRow - 1)
rendered.push(`${absoluteRow}${row.wrapped ? '~' : ''}| ${JSON.stringify(row.text)}`)
for (const style of row.styles) rendered.push(` style ${style}`)
}
flushBlanks(firstRow + rows.length - 1)
return rendered
}
/**
* Terminal emulator used by TUI snapshots. It consumes the same ANSI stream as
* a real terminal and exposes completed synchronized frames as an awaitable boundary.
*/
export class HeadlessTerminal implements Terminal {
readonly kittyProtocolActive = false
readonly drainInput = (): Promise<void> => Promise.resolve()
started = 0
stopped = 0
title = ''
progress = false
cursorVisible = true
frames = 0
private readonly emulator: XtermTerminal
private onInput: (data: string) => void = () => {}
private onResize: () => void = () => {}
private pendingWrite: Promise<void> = Promise.resolve()
private readonly frameWaiters = new Set<FrameWaiter>()
constructor(columns = 80, rows = 24) {
this.emulator = new XtermTerminal({
cols: columns,
rows,
scrollback: 1_000,
allowProposedApi: true,
drawBoldTextInBrightColors: false,
logLevel: 'off',
})
}
get columns(): number {
return this.emulator.cols
}
get rows(): number {
return this.emulator.rows
}
start(onInput: (data: string) => void, onResize: () => void): void {
this.started += 1
this.onInput = onInput
this.onResize = onResize
}
stop(): void {
this.stopped += 1
}
write(data: string): void {
const completedFrames = occurrenceCount(data, FRAME_END)
this.pendingWrite = new Promise((resolve) => {
this.emulator.write(data, () => {
this.frames += completedFrames
for (const waiter of this.frameWaiters) {
if (this.frames < waiter.target) continue
clearTimeout(waiter.timer)
this.frameWaiters.delete(waiter)
waiter.resolve()
}
resolve()
})
})
}
moveBy(lines: number): void {
if (lines > 0) this.write(`\x1b[${lines}B`)
if (lines < 0) this.write(`\x1b[${-lines}A`)
}
hideCursor(): void {
this.cursorVisible = false
this.write('\x1b[?25l')
}
showCursor(): void {
this.cursorVisible = true
this.write('\x1b[?25h')
}
clearLine(): void {
this.write('\x1b[K')
}
clearFromCursor(): void {
this.write('\x1b[J')
}
clearScreen(): void {
this.write('\x1b[2J\x1b[H')
}
setTitle(title: string): void {
this.title = title
this.write(`\x1b]0;${title}\x07`)
}
setProgress(active: boolean): void {
this.progress = active
}
send(data: string): void {
this.onInput(data)
}
resize(columns: number, rows = this.rows): void {
this.emulator.resize(columns, rows)
this.onResize()
}
/** Wait until pi-tui completes a synchronized frame newer than `after`. */
async waitForFrame(after = this.frames): Promise<void> {
if (this.frames <= after) {
await new Promise<void>((resolve, reject) => {
const waiter: FrameWaiter = {
target: after + 1,
resolve,
reject,
timer: setTimeout(() => {
this.frameWaiters.delete(waiter)
reject(new Error(`TUI did not complete frame ${after + 1} within ${FRAME_TIMEOUT_MS}ms`))
}, FRAME_TIMEOUT_MS),
}
this.frameWaiters.add(waiter)
})
}
await this.flush()
}
/** Await every terminal write queued through the current task. */
async flush(): Promise<void> {
let pending: Promise<void>
do {
pending = this.pendingWrite
await pending
} while (pending !== this.pendingWrite)
}
/**
* Reject palette output that would become theme-specific in a user's terminal.
* @returns One location per RGB, extended-palette, or explicit-background cell.
*/
themeViolations(): string[] {
const violations: string[] = []
const buffer = this.emulator.buffer.active
for (let row = 0; row < buffer.length; row++) {
const line = buffer.getLine(row)
if (line === undefined) continue
for (let column = 0; column < this.columns; column++) {
const cell = line.getCell(column)
if (cell === undefined) continue
const reasons = [
cell.isFgRGB() ? 'rgb-fg' : undefined,
cell.isBgRGB() ? 'rgb-bg' : undefined,
cell.isFgPalette() && cell.getFgColor() > 15 ? `extended-fg-${cell.getFgColor()}` : undefined,
cell.isBgPalette() && cell.getBgColor() > 15 ? `extended-bg-${cell.getBgColor()}` : undefined,
!cell.isBgDefault() ? 'explicit-bg' : undefined,
].filter((reason): reason is string => reason !== undefined)
if (reasons.length > 0) violations.push(`${row}:${column} ${reasons.join(',')}`)
}
}
return violations
}
/** Serialize terminal cells and metadata into a stable, reviewable golden. */
async snapshot(options: TerminalSnapshotOptions = {}): Promise<string> {
await this.flush()
const buffer = this.emulator.buffer.active
const firstRow = options.includeScrollback === true ? 0 : buffer.viewportY
const rowCount = options.includeScrollback === true ? buffer.length : this.rows
const rows = Array.from({ length: rowCount }, (_, index) => snapshotRow(this.emulator, firstRow + index))
const cursorBufferRow = buffer.baseY + buffer.cursorY
const cursorViewportRow = cursorBufferRow - buffer.viewportY
return [
`terminal ${this.columns}x${this.rows} buffer=${buffer.type} length=${buffer.length} base=${buffer.baseY} viewport=${buffer.viewportY}`,
`lifecycle started=${this.started} stopped=${this.stopped} progress=${this.progress ? 'active' : 'inactive'}`,
`title ${JSON.stringify(this.title)}`,
`cursor ${this.cursorVisible ? 'visible' : 'hidden'} column=${buffer.cursorX} viewportRow=${cursorViewportRow} bufferRow=${cursorBufferRow}`,
options.includeScrollback === true ? 'buffer' : 'viewport',
...renderRows(rows, firstRow),
'',
].join('\n')
}
async dispose(): Promise<void> {
await this.flush()
for (const waiter of this.frameWaiters) {
clearTimeout(waiter.timer)
waiter.reject(new Error('terminal disposed before the requested frame completed'))
}
this.frameWaiters.clear()
this.emulator.dispose()
}
}
@@ -0,0 +1,107 @@
terminal 100x40 buffer=normal length=41 base=1 viewport=1
lifecycle started=1 stopped=0 progress=inactive
title "DSH snapshot"
cursor hidden column=1 viewportRow=37 bufferRow=38
buffer
0| "╭──────────────────────────────────────────────────────────────────────────────────────────────────╮"
style 0-99 fg=bright-blue
1| "│ DEEPSEEK HARNESS │"
style 0-0 fg=bright-blue
style 2-9 fg=bright-blue bold
style 11-17 bold
style 99-99 fg=bright-blue
2| "│ Snapshot agent ready. │"
style 0-0 fg=bright-blue
style 2-22 fg=bright-black
style 99-99 fg=bright-blue
3| "│ main • deepseek-v4-flash • main-session │"
style 0-0 fg=bright-blue
style 2-44 dim
style 99-99 fg=bright-blue
4| "╰──────────────────────────────────────────────────────────────────────────────────────────────────╯"
style 0-99 fg=bright-blue
5| <blank>
6| "▌ "
style 0-0 fg=green
7| "▌ ✓ pnpm run test:coverage "
style 0-0 fg=green
style 2-2 fg=green bold
style 3-25 bold
8| "▌ Run the coverage gate "
style 0-0 fg=green
style 2-22 fg=bright-black
9| "▌ /workspace/project "
style 0-0 fg=green
style 2-19 dim
10| "▌ packages/ui/tui 100% "
style 0-0 fg=green
11| "▌ … 4 more lines (Ctrl+O to expand) "
style 0-0 fg=green
style 2-34 dim
12| "▌ "
style 0-0 fg=green
13| <blank>
14| "▌ "
style 0-0 fg=green
15| "▌ ✓ Edit renderer "
style 0-0 fg=green
style 2-2 fg=green bold
style 3-16 bold
16| "▌ src/view.ts "
style 0-0 fg=green
style 2-12 bold
17| "▌ - old line "
style 0-0 fg=green
style 2-11 fg=red
18| "▌ - keep "
style 0-0 fg=green
style 2-7 fg=red
19| "▌ … 5 more lines (Ctrl+O to expand) "
style 0-0 fg=green
style 2-34 dim
20| "▌ "
style 0-0 fg=green
21| <blank>
22| "▌ "
style 0-0 fg=green
23| "▌ ✓ Delegate renderer audit "
style 0-0 fg=green
style 2-2 fg=green bold
style 3-26 bold
24| "▌ The renderer has explicit lifecycle ownership. "
style 0-0 fg=green
25| "▌ "
style 0-0 fg=green
26| <blank>
27| "▌ "
style 0-0 fg=green
28| "▌ ✓ Read output from background task subagent-7 "
style 0-0 fg=green
style 2-2 fg=green bold
style 3-46 bold
29| "▌ audit complete "
style 0-0 fg=green
30| "▌ [status: completed] "
style 0-0 fg=green
31| "▌ "
style 0-0 fg=green
32| <blank>
33| "▌ "
style 0-0 fg=green
34| "▌ ✓ Load skill dsh-code-review "
style 0-0 fg=green
style 2-2 fg=green bold
style 3-29 bold
35| "▌ Loaded review instructions. "
style 0-0 fg=green
36| "▌ "
style 0-0 fg=green
37| "────────────────────────────────────────────────────────────────────────────────────────────────────"
style 0-99 dim
38| " "
style 1-1 inverse
39| "────────────────────────────────────────────────────────────────────────────────────────────────────"
style 0-99 dim
40| "/workspace/project ↑0 ↓0 idle reasoning:on tools:compact"
style 0-24 dim
style 67-99 dim
@@ -0,0 +1,127 @@
terminal 100x40 buffer=normal length=50 base=10 viewport=10
lifecycle started=1 stopped=0 progress=inactive
title "DSH snapshot"
cursor hidden column=1 viewportRow=37 bufferRow=47
buffer
0| "╭──────────────────────────────────────────────────────────────────────────────────────────────────╮"
style 0-99 fg=bright-blue
1| "│ DEEPSEEK HARNESS │"
style 0-0 fg=bright-blue
style 2-9 fg=bright-blue bold
style 11-17 bold
style 99-99 fg=bright-blue
2| "│ Snapshot agent ready. │"
style 0-0 fg=bright-blue
style 2-22 fg=bright-black
style 99-99 fg=bright-blue
3| "│ main • deepseek-v4-flash • main-session │"
style 0-0 fg=bright-blue
style 2-44 dim
style 99-99 fg=bright-blue
4| "╰──────────────────────────────────────────────────────────────────────────────────────────────────╯"
style 0-99 fg=bright-blue
5| <blank>
6| "▌ "
style 0-0 fg=green
7| "▌ ✓ pnpm run test:coverage "
style 0-0 fg=green
style 2-2 fg=green bold
style 3-25 bold
8| "▌ Run the coverage gate "
style 0-0 fg=green
style 2-22 fg=bright-black
9| "▌ /workspace/project "
style 0-0 fg=green
style 2-19 dim
10| "▌ packages/ui/tui 100% "
style 0-0 fg=green
11| "▌ 4016 tests passed "
style 0-0 fg=green
12| "▌ 1 test skipped "
style 0-0 fg=green
13| "▌ coverage complete "
style 0-0 fg=green
14| "▌ [exit 0] "
style 0-0 fg=green
style 2-9 dim
15| "▌ "
style 0-0 fg=green
16| <blank>
17| "▌ "
style 0-0 fg=green
18| "▌ ✓ Edit renderer "
style 0-0 fg=green
style 2-2 fg=green bold
style 3-16 bold
19| "▌ src/view.ts "
style 0-0 fg=green
style 2-12 bold
20| "▌ - old line "
style 0-0 fg=green
style 2-11 fg=red
21| "▌ - keep "
style 0-0 fg=green
style 2-7 fg=red
22| "▌ + new line "
style 0-0 fg=green
style 2-11 fg=green
23| "▌ + keep "
style 0-0 fg=green
style 2-7 fg=green
24| "▌ "
style 0-0 fg=green
25| "▌ tests/view.spec.ts "
style 0-0 fg=green
style 2-19 bold
26| "▌ + expect(screen).toMatchSnapshot() "
style 0-0 fg=green
style 2-35 fg=green
27| "▌ "
style 0-0 fg=green
28| <blank>
29| "▌ "
style 0-0 fg=green
30| "▌ ✓ Delegate renderer audit "
style 0-0 fg=green
style 2-2 fg=green bold
style 3-26 bold
31| "▌ The renderer has explicit lifecycle ownership. "
style 0-0 fg=green
32| "▌ "
style 0-0 fg=green
33| <blank>
34| "▌ "
style 0-0 fg=green
35| "▌ ✓ Read output from background task subagent-7 "
style 0-0 fg=green
style 2-2 fg=green bold
style 3-46 bold
36| "▌ audit complete "
style 0-0 fg=green
37| "▌ [status: completed] "
style 0-0 fg=green
38| "▌ "
style 0-0 fg=green
39| <blank>
40| "▌ "
style 0-0 fg=green
41| "▌ ✓ Load skill dsh-code-review "
style 0-0 fg=green
style 2-2 fg=green bold
style 3-29 bold
42| "▌ Loaded review instructions. "
style 0-0 fg=green
43| "▌ "
style 0-0 fg=green
44| <blank>
45| " Tool cards expanded. "
style 1-20 fg=bright-black
46| "────────────────────────────────────────────────────────────────────────────────────────────────────"
style 0-99 dim
47| " "
style 1-1 inverse
48| "────────────────────────────────────────────────────────────────────────────────────────────────────"
style 0-99 dim
49| "/workspace/project ↑0 ↓0 idle reasoning:on tools:expanded"
style 0-24 dim
style 66-99 dim
@@ -0,0 +1,50 @@
terminal 96x36 buffer=normal length=36 base=0 viewport=0
lifecycle started=1 stopped=0 progress=inactive
title "DSH snapshot"
cursor hidden column=1 viewportRow=14 bufferRow=14
buffer
0| "╭──────────────────────────────────────────────────────────────────────────────────────────────╮"
style 0-95 fg=bright-blue
1| "│ DEEPSEEK HARNESS │"
style 0-0 fg=bright-blue
style 2-9 fg=bright-blue bold
style 11-17 bold
style 95-95 fg=bright-blue
2| "│ Snapshot agent ready. │"
style 0-0 fg=bright-blue
style 2-22 fg=bright-black
style 95-95 fg=bright-blue
3| "│ main • deepseek-v4-flash • main-session │"
style 0-0 fg=bright-blue
style 2-44 dim
style 95-95 fg=bright-blue
4| "╰──────────────────────────────────────────────────────────────────────────────────────────────╯"
style 0-95 fg=bright-blue
5| <blank>
6| "▌ "
style 0-0 fg=green
7| "▌ ✓ const first = await tools.bash({ command: 'echo CODE_ONE' }) "
style 0-0 fg=green
style 2-2 fg=green bold
style 3-95 bold
8| "▌ const second = await tools.bas "
style 0-0 fg=green
style 2-31 bold
9| "▌ CODE_ONE "
style 0-0 fg=green
10| "▌ CODE_TWO "
style 0-0 fg=green
11| "▌ combined: CODE_ONE+CODE_TWO "
style 0-0 fg=green
12| "▌ "
style 0-0 fg=green
13| "────────────────────────────────────────────────────────────────────────────────────────────────"
style 0-95 dim
14| " "
style 1-1 inverse
15| "────────────────────────────────────────────────────────────────────────────────────────────────"
style 0-95 dim
16| "/workspace/project ↑0 ↓0 idle reasoning:on tools:compact"
style 0-24 dim
style 63-95 dim
17-35| <blank>
@@ -0,0 +1,52 @@
terminal 96x36 buffer=normal length=36 base=0 viewport=0
lifecycle started=1 stopped=0 progress=inactive
title "DSH snapshot"
cursor hidden column=1 viewportRow=15 bufferRow=15
buffer
0| "╭──────────────────────────────────────────────────────────────────────────────────────────────╮"
style 0-95 fg=bright-blue
1| "│ DEEPSEEK HARNESS │"
style 0-0 fg=bright-blue
style 2-9 fg=bright-blue bold
style 11-17 bold
style 95-95 fg=bright-blue
2| "│ Snapshot agent ready. │"
style 0-0 fg=bright-blue
style 2-22 fg=bright-black
style 95-95 fg=bright-blue
3| "│ main • deepseek-v4-flash • main-session │"
style 0-0 fg=bright-blue
style 2-44 dim
style 95-95 fg=bright-blue
4| "╰──────────────────────────────────────────────────────────────────────────────────────────────╯"
style 0-95 fg=bright-blue
5| <blank>
6| "▌ "
style 0-0 fg=yellow
7| "▌ ◌ const first = await tools.bash({ command: 'echo CODE_ONE' }) "
style 0-0 fg=yellow
style 2-2 fg=yellow bold
style 3-95 bold
8| "▌ const second = await tools.bas "
style 0-0 fg=yellow
style 2-31 bold
9| "▌ const first = await tools.bash({ command: 'echo CODE_ONE' }) "
style 0-0 fg=yellow
10| "▌ const second = await tools.bash({ command: 'echo CODE_TWO' }) "
style 0-0 fg=yellow
11| "▌ console.log(first, second) "
style 0-0 fg=yellow
12| "▌ return `${first}+${second}` "
style 0-0 fg=yellow
13| "▌ "
style 0-0 fg=yellow
14| "────────────────────────────────────────────────────────────────────────────────────────────────"
style 0-95 dim
15| " "
style 1-1 inverse
16| "────────────────────────────────────────────────────────────────────────────────────────────────"
style 0-95 dim
17| "/workspace/project ↑0 ↓0 idle reasoning:on tools:compact"
style 0-24 dim
style 63-95 dim
18-35| <blank>
@@ -0,0 +1,75 @@
terminal 96x36 buffer=normal length=43 base=7 viewport=7
lifecycle started=1 stopped=0 progress=inactive
title "DSH snapshot"
cursor hidden column=1 viewportRow=33 bufferRow=40
viewport
7| "▌ You "
style 0-0 fg=bright-blue
style 2-4 fg=bright-blue bold
8| "▌ Explain snapshot fidelity with cells. "
style 0-0 fg=bright-blue
style 10-26 bold
style 33-37 fg=cyan
9| "▌ "
style 0-0 fg=bright-blue
10| <blank>
11| " Reasoning "
style 1-9 fg=bright-black italic
12| " Compare the terminal state, not write fragments. "
style 1-48 fg=bright-black italic
13| <blank>
14| " Assistant "
style 1-9 fg=bright-magenta bold
15| " Result "
style 1-6 fg=bright-blue bold
16| " "
17| " - final viewport "
style 1-2 fg=bright-blue
18| " - semantic styles "
style 1-2 fg=bright-blue
19| " "
20| " │ deterministic and reviewable "
style 1-2 fg=bright-magenta
style 3-30 fg=bright-black italic
21| <blank>
22| "▌ "
style 0-0 fg=bright-blue
23| "▌ You "
style 0-0 fg=bright-blue
style 2-4 fg=bright-blue bold
24| "▌ Show the live update. "
style 0-0 fg=bright-blue
25| "▌ "
style 0-0 fg=bright-blue
26| <blank>
27| " Reasoning "
style 1-9 fg=bright-black italic
28| " Inspecting width and styles. "
style 1-28 fg=bright-black italic
29| <blank>
30| " Assistant "
style 1-9 fg=bright-magenta bold
31| " Streaming visible state is complete. "
style 11-23 bold
32| <blank>
33| " The model reached its output-token limit. "
style 1-41 fg=yellow
34| <blank>
35| "Plan"
style 0-3 fg=bright-blue bold
36| " ✓ model the terminal"
style 2-2 fg=green
style 4-21 fg=bright-black
37| " ● capture advanced states"
style 2-2 fg=yellow
38| " ○ verify PTY cleanup"
style 2-2 dim
39| "────────────────────────────────────────────────────────────────────────────────────────────────"
style 0-95 dim
40| " "
style 1-1 inverse
41| "────────────────────────────────────────────────────────────────────────────────────────────────"
style 0-95 dim
42| "/workspace/project ↑13k ↓760 idle reasoning:on tools:compact"
style 0-28 dim
style 63-95 dim
@@ -0,0 +1,73 @@
terminal 96x36 buffer=normal length=36 base=0 viewport=0
lifecycle started=1 stopped=0 progress=inactive
title "DSH snapshot"
cursor hidden column=1 viewportRow=27 bufferRow=27
viewport
0| "╭──────────────────────────────────────────────────────────────────────────────────────────────╮"
style 0-95 fg=bright-blue
1| "│ DEEPSEEK HARNESS │"
style 0-0 fg=bright-blue
style 2-9 fg=bright-blue bold
style 11-17 bold
style 95-95 fg=bright-blue
2| "│ Snapshot agent ready. │"
style 0-0 fg=bright-blue
style 2-22 fg=bright-black
style 95-95 fg=bright-blue
3| "│ main • deepseek-v4-flash • main-session │"
style 0-0 fg=bright-blue
style 2-44 dim
style 95-95 fg=bright-blue
4| "╰──────────────────────────────────────────────────────────────────────────────────────────────╯"
style 0-95 fg=bright-blue
5| <blank>
6| "▌ "
style 0-0 fg=bright-blue
7| "▌ You "
style 0-0 fg=bright-blue
style 2-4 fg=bright-blue bold
8| "▌ Explain snapshot fidelity with cells. "
style 0-0 fg=bright-blue
style 10-26 bold
style 33-37 fg=cyan
9| "▌ "
style 0-0 fg=bright-blue
10| <blank>
11| " Reasoning "
style 1-9 fg=bright-black italic
12| " Compare the terminal state, not write fragments. "
style 1-48 fg=bright-black italic
13| <blank>
14| " Assistant "
style 1-9 fg=bright-magenta bold
15| " Result "
style 1-6 fg=bright-blue bold
16| " "
17| " - final viewport "
style 1-2 fg=bright-blue
18| " - semantic styles "
style 1-2 fg=bright-blue
19| " "
20| " │ deterministic and reviewable "
style 1-2 fg=bright-magenta
style 3-30 fg=bright-black italic
21| <blank>
22| "Plan"
style 0-3 fg=bright-blue bold
23| " ✓ model the terminal"
style 2-2 fg=green
style 4-21 fg=bright-black
24| " ● capture advanced states"
style 2-2 fg=yellow
25| " ○ verify PTY cleanup"
style 2-2 dim
26| "────────────────────────────────────────────────────────────────────────────────────────────────"
style 0-95 dim
27| " "
style 1-1 inverse
28| "────────────────────────────────────────────────────────────────────────────────────────────────"
style 0-95 dim
29| "/workspace/project ↑13k ↓640 idle reasoning:on tools:compact"
style 0-28 dim
style 63-95 dim
30-35| <blank>
@@ -0,0 +1,75 @@
terminal 96x36 buffer=normal length=41 base=5 viewport=5
lifecycle started=1 stopped=0 progress=inactive
title "DSH snapshot"
cursor hidden column=1 viewportRow=33 bufferRow=38
viewport
5| <blank>
6| "▌ "
style 0-0 fg=bright-blue
7| "▌ You "
style 0-0 fg=bright-blue
style 2-4 fg=bright-blue bold
8| "▌ Explain snapshot fidelity with cells. "
style 0-0 fg=bright-blue
style 10-26 bold
style 33-37 fg=cyan
9| "▌ "
style 0-0 fg=bright-blue
10| <blank>
11| " Reasoning "
style 1-9 fg=bright-black italic
12| " Compare the terminal state, not write fragments. "
style 1-48 fg=bright-black italic
13| <blank>
14| " Assistant "
style 1-9 fg=bright-magenta bold
15| " Result "
style 1-6 fg=bright-blue bold
16| " "
17| " - final viewport "
style 1-2 fg=bright-blue
18| " - semantic styles "
style 1-2 fg=bright-blue
19| " "
20| " │ deterministic and reviewable "
style 1-2 fg=bright-magenta
style 3-30 fg=bright-black italic
21| <blank>
22| "▌ "
style 0-0 fg=bright-blue
23| "▌ You "
style 0-0 fg=bright-blue
style 2-4 fg=bright-blue bold
24| "▌ Show the live update. "
style 0-0 fg=bright-blue
25| "▌ "
style 0-0 fg=bright-blue
26| <blank>
27| " Reasoning "
style 1-9 fg=bright-black italic
28| " Inspecting width and styles. "
style 1-28 fg=bright-black italic
29| <blank>
30| " Assistant "
style 1-9 fg=bright-magenta bold
31| " Streaming visible state… "
style 11-23 bold
32| <blank>
33| "Plan"
style 0-3 fg=bright-blue bold
34| " ✓ model the terminal"
style 2-2 fg=green
style 4-21 fg=bright-black
35| " ● capture advanced states"
style 2-2 fg=yellow
36| " ○ verify PTY cleanup"
style 2-2 dim
37| "────────────────────────────────────────────────────────────────────────────────────────────────"
style 0-95 dim
38| " "
style 1-1 inverse
39| "────────────────────────────────────────────────────────────────────────────────────────────────"
style 0-95 dim
40| "/workspace/project ↑13k ↓640 idle reasoning:on tools:compact"
style 0-28 dim
style 63-95 dim
@@ -0,0 +1,73 @@
terminal 96x36 buffer=normal length=36 base=0 viewport=0
lifecycle started=1 stopped=0 progress=inactive
title "DSH snapshot"
cursor hidden column=1 viewportRow=25 bufferRow=25
buffer
0| "╭──────────────────────────────────────────────────────────────────────────────────────────────╮"
style 0-95 fg=bright-blue
1| "│ DEEPSEEK HARNESS │"
style 0-0 fg=bright-blue
style 2-9 fg=bright-blue bold
style 11-17 bold
style 95-95 fg=bright-blue
2| "│ Snapshot agent ready. │"
style 0-0 fg=bright-blue
style 2-22 fg=bright-black
style 95-95 fg=bright-blue
3| "│ main • deepseek-v4-flash • main-session │"
style 0-0 fg=bright-blue
style 2-44 dim
style 95-95 fg=bright-blue
4| "╰──────────────────────────────────────────────────────────────────────────────────────────────╯"
style 0-95 fg=bright-blue
5| <blank>
6| "▌ "
style 0-0 fg=green
7| "▌ ✓ Inspect cordis runtime: tools "
style 0-0 fg=green
style 2-2 fg=green bold
style 3-32 bold
8| "▌ ## tools "
style 0-0 fg=green
9| "▌ run_code "
style 0-0 fg=green
10| "▌ workflow "
style 0-0 fg=green
11| "▌ cordis_mount "
style 0-0 fg=green
12| "▌ cordis_unmount "
style 0-0 fg=green
13| "▌ "
style 0-0 fg=green
14| <blank>
15| "▌ "
style 0-0 fg=green
16| "▌ ✓ Mount plugin into live cordis runtime "
style 0-0 fg=green
style 2-2 fg=green bold
style 3-40 bold
17| "▌ mounted dyn-1 (plugin \"snapshot-marker\", state: active) "
style 0-0 fg=green
18| "▌ "
style 0-0 fg=green
19| <blank>
20| "▌ "
style 0-0 fg=green
21| "▌ ✓ Unmount dyn-1 "
style 0-0 fg=green
style 2-2 fg=green bold
style 3-16 bold
22| "▌ unmounted dyn-1 (plugin \"snapshot-marker\") "
style 0-0 fg=green
23| "▌ "
style 0-0 fg=green
24| "────────────────────────────────────────────────────────────────────────────────────────────────"
style 0-95 dim
25| " "
style 1-1 inverse
26| "────────────────────────────────────────────────────────────────────────────────────────────────"
style 0-95 dim
27| "/workspace/project ↑0 ↓0 idle reasoning:on tools:compact"
style 0-24 dim
style 63-95 dim
28-35| <blank>
@@ -0,0 +1,59 @@
terminal 96x36 buffer=normal length=36 base=0 viewport=0
lifecycle started=1 stopped=0 progress=inactive
title "DSH snapshot"
cursor hidden column=1 viewportRow=18 bufferRow=18
buffer
0| "╭──────────────────────────────────────────────────────────────────────────────────────────────╮"
style 0-95 fg=bright-blue
1| "│ DEEPSEEK HARNESS │"
style 0-0 fg=bright-blue
style 2-9 fg=bright-blue bold
style 11-17 bold
style 95-95 fg=bright-blue
2| "│ Snapshot agent ready. │"
style 0-0 fg=bright-blue
style 2-22 fg=bright-black
style 95-95 fg=bright-blue
3| "│ main • deepseek-v4-flash • main-session │"
style 0-0 fg=bright-blue
style 2-44 dim
style 95-95 fg=bright-blue
4| "╰──────────────────────────────────────────────────────────────────────────────────────────────╯"
style 0-95 fg=bright-blue
5| <blank>
6| "▌ ◌ Inspect cordis runtime: tools "
style 0-0 fg=yellow
style 2-2 fg=yellow bold
style 3-32 bold
7| <blank>
8| "▌ "
style 0-0 fg=yellow
9| "▌ ◌ Mount plugin into live cordis runtime "
style 0-0 fg=yellow
style 2-2 fg=yellow bold
style 3-40 bold
10| "▌ { "
style 0-0 fg=yellow
11| "▌ \"code\": \"return { name: 'snapshot-marker', apply(ctx) { ctx.provide('snapshotMarker', { "
style 0-0 fg=yellow
12| "▌ ready: true }) } }\" "
style 0-0 fg=yellow
13| "▌ } "
style 0-0 fg=yellow
14| "▌ "
style 0-0 fg=yellow
15| <blank>
16| "▌ ◌ Unmount dyn-1 "
style 0-0 fg=yellow
style 2-2 fg=yellow bold
style 3-16 bold
17| "────────────────────────────────────────────────────────────────────────────────────────────────"
style 0-95 dim
18| " "
style 1-1 inverse
19| "────────────────────────────────────────────────────────────────────────────────────────────────"
style 0-95 dim
20| "/workspace/project ↑0 ↓0 idle reasoning:on tools:compact"
style 0-24 dim
style 63-95 dim
21-35| <blank>
@@ -0,0 +1,52 @@
terminal 92x32 buffer=normal length=32 base=0 viewport=0
lifecycle started=1 stopped=1 progress=inactive
title "DSH snapshot"
cursor visible column=0 viewportRow=22 bufferRow=22
buffer
0| "╭──────────────────────────────────────────────────────────────────────────────────────────╮"
style 0-91 fg=bright-blue
1| "│ DEEPSEEK HARNESS │"
style 0-0 fg=bright-blue
style 2-9 fg=bright-blue bold
style 11-17 bold
style 91-91 fg=bright-blue
2| "│ Snapshot agent ready. │"
style 0-0 fg=bright-blue
style 2-22 fg=bright-black
style 91-91 fg=bright-blue
3| "│ main • deepseek-v4-flash • main-session │"
style 0-0 fg=bright-blue
style 2-44 dim
style 91-91 fg=bright-blue
4| "╰──────────────────────────────────────────────────────────────────────────────────────────╯"
style 0-91 fg=bright-blue
5| <blank>
6| " Keyboard shortcuts "
style 1-18 fg=bright-blue bold
7| " Enter send • Shift/Alt+Enter newline • Up/Down prompt history "
style 1-61 fg=bright-black
8| " Esc cancel active turn • Ctrl+O expand tool cards • Ctrl+R toggle reasoning "
style 1-75 fg=bright-black
9| " Ctrl+C cancel while running; clear input or exit while idle • Ctrl+D exit "
style 1-73 fg=bright-black
10| " /help /clear /cancel /reasoning /tools /redraw /exit "
style 1-52 fg=bright-black
11| <blank>
12| " Unknown command: /unknown-advanced-command "
style 1-42 fg=yellow
13| <blank>
14| " provider stream failed after partial output "
style 1-43 fg=red
15| <blank>
16| " The previous process ended during this turn. "
style 1-44 fg=yellow
17| "────────────────────────────────────────────────────────────────────────────────────────────"
style 0-91 dim
18| " "
style 1-1 inverse
19| "────────────────────────────────────────────────────────────────────────────────────────────"
style 0-91 dim
20| "/workspace/project ↑0 ↓0 idle reasoning:on tools:compact"
style 0-24 dim
style 59-91 dim
21-31| <blank>
@@ -0,0 +1,53 @@
terminal 96x36 buffer=normal length=36 base=0 viewport=0
lifecycle started=1 stopped=0 progress=inactive
title "DSH snapshot"
cursor hidden column=1 viewportRow=16 bufferRow=16
buffer
0| "╭──────────────────────────────────────────────────────────────────────────────────────────────╮"
style 0-95 fg=bright-blue
1| "│ DEEPSEEK HARNESS │"
style 0-0 fg=bright-blue
style 2-9 fg=bright-blue bold
style 11-17 bold
style 95-95 fg=bright-blue
2| "│ Snapshot agent ready. │"
style 0-0 fg=bright-blue
style 2-22 fg=bright-black
style 95-95 fg=bright-blue
3| "│ main • deepseek-v4-flash • main-session │"
style 0-0 fg=bright-blue
style 2-44 dim
style 95-95 fg=bright-blue
4| "╰──────────────────────────────────────────────────────────────────────────────────────────────╯"
style 0-95 fg=bright-blue
5| <blank>
6| "▌ "
style 0-0 fg=green
7| "▌ ✓ workflow: tui-matrix "
style 0-0 fg=green
style 2-2 fg=green bold
style 3-23 bold
8| "▌ workflow \"tui-matrix\" completed (2 agents). "
style 0-0 fg=green
9| "▌ Return value: "
style 0-0 fg=green
10| "▌ { "
style 0-0 fg=green
11| "▌ \"reports\": [\"layout ok\", \"lifecycle ok\"], "
style 0-0 fg=green
12| "▌ \"verdict\": \"covered\" "
style 0-0 fg=green
13| "▌ } "
style 0-0 fg=green
14| "▌ "
style 0-0 fg=green
15| "────────────────────────────────────────────────────────────────────────────────────────────────"
style 0-95 dim
16| " "
style 1-1 inverse
17| "────────────────────────────────────────────────────────────────────────────────────────────────"
style 0-95 dim
18| "/workspace/project ↑0 ↓0 idle reasoning:on tools:compact"
style 0-24 dim
style 63-95 dim
19-35| <blank>
@@ -0,0 +1,55 @@
terminal 96x36 buffer=normal length=36 base=0 viewport=0
lifecycle started=1 stopped=0 progress=inactive
title "DSH snapshot"
cursor hidden column=1 viewportRow=17 bufferRow=17
buffer
0| "╭──────────────────────────────────────────────────────────────────────────────────────────────╮"
style 0-95 fg=bright-blue
1| "│ DEEPSEEK HARNESS │"
style 0-0 fg=bright-blue
style 2-9 fg=bright-blue bold
style 11-17 bold
style 95-95 fg=bright-blue
2| "│ Snapshot agent ready. │"
style 0-0 fg=bright-blue
style 2-22 fg=bright-black
style 95-95 fg=bright-blue
3| "│ main • deepseek-v4-flash • main-session │"
style 0-0 fg=bright-blue
style 2-44 dim
style 95-95 fg=bright-blue
4| "╰──────────────────────────────────────────────────────────────────────────────────────────────╯"
style 0-95 fg=bright-blue
5| <blank>
6| "▌ "
style 0-0 fg=yellow
7| "▌ ◌ workflow: tui-matrix "
style 0-0 fg=yellow
style 2-2 fg=yellow bold
style 3-23 bold
8| "▌ phase('Inspect') "
style 0-0 fg=yellow
9| "▌ const reports = await parallel([ "
style 0-0 fg=yellow
10| "▌ () => agent('Audit layout', { label: 'layout', phase: 'Inspect' }), "
style 0-0 fg=yellow
11| "▌ () => agent('Audit lifecycle', { label: 'lifecycle', phase: 'Inspect' }), "
style 0-0 fg=yellow
12| "▌ ]) "
style 0-0 fg=yellow
13| "▌ phase('Verify') "
style 0-0 fg=yellow
14| "▌ return { reports, verdict: 'covered' } "
style 0-0 fg=yellow
15| "▌ "
style 0-0 fg=yellow
16| "────────────────────────────────────────────────────────────────────────────────────────────────"
style 0-95 dim
17| " "
style 1-1 inverse
18| "────────────────────────────────────────────────────────────────────────────────────────────────"
style 0-95 dim
19| "/workspace/project ↑0 ↓0 idle reasoning:on tools:compact"
style 0-24 dim
style 63-95 dim
20-35| <blank>
@@ -0,0 +1,52 @@
terminal 92x32 buffer=normal length=32 base=0 viewport=0
lifecycle started=1 stopped=0 progress=inactive
title "DSH snapshot"
cursor hidden column=1 viewportRow=18 bufferRow=18
buffer
0| "╭──────────────────────────────────────────────────────────────────────────────────────────╮"
style 0-91 fg=bright-blue
1| "│ DEEPSEEK HARNESS │"
style 0-0 fg=bright-blue
style 2-9 fg=bright-blue bold
style 11-17 bold
style 91-91 fg=bright-blue
2| "│ Snapshot agent ready. │"
style 0-0 fg=bright-blue
style 2-22 fg=bright-black
style 91-91 fg=bright-blue
3| "│ main • deepseek-v4-flash • main-session │"
style 0-0 fg=bright-blue
style 2-44 dim
style 91-91 fg=bright-blue
4| "╰──────────────────────────────────────────────────────────────────────────────────────────╯"
style 0-91 fg=bright-blue
5| <blank>
6| " Keyboard shortcuts "
style 1-18 fg=bright-blue bold
7| " Enter send • Shift/Alt+Enter newline • Up/Down prompt history "
style 1-61 fg=bright-black
8| " Esc cancel active turn • Ctrl+O expand tool cards • Ctrl+R toggle reasoning "
style 1-75 fg=bright-black
9| " Ctrl+C cancel while running; clear input or exit while idle • Ctrl+D exit "
style 1-73 fg=bright-black
10| " /help /clear /cancel /reasoning /tools /redraw /exit "
style 1-52 fg=bright-black
11| <blank>
12| " Unknown command: /unknown-advanced-command "
style 1-42 fg=yellow
13| <blank>
14| " provider stream failed after partial output "
style 1-43 fg=red
15| <blank>
16| " The previous process ended during this turn. "
style 1-44 fg=yellow
17| "────────────────────────────────────────────────────────────────────────────────────────────"
style 0-91 dim
18| " "
style 1-1 inverse
19| "────────────────────────────────────────────────────────────────────────────────────────────"
style 0-91 dim
20| "/workspace/project ↑0 ↓0 idle reasoning:on tools:compact"
style 0-24 dim
style 59-91 dim
21-31| <blank>
@@ -0,0 +1,69 @@
terminal 56x20 buffer=normal length=20 base=0 viewport=0
lifecycle started=1 stopped=0 progress=inactive
title "DSH snapshot"
cursor hidden column=56 viewportRow=13 bufferRow=13
viewport
0| "╭──────────────────────────────────────────────────────╮"
style 0-55 fg=bright-blue
1| "│ DEEPSEEK HARNESS │"
style 0-0 fg=bright-blue
style 2-9 fg=bright-blue bold
style 11-17 bold
style 55-55 fg=bright-blue
2| "│ Snapshot agent ready. │"
style 0-0 fg=bright-blue
style 2-22 fg=bright-black
style 55-55 fg=bright-blue
3| "│ main • deepseek-v4-flash • main-session │"
style 0-0 fg=bright-blue
style 2-44 dim
style 55-55 fg=bright-blue
4| "╰───╭ Coverage ────────────────────────────────────╮───╯"
style 0-55 fg=bright-blue
5| "────│ Which advanced TUI states belong in the │────"
style 0-3 dim
style 4-4 fg=bright-blue
style 6-50 bold
style 51-51 fg=bright-blue bold
style 52-55 dim
6| " │ required matrix? │ "
style 1-1 inverse
style 4-4 fg=bright-blue
style 6-21 bold
style 51-51 fg=bright-blue
7| "────│ │────"
style 0-3 dim
style 4-4 fg=bright-blue
style 51-51 fg=bright-blue
style 52-55 dim
8| "/wor│ [ ] Code Mode — run_code programs and capt │:com"
style 0-3 dim
style 4-4 fg=bright-blue
style 6-6 fg=bright-blue inverse
style 7-20 inverse
style 21-49 fg=bright-black inverse
style 51-51 fg=bright-blue
style 52-55 dim
9| " │ [ ] Workflows — phases and parallel agents │ "
style 4-4 fg=bright-blue
style 21-49 fg=bright-black
style 51-51 fg=bright-blue
10| " │ [ ] Cordis tools — inspect, mount, and unm │ "
style 4-4 fg=bright-blue
style 24-49 fg=bright-black
style 51-51 fg=bright-blue
11| " │ 1/4 │ "
style 4-4 fg=bright-blue
style 6-8 dim
style 51-51 fg=bright-blue
12| " │ ↑↓ navigate • Space toggle • Enter submit • │ "
style 4-4 fg=bright-blue
style 6-49 dim
style 51-51 fg=bright-blue
13| " │ Select at least one option, or press C for a │ "
style 4-4 fg=bright-blue
style 6-49 fg=red
style 51-51 fg=bright-blue
14| " ╰──────────────────────────────────────────────╯ "
style 4-51 fg=bright-blue
15-19| <blank>
@@ -0,0 +1,67 @@
terminal 56x20 buffer=normal length=20 base=0 viewport=0
lifecycle started=1 stopped=0 progress=inactive
title "DSH snapshot"
cursor hidden column=0 viewportRow=19 bufferRow=19
viewport
0| "╭──────────────────────────────────────────────────────╮"
style 0-55 fg=bright-blue
1| "│ DEEPSEEK HARNESS │"
style 0-0 fg=bright-blue
style 2-9 fg=bright-blue bold
style 11-17 bold
style 55-55 fg=bright-blue
2| "│ Snapshot agent ready. │"
style 0-0 fg=bright-blue
style 2-22 fg=bright-black
style 55-55 fg=bright-blue
3| "│ main • deepseek-v4-flash • main-session │"
style 0-0 fg=bright-blue
style 2-44 dim
style 55-55 fg=bright-blue
4| "╰──────────────────────────────────────────────────────╯"
style 0-55 fg=bright-blue
5| "────╭ Coverage ────────────────────────────────────╮────"
style 0-3 dim
style 4-51 fg=bright-blue
style 52-55 dim
6| " │ Which advanced TUI states belong in the │ "
style 1-1 inverse
style 4-4 fg=bright-blue
style 6-50 bold
style 51-51 fg=bright-blue bold
7| "────│ required matrix? │────"
style 0-3 dim
style 4-4 fg=bright-blue
style 6-21 bold
style 51-51 fg=bright-blue
style 52-55 dim
8| "/wor│ │:com"
style 0-3 dim
style 4-4 fg=bright-blue
style 51-51 fg=bright-blue
style 52-55 dim
9| " │ [ ] Code Mode — run_code programs and capt │ "
style 4-4 fg=bright-blue
style 6-6 fg=bright-blue inverse
style 7-20 inverse
style 21-49 fg=bright-black inverse
style 51-51 fg=bright-blue
10| " │ [ ] Workflows — phases and parallel agents │ "
style 4-4 fg=bright-blue
style 21-49 fg=bright-black
style 51-51 fg=bright-blue
11| " │ [ ] Cordis tools — inspect, mount, and unm │ "
style 4-4 fg=bright-blue
style 24-49 fg=bright-black
style 51-51 fg=bright-blue
12| " │ 1/4 │ "
style 4-4 fg=bright-blue
style 6-8 dim
style 51-51 fg=bright-blue
13| " │ ↑↓ navigate • Space toggle • Enter submit • │ "
style 4-4 fg=bright-blue
style 6-49 dim
style 51-51 fg=bright-blue
14| " ╰──────────────────────────────────────────────╯ "
style 4-51 fg=bright-blue
15-19| <blank>
@@ -0,0 +1,45 @@
terminal 44x18 buffer=normal length=18 base=0 viewport=0
lifecycle started=1 stopped=0 progress=inactive
title "DSH snapshot"
cursor hidden column=1 viewportRow=12 bufferRow=12
buffer
0| "╭──────────────────────────────────────────╮"
style 0-43 fg=bright-blue
1| "│ DEEPSEEK HARNESS │"
style 0-0 fg=bright-blue
style 2-9 fg=bright-blue bold
style 11-17 bold
style 43-43 fg=bright-blue
2| "│ Snapshot agent ready. │"
style 0-0 fg=bright-blue
style 2-22 fg=bright-black
style 43-43 fg=bright-blue
3| "│ main • deepseek-v4-flash • │"
style 0-0 fg=bright-blue
style 2-42 dim
style 43-43 fg=bright-blue dim
4| "│ main-session │"
style 0-0 fg=bright-blue
style 2-13 dim
style 43-43 fg=bright-blue
5| "╰──────────────────────────────────────────╯"
style 0-43 fg=bright-blue
6| <blank>
7| " Context · compact "
style 1-17 dim
8| " Compacted summary: the prior command "
style 1-43 fg=bright-black
9| " completed and its details were retired "
style 1-43 fg=bright-black
10| " from the active surface. "
style 1-24 fg=bright-black
11| "────────────────────────────────────────────"
style 0-43 dim
12| " "
style 1-1 inverse
13| "────────────────────────────────────────────"
style 0-43 dim
14| "/workspace/project ↑0 ↓0 idle reasoning:o"
style 0-24 dim
style 27-43 dim
15-17| <blank>
@@ -0,0 +1,37 @@
terminal 104x30 buffer=normal length=30 base=0 viewport=0
lifecycle started=1 stopped=0 progress=inactive
title "DSH snapshot"
cursor hidden column=1 viewportRow=9 bufferRow=9
buffer
0| "╭──────────────────────────────────────────────────────────────────────────────────────────────────────╮"
style 0-103 fg=bright-blue
1| "│ DEEPSEEK HARNESS │"
style 0-0 fg=bright-blue
style 2-9 fg=bright-blue bold
style 11-17 bold
style 103-103 fg=bright-blue
2| "│ Snapshot agent ready. │"
style 0-0 fg=bright-blue
style 2-22 fg=bright-black
style 103-103 fg=bright-blue
3| "│ main • deepseek-v4-flash • main-session │"
style 0-0 fg=bright-blue
style 2-44 dim
style 103-103 fg=bright-blue
4| "╰──────────────────────────────────────────────────────────────────────────────────────────────────────╯"
style 0-103 fg=bright-blue
5| <blank>
6| " Context · compact "
style 1-17 dim
7| " Compacted summary: the prior command completed and its details were retired from the active surface. "
style 1-100 fg=bright-black
8| "────────────────────────────────────────────────────────────────────────────────────────────────────────"
style 0-103 dim
9| " "
style 1-1 inverse
10| "────────────────────────────────────────────────────────────────────────────────────────────────────────"
style 0-103 dim
11| "/workspace/project ↑0 ↓0 idle reasoning:on tools:compact"
style 0-24 dim
style 71-103 dim
12-29| <blank>
@@ -0,0 +1,67 @@
terminal 80x24 buffer=normal length=25 base=1 viewport=1
lifecycle started=1 stopped=0 progress=inactive
title "DSH snapshot"
cursor hidden column=1 viewportRow=21 bufferRow=22
buffer
0| "╭──────────────────────────────────────────────────────────────────────────────╮"
style 0-79 fg=bright-blue
1| "│ DEEPSEEK HARNESS │"
style 0-0 fg=bright-blue
style 2-9 fg=bright-blue bold
style 11-17 bold
style 79-79 fg=bright-blue
2| "│ Snapshot agent ready. │"
style 0-0 fg=bright-blue
style 2-22 fg=bright-black
style 79-79 fg=bright-blue
3| "│ main • deepseek-v4-flash • main-session │"
style 0-0 fg=bright-blue
style 2-44 dim
style 79-79 fg=bright-blue
4| "╰──────────────────────────────────────────────────────────────────────────────╯"
style 0-79 fg=bright-blue
5| <blank>
6| "▌ "
style 0-0 fg=bright-blue
7| "▌ You "
style 0-0 fg=bright-blue
style 2-4 fg=bright-blue bold
8| "▌ Old prompt with a long line that exercises wrapping before compaction. "
style 0-0 fg=bright-blue
9| "▌ "
style 0-0 fg=bright-blue
10| <blank>
11| "▌ "
style 0-0 fg=green
12| "▌ ✓ pnpm run test:coverage "
style 0-0 fg=green
style 2-2 fg=green bold
style 3-25 bold
13| "▌ Run the coverage gate "
style 0-0 fg=green
style 2-22 fg=bright-black
14| "▌ /workspace/project "
style 0-0 fg=green
style 2-19 dim
15| "▌ packages/ui/tui 100% "
style 0-0 fg=green
16| "▌ 4016 tests passed "
style 0-0 fg=green
17| "▌ 1 test skipped "
style 0-0 fg=green
18| "▌ coverage complete "
style 0-0 fg=green
19| "▌ [exit 0] "
style 0-0 fg=green
style 2-9 dim
20| "▌ "
style 0-0 fg=green
21| "────────────────────────────────────────────────────────────────────────────────"
style 0-79 dim
22| " "
style 1-1 inverse
23| "────────────────────────────────────────────────────────────────────────────────"
style 0-79 dim
24| "/workspace/project ↑0 ↓0 idle reasoning:on tools:compact"
style 0-24 dim
style 47-79 dim
+472
View File
@@ -0,0 +1,472 @@
import { mkdir, readdir, writeFile } from 'node:fs/promises'
import { dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { afterAll, describe, expect, it } from 'vitest'
import type { Context } from 'cordis'
import { CallId, type ContentBlock } from '@deepseek-ai/dsh-llm'
import type { Session } from '@deepseek-ai/dsh-session'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRegistry, { type ToolDefinition, type ToolResultView } from '@deepseek-ai/dsh-tools'
import * as ToolCordis from '@deepseek-ai/dsh-tool-cordis'
import * as ToolWorkflow from '@deepseek-ai/dsh-tool-workflow'
import {
appendAssistant,
appendUser,
createTuiTestHarness,
disposeTuiTestHarness,
type TuiHarness,
type TuiHarnessOptions,
} from './harness.ts'
import { HeadlessTerminal, type TerminalSnapshotOptions } from './headless-terminal.ts'
const SNAPSHOTS_DIR = join(dirname(fileURLToPath(import.meta.url)), 'snapshots')
const REFRESHING = process.env.DSH_SNAPSHOT === 'refresh'
const CHECKPOINTS = [
'conversation-replay',
'conversation-streaming',
'conversation-complete',
'code-mode-pending',
'code-mode-complete',
'dynamic-workflow-pending',
'dynamic-workflow-complete',
'cordis-tools-pending',
'cordis-tools-complete',
'advanced-cards-collapsed',
'advanced-cards-expanded',
'question-dialog',
'question-dialog-validation',
'surface-before-compaction',
'surface-after-compaction-narrow',
'surface-after-compaction-wide',
'errors-and-help',
'disposed-terminal',
] as const
type Checkpoint = typeof CHECKPOINTS[number]
type SnapshotHarness = TuiHarness<HeadlessTerminal, (code: number) => void>
const observedCheckpoints = new Set<Checkpoint>()
async function checkpoint(
name: Checkpoint,
terminal: HeadlessTerminal,
options: TerminalSnapshotOptions = {},
): Promise<void> {
observedCheckpoints.add(name)
expect(terminal.themeViolations(), `${name} must remain theme-agnostic`).toEqual([])
const snapshot = await terminal.snapshot(options)
const path = join(SNAPSHOTS_DIR, `${name}.golden.txt`)
if (REFRESHING) {
await mkdir(SNAPSHOTS_DIR, { recursive: true })
await writeFile(path, snapshot)
}
await expect(snapshot).toMatchFileSnapshot(path)
}
async function setupSnapshot(
options: TuiHarnessOptions = {},
size: { columns?: number; rows?: number } = {},
): Promise<SnapshotHarness> {
const terminal = new HeadlessTerminal(size.columns ?? 96, size.rows ?? 36)
const before = terminal.frames
const result = await createTuiTestHarness(terminal, () => {}, {
...options,
cwd: options.cwd === undefined ? '/workspace/project' : options.cwd,
config: Object.assign({
welcome: 'Snapshot agent ready.',
color: true,
title: 'DSH snapshot',
}, options.config),
})
await terminal.waitForFrame(before)
return result
}
async function renderAfter(harness: SnapshotHarness, action: () => void): Promise<void> {
const before = harness.terminal.frames
action()
await harness.terminal.waitForFrame(before)
}
async function disposeSnapshot(harness: SnapshotHarness): Promise<void> {
await disposeTuiTestHarness(harness)
await harness.terminal.dispose()
}
async function configureAdvancedTools(ctx: Context): Promise<void> {
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry, { mode: 'code' })
ctx.provide('workflows', {} as never)
await ctx.plugin(ToolWorkflow, { toolName: 'workflow', maxResultChars: 50_000 })
await ctx.plugin(ToolCordis, { vmTimeoutMs: 5_000 })
}
interface ToolCallFixture {
id: string
name: string
arguments: unknown
}
function appendToolCalls(session: Session, calls: readonly ToolCallFixture[]): void {
appendAssistant(session, calls.map(call => ({
type: 'tool-call',
id: CallId(call.id),
name: call.name,
arguments: JSON.stringify(call.arguments),
})))
for (const call of calls) {
session.append('tool/call', {
turn: 1,
step: 0,
callId: CallId(call.id),
name: call.name,
arguments: JSON.stringify(call.arguments),
})
}
}
function appendToolResult(
session: Session,
id: string,
content: ContentBlock[],
options: { isError?: boolean; meta?: unknown } = {},
): void {
session.append('tool/result', {
turn: 1,
step: 0,
callId: CallId(id),
content,
isError: options.isError ?? false,
...options.meta === undefined ? {} : { meta: options.meta },
}, { surfaceOp: 'append' })
}
function visualTool(
name: string,
call: NonNullable<ToolDefinition['presentCall']>,
result?: NonNullable<ToolDefinition['presentResult']>,
): ToolDefinition {
return {
name,
description: `${name} snapshot fixture`,
parameters: {},
execute: () => Promise.resolve([]),
presentCall: call,
...result === undefined ? {} : { presentResult: result },
}
}
const ADVANCED_CARD_TOOLS: Record<string, ToolDefinition> = {
bash: visualTool(
'bash',
() => ({ card: 'terminal', title: 'pnpm run test:coverage', description: 'Run the coverage gate', cwd: '/workspace/project' }),
() => ({ card: 'terminal', output: 'packages/ui/tui 100%\n4016 tests passed\n1 test skipped\ncoverage complete', exitCode: 0 }),
),
edit: visualTool(
'edit',
() => ({ card: 'diff', title: 'Edit renderer', diffs: [{ path: 'src/view.ts', oldText: 'old line', newText: 'new line' }] }),
(): ToolResultView => ({
card: 'diff',
diffs: [
{ path: 'src/view.ts', oldText: 'old line\nkeep', newText: 'new line\nkeep' },
{ path: 'tests/view.spec.ts', oldText: null, newText: 'expect(screen).toMatchSnapshot()' },
],
}),
),
subagent: visualTool('subagent', args => ({
card: 'generic',
title: 'Delegate renderer audit',
rawInput: (args as { prompt: string }).prompt,
})),
task_output: visualTool('task_output', args => ({
card: 'generic',
kind: 'read',
title: `Read output from background task ${(args as { task_id: string }).task_id}`,
rawInput: (args as { task_id: string }).task_id,
})),
skill: visualTool('skill', args => ({
card: 'generic',
kind: 'read',
title: `Load skill ${(args as { name: string }).name}`,
rawInput: (args as { name: string }).name,
})),
}
describe('TUI terminal-state snapshots', () => {
it('pins resumed conversation, streaming, completion, plans, tokens, and Markdown', async () => {
const harness = await setupSnapshot({
beforeMount(session) {
appendUser(session, 'Explain **snapshot fidelity** with `cells`.')
appendAssistant(session, [
{ type: 'reasoning', text: 'Compare the terminal state, not write fragments.' },
{ type: 'text', text: '## Result\n\n- final viewport\n- semantic styles\n\n> deterministic and reviewable' },
], { inputTokens: 12_500, outputTokens: 640 })
session.append('todo/write', {
todos: [
{ content: 'model the terminal', status: 'completed' },
{ content: 'capture advanced states', status: 'in_progress' },
{ content: 'verify PTY cleanup', status: 'pending' },
],
})
},
})
await checkpoint('conversation-replay', harness.terminal)
await renderAfter(harness, () => {
appendUser(harness.session, 'Show the live update.')
harness.session.append('assistant/chunk', {
turn: 2,
step: 0,
chunk: { type: 'block-start', index: 0, blockType: 'reasoning' },
})
harness.session.append('assistant/chunk', {
turn: 2,
step: 0,
chunk: { type: 'reasoning-delta', index: 0, text: 'Inspecting width and styles.' },
})
harness.session.append('assistant/chunk', {
turn: 2,
step: 0,
chunk: { type: 'block-start', index: 1, blockType: 'text' },
})
harness.session.append('assistant/chunk', {
turn: 2,
step: 0,
chunk: { type: 'text-delta', index: 1, text: 'Streaming **visible state**…' },
})
})
await checkpoint('conversation-streaming', harness.terminal)
await renderAfter(harness, () => {
appendAssistant(harness.session, [
{ type: 'reasoning', text: 'Inspecting width and styles.' },
{ type: 'text', text: 'Streaming **visible state** is complete.' },
], { inputTokens: 800, outputTokens: 120 })
harness.session.append('turn/end', { turn: 2, reason: { kind: 'max-tokens' } })
})
await checkpoint('conversation-complete', harness.terminal)
await disposeSnapshot(harness)
})
it('pins Code Mode run_code with its production presenter', async () => {
const harness = await setupSnapshot({ configureContext: configureAdvancedTools })
const call = {
id: 'code-1',
name: 'run_code',
arguments: {
code: "const first = await tools.bash({ command: 'echo CODE_ONE' })\nconst second = await tools.bash({ command: 'echo CODE_TWO' })\nconsole.log(first, second)\nreturn `${first}+${second}`",
},
}
await renderAfter(harness, () => { appendToolCalls(harness.session, [call]) })
await checkpoint('code-mode-pending', harness.terminal, { includeScrollback: true })
await renderAfter(harness, () => {
appendToolResult(harness.session, call.id, [{ type: 'text', text: 'CODE_ONE\n+CODE_TWO' }], {
meta: { logs: ['CODE_ONE', 'CODE_TWO', 'combined: CODE_ONE+CODE_TWO'] },
})
})
await checkpoint('code-mode-complete', harness.terminal, { includeScrollback: true })
await disposeSnapshot(harness)
})
it('pins a dynamic workflow with phases, parallel agents, and structured output', async () => {
const harness = await setupSnapshot({ configureContext: configureAdvancedTools })
const call = {
id: 'workflow-1',
name: 'workflow',
arguments: {
meta: {
name: 'tui-matrix',
description: 'Audit terminal states from independent angles',
phases: [
{ title: 'Inspect', detail: 'Map renderer branches' },
{ title: 'Verify', detail: 'Challenge missing states', provider: 'deepseek', model: 'deepseek-v4-flash' },
],
},
args: { packages: ['ui/tui', 'workflow/tool-workflow'] },
script: "phase('Inspect')\nconst reports = await parallel([\n () => agent('Audit layout', { label: 'layout', phase: 'Inspect' }),\n () => agent('Audit lifecycle', { label: 'lifecycle', phase: 'Inspect' }),\n])\nphase('Verify')\nreturn { reports, verdict: 'covered' }",
},
}
await renderAfter(harness, () => { appendToolCalls(harness.session, [call]) })
await checkpoint('dynamic-workflow-pending', harness.terminal, { includeScrollback: true })
await renderAfter(harness, () => {
appendToolResult(harness.session, call.id, [{
type: 'text',
text: 'workflow "tui-matrix" completed (2 agents).\nReturn value:\n{\n "reports": ["layout ok", "lifecycle ok"],\n "verdict": "covered"\n}',
}])
})
await checkpoint('dynamic-workflow-complete', harness.terminal, { includeScrollback: true })
await disposeSnapshot(harness)
})
it('pins cordis inspect, dynamic mount, and unmount cards with production presenters', async () => {
const harness = await setupSnapshot({ configureContext: configureAdvancedTools })
const calls = [
{ id: 'cordis-1', name: 'cordis_inspect', arguments: { what: 'tools' } },
{
id: 'cordis-2',
name: 'cordis_mount',
arguments: { code: "return { name: 'snapshot-marker', apply(ctx) { ctx.provide('snapshotMarker', { ready: true }) } }" },
},
{ id: 'cordis-3', name: 'cordis_unmount', arguments: { id: 'dyn-1' } },
]
await renderAfter(harness, () => { appendToolCalls(harness.session, calls) })
await checkpoint('cordis-tools-pending', harness.terminal, { includeScrollback: true })
await renderAfter(harness, () => {
appendToolResult(harness.session, 'cordis-1', [{ type: 'text', text: '## tools\nrun_code\nworkflow\ncordis_mount\ncordis_unmount' }])
appendToolResult(harness.session, 'cordis-2', [{ type: 'text', text: 'mounted dyn-1 (plugin "snapshot-marker", state: active)' }])
appendToolResult(harness.session, 'cordis-3', [{ type: 'text', text: 'unmounted dyn-1 (plugin "snapshot-marker")' }])
})
await checkpoint('cordis-tools-complete', harness.terminal, { includeScrollback: true })
await disposeSnapshot(harness)
})
it('pins terminal, diff, subagent, task, skill, collapsed, and expanded cards', async () => {
const harness = await setupSnapshot({
tools: ADVANCED_CARD_TOOLS,
config: { maxToolOutputLines: 3 },
}, { columns: 100, rows: 40 })
const calls = [
{ id: 'advanced-1', name: 'bash', arguments: { command: 'pnpm run test:coverage' } },
{ id: 'advanced-2', name: 'edit', arguments: { file_path: 'src/view.ts' } },
{ id: 'advanced-3', name: 'subagent', arguments: { prompt: 'Review renderer ownership and report only gaps.' } },
{ id: 'advanced-4', name: 'task_output', arguments: { task_id: 'subagent-7', wait: true } },
{ id: 'advanced-5', name: 'skill', arguments: { name: 'dsh-code-review' } },
]
await renderAfter(harness, () => {
appendToolCalls(harness.session, calls)
appendToolResult(harness.session, 'advanced-1', [{ type: 'text', text: 'raw process output' }])
appendToolResult(harness.session, 'advanced-2', [{ type: 'text', text: 'edit complete' }])
appendToolResult(harness.session, 'advanced-3', [{ type: 'text', text: 'The renderer has explicit lifecycle ownership.' }])
appendToolResult(harness.session, 'advanced-4', [{ type: 'text', text: 'audit complete\n[status: completed]' }])
appendToolResult(harness.session, 'advanced-5', [{ type: 'text', text: 'Loaded review instructions.' }])
})
await checkpoint('advanced-cards-collapsed', harness.terminal, { includeScrollback: true })
await renderAfter(harness, () => { harness.terminal.send('\x0f') })
await checkpoint('advanced-cards-expanded', harness.terminal, { includeScrollback: true })
await disposeSnapshot(harness)
})
it('pins a constrained multi-select question and its validation state', async () => {
const harness = await setupSnapshot({
config: {
maxQuestionOptions: 3,
questionDialogWidth: 48,
questionDialogMaxHeight: 16,
},
}, { columns: 56, rows: 20 })
const controller = new AbortController()
const beforeQuestion = harness.terminal.frames
const answer = harness.ctx.userInteraction.ask({
questions: [{
id: 'coverage',
header: 'Coverage',
question: 'Which advanced TUI states belong in the required matrix?',
multiSelect: true,
options: [
{ label: 'Code Mode', description: 'run_code programs and captured output' },
{ label: 'Workflows', description: 'phases and parallel agents' },
{ label: 'Cordis tools', description: 'inspect, mount, and unmount' },
{ label: 'Compaction', description: 'surface replacement and reflow' },
],
}],
signal: controller.signal,
})
const rejected = expect(answer).rejects.toMatchObject({ code: 'ASK_ABORTED' })
await harness.terminal.waitForFrame(beforeQuestion)
await checkpoint('question-dialog', harness.terminal)
await renderAfter(harness, () => { harness.terminal.send('\r') })
await checkpoint('question-dialog-validation', harness.terminal)
controller.abort()
await rejected
await disposeSnapshot(harness)
})
it('pins compaction surface replacement and narrow-to-wide reflow', async () => {
let replacementStart = 0
let replacementEnd = 0
let replacementSources: number[] = []
const harness = await setupSnapshot({
tools: ADVANCED_CARD_TOOLS,
beforeMount(session) {
const user = session.append('user/message', {
content: [{ type: 'text', text: 'Old prompt with a long line that exercises wrapping before compaction.' }],
source: { kind: 'user' },
}, { surfaceOp: 'append' })
const assistant = session.append('assistant/message', {
turn: 1,
step: 0,
provenance: { provider: 'mock', model: 'deepseek-v4-flash' },
content: [{ type: 'tool-call', id: CallId('old-tool'), name: 'bash', arguments: '{}' }],
}, { surfaceOp: 'append' })
session.append('tool/call', { turn: 1, step: 0, callId: CallId('old-tool'), name: 'bash', arguments: '{}' })
const result = session.append('tool/result', {
turn: 1,
step: 0,
callId: CallId('old-tool'),
content: [{ type: 'text', text: 'obsolete output that must disappear' }],
isError: false,
}, { surfaceOp: 'append' })
replacementStart = user.seq
replacementEnd = result.seq
replacementSources = [user.seq, assistant.seq, result.seq]
},
}, { columns: 80, rows: 24 })
await checkpoint('surface-before-compaction', harness.terminal, { includeScrollback: true })
await renderAfter(harness, () => {
harness.session.append('context/message', {
content: [{ type: 'text', text: 'Compacted summary: the prior command completed and its details were retired from the active surface.' }],
source: { kind: 'plugin', plugin: 'compact' },
}, {
surfaceOp: { op: 'replace', start: replacementStart, end: replacementEnd },
sourceEventSeqs: replacementSources,
})
harness.terminal.resize(44, 18)
})
await checkpoint('surface-after-compaction-narrow', harness.terminal, { includeScrollback: true })
await renderAfter(harness, () => { harness.terminal.resize(104, 30) })
await checkpoint('surface-after-compaction-wide', harness.terminal, { includeScrollback: true })
await disposeSnapshot(harness)
})
it('pins help, unknown commands, live errors, turn failures, and terminal restoration', async () => {
const harness = await setupSnapshot({}, { columns: 92, rows: 32 })
await renderAfter(harness, () => {
harness.terminal.send('/help')
harness.terminal.send('\r')
harness.terminal.send('/unknown-advanced-command')
harness.terminal.send('\r')
harness.ctx.emit('agent/error', harness.agent, 3, 1, new Error('provider stream failed after partial output'))
harness.session.append('turn/end', {
turn: 3,
reason: { kind: 'error', step: 1, message: 'provider stream failed after partial output' },
})
harness.session.append('turn/end', {
turn: 4,
reason: { kind: 'interrupted' },
})
})
await checkpoint('errors-and-help', harness.terminal, { includeScrollback: true })
await harness.controller.dispose()
await harness.terminal.flush()
await checkpoint('disposed-terminal', harness.terminal, { includeScrollback: true })
await harness.ctx.fiber.dispose()
await harness.terminal.dispose()
})
})
afterAll(async () => {
expect([...observedCheckpoints].sort()).toEqual([...CHECKPOINTS].sort())
const files = (await readdir(SNAPSHOTS_DIR))
.filter(file => file.endsWith('.golden.txt'))
.sort()
expect(files).toEqual(CHECKPOINTS.map(name => `${name}.golden.txt`).sort())
})
+16 -85
View File
@@ -1,18 +1,23 @@
import { describe, expect, it, vi } from 'vitest'
import { Context } from 'cordis'
import type { Terminal } from '@earendil-works/pi-tui'
import AgentRegistry, { AgentId, type Agent, type AgentStatus } from '@deepseek-ai/dsh-agent'
import type { ContentBlock } from '@deepseek-ai/dsh-llm'
import SessionStore, { SessionId, type Session } from '@deepseek-ai/dsh-session'
import AgentRegistry, { AgentId, type Agent } from '@deepseek-ai/dsh-agent'
import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
import type { ToolDefinition } from '@deepseek-ai/dsh-tools'
import UserInteractionService from '@deepseek-ai/dsh-user-interaction'
import {
createTuiChat,
mountTui,
resolveTuiConfig,
type Config,
type TuiRuntime,
} from '../src/index.ts'
import {
appendAssistant,
appendUser,
createTuiTestHarness,
disposeTuiTestHarness,
type TuiHarnessOptions,
} from './harness.ts'
class FakeTerminal implements Terminal {
columns = 88
@@ -84,97 +89,23 @@ class FakeTerminal implements Terminal {
}
}
interface FakeAgent extends Agent {
status: AgentStatus
sent: ContentBlock[][]
steered: ContentBlock[][]
cancelled: string[]
}
async function tick(): Promise<void> {
await new Promise(resolve => setTimeout(resolve, 25))
}
async function setup(options: {
status?: AgentStatus
config?: Config
tools?: Record<string, ToolDefinition>
beforeMount?: (session: Session) => void
cwd?: string | null
} = {}) {
const ctx = new Context()
await ctx.plugin(SessionStore)
await ctx.plugin(AgentRegistry)
await ctx.plugin(UserInteractionService)
const tools = options.tools ?? {}
ctx.provide('tools', {
get(name: string) {
return tools[name]
},
} as never)
const session = ctx.sessions.create(
SessionId('main-session'),
options.cwd === null ? undefined : { meta: { cwd: options.cwd ?? process.cwd() } },
)
options.beforeMount?.(session)
const sent: ContentBlock[][] = []
const steered: ContentBlock[][] = []
const cancelled: string[] = []
const agent: FakeAgent = {
id: AgentId('main'),
options: { model: 'deepseek-v4-flash' },
session,
status: options.status ?? 'idle',
ctx,
sent,
steered,
cancelled,
send(content) {
sent.push(content)
},
steer(content) {
steered.push(content)
},
inject() {},
cancel(reason) {
cancelled.push(reason ?? '')
},
whenIdle() {
return Promise.resolve()
},
}
ctx.agents.register(agent)
async function setup(options: TuiHarnessOptions = {}) {
const terminal = new FakeTerminal()
const exit = vi.fn()
const controller = createTuiChat(ctx, Object.assign({
welcome: 'Coding agent ready.',
agent: 'main',
color: false,
}, options.config), { terminal, exit })
const result = await createTuiTestHarness(terminal, exit, {
...options,
cwd: options.cwd === undefined ? process.cwd() : options.cwd,
})
await tick()
return { ctx, session, agent, terminal, exit, controller }
return result
}
async function dispose(setupResult: Awaited<ReturnType<typeof setup>>): Promise<void> {
await setupResult.controller.dispose()
await setupResult.ctx.fiber.dispose()
}
function appendUser(session: Session, text: string): void {
session.append('user/message', {
content: [{ type: 'text', text }],
source: { kind: 'user' },
}, { surfaceOp: 'append' })
}
function appendAssistant(session: Session, content: ContentBlock[], usage?: { inputTokens: number; outputTokens: number }): void {
session.append('assistant/message', {
turn: 1,
step: 0,
provenance: { provider: 'mock', model: 'deepseek-v4-flash' },
content,
...usage === undefined ? {} : { usage },
}, { surfaceOp: 'append' })
await disposeTuiTestHarness(setupResult)
}
describe('TUI config', () => {
+20
View File
@@ -2069,12 +2069,27 @@ importers:
'@deepseek-ai/dsh-session':
specifier: workspace:^
version: link:../../core/session
'@deepseek-ai/dsh-system-prompt':
specifier: workspace:^
version: link:../../core/system-prompt
'@deepseek-ai/dsh-tool-cordis':
specifier: workspace:^
version: link:../../cordis/tool-cordis
'@deepseek-ai/dsh-tool-workflow':
specifier: workspace:^
version: link:../../workflow/tool-workflow
'@deepseek-ai/dsh-tools':
specifier: workspace:^
version: link:../../core/tools
'@deepseek-ai/dsh-user-interaction':
specifier: workspace:^
version: link:../user-interaction
'@deepseek-ai/dsh-workflow':
specifier: workspace:^
version: link:../../workflow/workflow
'@xterm/headless':
specifier: 5.5.0
version: 5.5.0
cordis:
specifier: ^4.0.0-rc.6
version: 4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@vendor+loader)
@@ -4575,6 +4590,9 @@ packages:
resolution: {integrity: sha512-A9gOqLdi6cV4ibazAjcQufGj0B1y/vDqYrcuP6d/6x8P27gRS8643Dj9o1dEKtB6O7fwxb2FgBmJS2mX7gpvdw==}
engines: {node: '>=14.6'}
'@xterm/headless@5.5.0':
resolution: {integrity: sha512-5xXB7kdQlFBP82ViMJTwwEc3gKCLGKR/eoxQm4zge7GPBl86tCdI0IdPJjoKd8mUSFXz5V7i/25sfsEkP4j46g==}
accepts@2.0.0:
resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==}
engines: {node: '>= 0.6'}
@@ -8867,6 +8885,8 @@ snapshots:
'@xmldom/xmldom@0.9.10': {}
'@xterm/headless@5.5.0': {}
accepts@2.0.0:
dependencies:
mime-types: 3.0.2
+5 -1
View File
@@ -39,7 +39,11 @@ export default defineConfig({
// through the root tsconfig paths map; the native option cannot do this.
plugins: [tsconfigPaths({ projects: ['./tsconfig.json'] })],
test: {
include: ['examples/*/tests/**/*.snapshot.ts', 'packages/sdk/*/tests/**/*.snapshot.ts'],
include: [
'examples/*/tests/**/*.snapshot.ts',
'packages/sdk/*/tests/**/*.snapshot.ts',
'packages/ui/tui/tests/**/*.snapshot.ts',
],
// Each test boots a subprocess; give it room and keep the worker file singular. Replay tests
// opt into bounded in-file concurrency, while record/refresh stay serial because they write
// fixtures. The environment knob restores serial replay with value 1 on constrained machines.