From 1119c537d0aa52f720955e9209eb07fc2e02cd09 Mon Sep 17 00:00:00 2001 From: Turtle Date: Fri, 17 Jul 2026 12:01:37 +0800 Subject: [PATCH 01/13] feat(ui): add dedicated TUI package Move the interactive pi-tui front door into @deepseek-ai/dsh-tui while keeping @deepseek-ai/dsh-stdio line-oriented for pipes. Select the terminal package in the demo app, preserve logger ownership, and cover the production Loader composition with a PTY smoke test. --- AGENTS.md | 2 +- README.i18n.yaml | 4 +- README.md | 2 +- README.zh.md | 2 +- docs/architecture.md | 2 +- docs/config-catalog.md | 57 +- docs/cookbook/extension-cookbook.i18n.yaml | 4 +- docs/cookbook/extension-cookbook.md | 2 +- docs/cookbook/extension-cookbook.zh.md | 2 +- docs/core-data-structures/user-interaction.md | 2 +- docs/development.i18n.yaml | 4 +- docs/development.md | 2 +- docs/development.zh.md | 2 +- docs/event-producer-consumer.md | 10 +- docs/i18n/translation-prompt.md | 6 +- docs/module-graph.md | 10 +- examples/README.md | 2 +- examples/coding-agent/README.md | 20 +- examples/coding-agent/composition.md | 4 +- examples/coding-agent/cordis.yml | 11 +- .../tests/tui-keyless-smoke.e2e.ts | 101 ++ examples/cordis-agent/composition.md | 2 +- examples/echo-agent/README.md | 2 +- examples/echo-agent/composition.md | 2 +- examples/echo-agent/cordis.yml | 2 +- knip.json | 2 +- packages/examples/README.md | 4 +- packages/examples/agent-spine-demo/README.md | 2 +- packages/examples/stdio-demo/README.md | 24 +- packages/examples/stdio-demo/package.json | 8 +- packages/examples/stdio-demo/src/bin.ts | 2 +- packages/examples/stdio-demo/src/index.ts | 86 +- .../stdio-demo/tests/stdio-agent.spec.ts | 45 +- packages/examples/stdio-demo/tsconfig.json | 3 + packages/todo/README.md | 2 +- packages/todo/tool-todo/README.md | 2 +- packages/ui/README.md | 7 +- packages/ui/acp/README.md | 2 +- packages/ui/tui/README.md | 56 + packages/ui/tui/package.json | 45 + packages/ui/tui/src/index.ts | 1284 +++++++++++++++++ packages/ui/tui/tests/plugin-shape.spec.ts | 19 + packages/ui/tui/tests/tui.spec.ts | 952 ++++++++++++ packages/ui/tui/tsconfig.json | 33 + packages/ui/user-interaction/README.md | 2 +- pnpm-lock.yaml | 56 + scripts/gen-doc-graphs.ts | 4 +- tsconfig.build.json | 1 + tsconfig.json | 1 + 49 files changed, 2808 insertions(+), 93 deletions(-) create mode 100644 examples/coding-agent/tests/tui-keyless-smoke.e2e.ts create mode 100644 packages/ui/tui/README.md create mode 100644 packages/ui/tui/package.json create mode 100644 packages/ui/tui/src/index.ts create mode 100644 packages/ui/tui/tests/plugin-shape.spec.ts create mode 100644 packages/ui/tui/tests/tui.spec.ts create mode 100644 packages/ui/tui/tsconfig.json diff --git a/AGENTS.md b/AGENTS.md index 23d7c0263b..f695fbac3b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -26,7 +26,7 @@ packages/ Harness packages at packages///, all named @deepseek-ai cordis/ self-referential toolset: the agent inspects/mounts plugins in its own runtime hooks/ Claude Code / Codex hook bridges + shared wire-protocol library session-persistence/ persistence seam + JSONL/SQLite backends - ui/ ACP/stdio/JSON-RPC bridges; boot, approval, interaction plugins + ui/ ACP/stdio/TUI/JSON-RPC bridges; boot, approval, interaction plugins examples/ demo bundles (agent-spine + stdio/ACP/JSON-RPC bins) leaves load support/ dev/test infrastructure packages util/ zero-dependency utilities diff --git a/README.i18n.yaml b/README.i18n.yaml index 790812344d..628d7168da 100644 --- a/README.i18n.yaml +++ b/README.i18n.yaml @@ -2,5 +2,5 @@ # 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 -README.md: 53dd3896eb15800125673e7c44f7de02daca9376 -README.zh.md: ab826f62658248249ec18c57b35c0065c0f909d1 +README.md: 92cbfaa8196b49d2b801776f0e0c178cb5f673dc +README.zh.md: 28604ad1e2926edd7d4eb91c8027785433a54d32 diff --git a/README.md b/README.md index 53dd3896eb..92cbfaa819 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ This monorepo is built on the [Cordis](https://github.com/cordiverse/cordis) fra ```sh pnpm install pnpm run test # vitest -pnpm run demo:repl # REPL agent demo (needs DEEPSEEK_API_KEY) +pnpm run demo:repl # interactive pi-tui coding agent (needs DEEPSEEK_API_KEY) pnpm run demo:acp # ACP server agent demo (needs DEEPSEEK_API_KEY) ``` diff --git a/README.zh.md b/README.zh.md index ab826f6265..28604ad1e2 100644 --- a/README.zh.md +++ b/README.zh.md @@ -11,7 +11,7 @@ ```sh pnpm install pnpm run test # vitest -pnpm run demo:repl # REPL agent demo (needs DEEPSEEK_API_KEY) +pnpm run demo:repl # interactive pi-tui coding agent (needs DEEPSEEK_API_KEY) pnpm run demo:acp # ACP server agent demo (needs DEEPSEEK_API_KEY) ``` diff --git a/docs/architecture.md b/docs/architecture.md index 848980a45a..0f6de3af03 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -139,7 +139,7 @@ Some seams bend the template deliberately. LLM keeps interface and consumer voca ### Bundles And Apps -`dsh-agent-spine-demo` is the default composition bundle: one plugin loading the shared spine ([README](../packages/examples/agent-spine-demo/README.md)). App packages compose it with a front door and boot `bin`: `dsh-stdio-demo` for terminal REPL, and `dsh-acp-demo` for ACP over JSON-RPC stdio with no stdout logger ([ui/](../packages/ui/README.md)). `dsh-jsonrpc-agent` instead boots an external `cordis.yml`; the Python SDK injects the package default only when no explicit config channel is set and drives `dsh-jsonrpc` over line-delimited stdio JSON-RPC ([Python SDK](../python/README.md)). A deployment is a thin `cordis.yml` leaf: swappable backends, one app entry, and optional product tools ([examples/](../examples/AGENTS.md), [runnable wirings](cookbook/extension-cookbook.md#runnable-wirings), [graph atlas](graph-atlas.md)). +`dsh-agent-spine-demo` is the default composition bundle: one plugin loading the shared spine ([README](../packages/examples/agent-spine-demo/README.md)). App packages compose it with a front door and boot `bin`: `dsh-stdio-demo` selects the `dsh-tui` package on interactive terminals and `dsh-stdio` on pipes, while `dsh-acp-demo` serves ACP over JSON-RPC stdio ([ui/](../packages/ui/README.md)). `dsh-jsonrpc-agent` instead boots an external `cordis.yml`; the Python SDK injects the package default only when no explicit config channel is set and drives `dsh-jsonrpc` over line-delimited stdio JSON-RPC ([Python SDK](../python/README.md)). A deployment is a thin `cordis.yml` leaf: swappable backends, one app entry, and optional product tools ([examples/](../examples/AGENTS.md), [runnable wirings](cookbook/extension-cookbook.md#runnable-wirings), [graph atlas](graph-atlas.md)). ### Where New Behavior Goes diff --git a/docs/config-catalog.md b/docs/config-catalog.md index 91fe7c6497..79aa6ce17a 100644 --- a/docs/config-catalog.md +++ b/docs/config-catalog.md @@ -700,7 +700,7 @@ Source: [`packages/ui/stdio/src/index.ts:30`](../packages/ui/stdio/src/index.ts) * is the explicit model-facing tool order (forwarded to the system-prompt plugin); * fresh sessions use `process.cwd()` as their workspace cwd; resumed sessions * keep their persisted cwd. `persistenceRoot` is the JSONL backend's directory; - * `welcome` is the UI banner. + * `welcome` is the UI banner and `ui` configures terminal mode/presentation. */ export interface Config { /** Model name for the `main` agent (must have a registered adapter). */ @@ -713,8 +713,10 @@ export interface Config { tools?: ToolsConfig /** Directory the JSONL session backend writes under. Defaults to `./.sessions`. */ persistenceRoot?: string - /** stdin-chat banner printed once on start. Defaults to `'ready.'`. */ + /** Terminal banner printed once on start. Defaults to `'ready.'`. */ welcome?: string + /** Terminal front-door selection and pi-tui presentation settings. */ + ui?: UiConfig /** Skill registry, local-provider, and model-facing consumer config forwarded to agent-spine-demo. */ skills?: agentCore.SkillConfig /** Model-facing bash tool config forwarded through agent-core. */ @@ -728,11 +730,22 @@ export interface Config { */ resumeSessionId?: string } + +/** App-level terminal selection with nested TUI presentation settings. */ +export interface UiConfig { + /** Select a concrete front door or infer it from the process streams. */ + mode?: TerminalMode + /** Settings forwarded only when the pi-tui front door is selected. */ + tui?: uiTui.TuiConfig +} + +/** Terminal front door selected by the app bundle. */ +export type TerminalMode = 'auto' | 'readline' | 'tui' ``` -Depends on: [`agentCore`](../packages/examples/agent-spine-demo/src/index.ts) · [`ToolsConfig`](#deepseek-aidsh-tools) +Depends on: [`agentCore`](../packages/examples/agent-spine-demo/src/index.ts) · [`ToolsConfig`](#deepseek-aidsh-tools) · [`uiTui`](../packages/ui/tui/src/index.ts) -Source: [`packages/examples/stdio-demo/src/index.ts:36`](../packages/examples/stdio-demo/src/index.ts) +Source: [`packages/examples/stdio-demo/src/index.ts:73`](../packages/examples/stdio-demo/src/index.ts) ## `@deepseek-ai/dsh-subagent-acp` @@ -1075,6 +1088,42 @@ export type ToolPresentationMode = 'native' | 'code' | 'both' Source: [`packages/core/tools/src/index.ts:307`](../packages/core/tools/src/index.ts) +## `@deepseek-ai/dsh-tui` + +Requires: `agents` · `userInteraction` · `tools` + +```ts config-catalog +/** Serializable plugin configuration. */ +export interface Config extends TuiConfig { + /** Header subtitle. Defaults to `ready.`. */ + welcome?: string + /** Agent id driven by this terminal. Defaults to `main`. */ + agent?: string +} + +/** Presentation settings for the pi-tui terminal mode. */ +export interface TuiConfig { + /** Render model reasoning blocks. */ + showReasoning?: boolean + /** Maximum tool-output lines shown before the card is collapsed. */ + maxToolOutputLines?: number + /** Maximum options visible at once in a user-question dialog. */ + maxQuestionOptions?: number + /** User-question dialog width in terminal columns. */ + questionDialogWidth?: number + /** User-question dialog maximum height in terminal rows. */ + questionDialogMaxHeight?: number + /** Show the terminal's hardware cursor at the pi editor's IME marker. */ + showHardwareCursor?: boolean + /** Apply the built-in ANSI color palette. */ + color?: boolean + /** Terminal window title while the UI is mounted. */ + title?: string +} +``` + +Source: [`packages/ui/tui/src/index.ts:100`](../packages/ui/tui/src/index.ts) + ## `@deepseek-ai/dsh-user-approval` ```ts config-catalog diff --git a/docs/cookbook/extension-cookbook.i18n.yaml b/docs/cookbook/extension-cookbook.i18n.yaml index 207ea114e0..598e9827b1 100644 --- a/docs/cookbook/extension-cookbook.i18n.yaml +++ b/docs/cookbook/extension-cookbook.i18n.yaml @@ -2,5 +2,5 @@ # 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 -extension-cookbook.md: 3474bc116b43f9be57b52e947f9cf99730f7e796 -extension-cookbook.zh.md: 1a605b20fe4e171a948ac2a046193a2f4d884e44 +extension-cookbook.md: 02dc22ff72807629a89d602f49752e6f429587b5 +extension-cookbook.zh.md: a204abdf6656312aea94640c60038af9f81f4f7b diff --git a/docs/cookbook/extension-cookbook.md b/docs/cookbook/extension-cookbook.md index 3474bc116b..02dc22ff72 100644 --- a/docs/cookbook/extension-cookbook.md +++ b/docs/cookbook/extension-cookbook.md @@ -87,7 +87,7 @@ export function apply(ctx: Context) { ## Runnable wirings -Three complete examples load their plugin trees from `cordis.yml`: [`examples/echo-agent`](../../examples/echo-agent) (mock model + echo tool — the all-mock skeleton check, `pnpm run demo:echo`), [`examples/coding-agent`](../../examples/coding-agent) (DeepSeek V4 + the bash tool suite behind a terminal REPL UI, `pnpm run demo:repl`), and [`examples/acp-agent`](../../examples/acp-agent) (an agent exposed as an ACP server over JSON-RPC stdio — the client-driver shape, `pnpm run demo:acp`). Each leaf is just its swappable backends plus an app-package entry: the stdio demos load [`@deepseek-ai/dsh-stdio-demo`](../../packages/examples/stdio-demo), the ACP demo loads [`@deepseek-ai/dsh-acp-demo`](../../packages/examples/acp-demo), and both app packages share the spine via the [`@deepseek-ai/dsh-agent-spine-demo`](../../packages/examples/agent-spine-demo) bundle. +Three complete examples load their plugin trees from `cordis.yml`: [`examples/echo-agent`](../../examples/echo-agent) (mock model + echo tool — the all-mock skeleton check, `pnpm run demo:echo`), [`examples/coding-agent`](../../examples/coding-agent) (DeepSeek V4 + the bash tool suite behind a pi-tui coding interface with a readline fallback, `pnpm run demo:repl`), and [`examples/acp-agent`](../../examples/acp-agent) (an agent exposed as an ACP server over JSON-RPC stdio — the client-driver shape, `pnpm run demo:acp`). Each leaf is just its swappable backends plus an app-package entry: the stdio demos load [`@deepseek-ai/dsh-stdio-demo`](../../packages/examples/stdio-demo), the ACP demo loads [`@deepseek-ai/dsh-acp-demo`](../../packages/examples/acp-demo), and both app packages share the spine via the [`@deepseek-ai/dsh-agent-spine-demo`](../../packages/examples/agent-spine-demo) bundle. ## The feature → mechanism map diff --git a/docs/cookbook/extension-cookbook.zh.md b/docs/cookbook/extension-cookbook.zh.md index 1a605b20fe..a204abdf66 100644 --- a/docs/cookbook/extension-cookbook.zh.md +++ b/docs/cookbook/extension-cookbook.zh.md @@ -87,7 +87,7 @@ export function apply(ctx: Context) { ## 可运行的组装示例 -三个完整示例从 `cordis.yml` 加载各自的插件树:[`examples/echo-agent`](../../examples/echo-agent)(mock 模型 + echo 工具——全 mock 骨架检查,`pnpm run demo:echo`)、[`examples/coding-agent`](../../examples/coding-agent)(DeepSeek V4 + bash 工具套件,配合终端 REPL UI,`pnpm run demo:repl`)、[`examples/acp-agent`](../../examples/acp-agent)(通过 JSON-RPC stdio 暴露为 ACP 服务器的 agent——客户端驱动形态,`pnpm run demo:acp`)。每个叶子只是其可替换后端加一个 app 包入口:stdio 演示加载 [`@deepseek-ai/dsh-stdio-demo`](../../packages/examples/stdio-demo),ACP 演示加载 [`@deepseek-ai/dsh-acp-demo`](../../packages/examples/acp-demo),两个 app 包通过 [`@deepseek-ai/dsh-agent-spine-demo`](../../packages/examples/agent-spine-demo) bundle 共享主干。 +三个完整示例从 `cordis.yml` 加载各自的插件树:[`examples/echo-agent`](../../examples/echo-agent)(mock 模型 + echo 工具——全 mock 骨架检查,`pnpm run demo:echo`)、[`examples/coding-agent`](../../examples/coding-agent)(DeepSeek V4 + bash 工具套件,配合 pi-tui 编码界面,并为管道输入保留 readline 回退,`pnpm run demo:repl`)、[`examples/acp-agent`](../../examples/acp-agent)(通过 JSON-RPC stdio 暴露为 ACP 服务器的 agent——客户端驱动形态,`pnpm run demo:acp`)。每个叶子只是其可替换后端加一个 app 包入口:stdio 演示加载 [`@deepseek-ai/dsh-stdio-demo`](../../packages/examples/stdio-demo),ACP 演示加载 [`@deepseek-ai/dsh-acp-demo`](../../packages/examples/acp-demo),两个 app 包通过 [`@deepseek-ai/dsh-agent-spine-demo`](../../packages/examples/agent-spine-demo) bundle 共享主干。 ## 功能→机制映射 diff --git a/docs/core-data-structures/user-interaction.md b/docs/core-data-structures/user-interaction.md index 4edc415039..afe61af475 100644 --- a/docs/core-data-structures/user-interaction.md +++ b/docs/core-data-structures/user-interaction.md @@ -1,6 +1,6 @@ # User Interaction -The user-interaction seam of [dsh-user-interaction](../../packages/ui/user-interaction). It is the provider-neutral vocabulary a tool or permission plugin uses when it needs the human to answer before the agent can continue. UI surfaces provide the active `UserInteractionProvider`: `dsh-stdio-demo` renders questions in readline, and `dsh-acp` maps them to ACP form elicitations. +The user-interaction seam of [dsh-user-interaction](../../packages/ui/user-interaction). It is the provider-neutral vocabulary a tool or permission plugin uses when it needs the human to answer before the agent can continue. UI surfaces provide the active `UserInteractionProvider`: `dsh-stdio-demo` selects keyboard-driven `dsh-tui` overlays or `dsh-stdio` readline prompts, and `dsh-acp` maps questions to ACP form elicitations. Source: [`packages/ui/user-interaction/src/index.ts`](../../packages/ui/user-interaction/src/index.ts) diff --git a/docs/development.i18n.yaml b/docs/development.i18n.yaml index 494eb09922..1d687a3f90 100644 --- a/docs/development.i18n.yaml +++ b/docs/development.i18n.yaml @@ -2,5 +2,5 @@ # 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 -development.md: b3c338f03f548b4de4b676850732731323d611f1 -development.zh.md: 20f5c585dd378a7b0a3bd6b0af8ebf7dc5e0fd3c +development.md: 512fdcd89b4d481dd2033bc4f4c2816a04b5feaa +development.zh.md: 9f2038a99b289d06f16d7d51f0d043caa34a300e diff --git a/docs/development.md b/docs/development.md index b3c338f03f..512fdcd89b 100644 --- a/docs/development.md +++ b/docs/development.md @@ -109,7 +109,7 @@ The echo demo does not need API credentials: pnpm run demo:echo ``` -The REPL agent demo uses the real DeepSeek adapter and needs `DEEPSEEK_API_KEY` in the environment or repo-root `.env`: +The coding-agent demo uses pi-tui interactively, falls back to readline for pipes, and needs `DEEPSEEK_API_KEY` in the environment or repo-root `.env`: ```sh pnpm run demo:repl diff --git a/docs/development.zh.md b/docs/development.zh.md index 20f5c585dd..9f2038a99b 100644 --- a/docs/development.zh.md +++ b/docs/development.zh.md @@ -109,7 +109,7 @@ echo 演示不需要 API 凭证: pnpm run demo:echo ``` -REPL agent 演示使用真实的 DeepSeek 适配器,需要环境变量或仓库根目录 `.env` 中的 `DEEPSEEK_API_KEY`: +coding-agent 演示在交互终端中使用 pi-tui,对管道输入回退到 readline,并需要环境变量或仓库根目录 `.env` 中的 `DEEPSEEK_API_KEY`: ```sh pnpm run demo:repl diff --git a/docs/event-producer-consumer.md b/docs/event-producer-consumer.md index d8c924b24d..b67788ea6b 100644 --- a/docs/event-producer-consumer.md +++ b/docs/event-producer-consumer.md @@ -7,16 +7,16 @@ This matrix shows which packages dispatch each harness-owned event and which pac | Event | Mode | Declared in | Dispatchers | Listeners | | --- | --- | --- | --- | --- | -| `agent/created` | `emit` | [`packages/core/agent/src/types.ts:139`](../packages/core/agent/src/types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`jsonrpc`](../packages/ui/jsonrpc), [`stdio`](../packages/ui/stdio) | -| `agent/disposed` | `emit` | [`packages/core/agent/src/types.ts:148`](../packages/core/agent/src/types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`stdio`](../packages/ui/stdio) | -| `agent/error` | `emit` | [`packages/core/agent/src/types.ts:283`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | - | +| `agent/created` | `emit` | [`packages/core/agent/src/types.ts:139`](../packages/core/agent/src/types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`jsonrpc`](../packages/ui/jsonrpc), [`stdio`](../packages/ui/stdio), [`tui`](../packages/ui/tui) | +| `agent/disposed` | `emit` | [`packages/core/agent/src/types.ts:148`](../packages/core/agent/src/types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`stdio`](../packages/ui/stdio), [`tui`](../packages/ui/tui) | +| `agent/error` | `emit` | [`packages/core/agent/src/types.ts:283`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`tui`](../packages/ui/tui) | | `agent/pre-step` | `serial` | [`packages/core/agent/src/types.ts:202`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`compact-basic`](../packages/compact/compact-basic), [`user-approval`](../packages/ui/user-approval) | | `agent/prompt-submit` | `waterfall` | [`packages/core/agent/src/types.ts:212`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`acp`](../packages/ui/acp), [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex), [`repeat-tool-guard`](../packages/guard/repeat-tool-guard) | | `agent/queued` | `emit` | [`packages/core/agent/src/types.ts:167`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | - | | `agent/request` | `waterfall` | [`packages/core/agent/src/types.ts:224`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | - | | `agent/session-prefix` | `waterfall` | [`packages/core/agent/src/types.ts:239`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`tool-skill`](../packages/skill/tool-skill) | | `agent/session-start` | `emit` | [`packages/core/agent/src/types.ts:180`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex) | -| `agent/status` | `emit` | [`packages/core/agent/src/types.ts:157`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`invariants`](../packages/support/invariants), [`repeat-tool-guard`](../packages/guard/repeat-tool-guard), [`stdio`](../packages/ui/stdio) | +| `agent/status` | `emit` | [`packages/core/agent/src/types.ts:157`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`invariants`](../packages/support/invariants), [`repeat-tool-guard`](../packages/guard/repeat-tool-guard), [`stdio`](../packages/ui/stdio), [`tui`](../packages/ui/tui) | | `agent/step-result` | `waterfall` | [`packages/core/agent/src/types.ts:250`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | - | | `agent/turn-continuation` | `waterfall` | [`packages/core/agent/src/types.ts:260`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex) | | `agent/turn-stop` | `serial` | [`packages/core/agent/src/types.ts:270`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`subagent-inprocess`](../packages/subagent/subagent-inprocess) | @@ -27,7 +27,7 @@ This matrix shows which packages dispatch each harness-owned event and which pac | `llm/stream` | `waterfall` | [`packages/llm/llm/src/index.ts:39`](../packages/llm/llm/src/index.ts) | [`llm`](../packages/llm/llm) (`waterfall`) | [`invariants`](../packages/support/invariants), [`llm-replay`](../packages/support/llm-replay) | | `session/created` | `emit` | [`packages/core/session/src/index.ts:47`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`invariants`](../packages/support/invariants), [`jsonrpc`](../packages/ui/jsonrpc), [`session-persistence`](../packages/session-persistence/session-persistence) | | `session/disposed` | `emit` | [`packages/core/session/src/index.ts:57`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | - | -| `session/event` | `emit` | [`packages/core/session/src/index.ts:69`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`acp`](../packages/ui/acp), [`invariants`](../packages/support/invariants), [`jsonrpc`](../packages/ui/jsonrpc), [`session-persistence`](../packages/session-persistence/session-persistence), [`stdio`](../packages/ui/stdio) | +| `session/event` | `emit` | [`packages/core/session/src/index.ts:69`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`acp`](../packages/ui/acp), [`invariants`](../packages/support/invariants), [`jsonrpc`](../packages/ui/jsonrpc), [`session-persistence`](../packages/session-persistence/session-persistence), [`stdio`](../packages/ui/stdio), [`tui`](../packages/ui/tui) | | `session/flush` | `parallel` | [`packages/core/session/src/index.ts:79`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`session-persistence`](../packages/session-persistence/session-persistence) | | `subagent/end` | `emit` | [`packages/subagent/subagent/src/index.ts:108`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude`](../packages/hooks/hooks-claude), [`jsonrpc`](../packages/ui/jsonrpc) | | `subagent/provider-added` | `emit` | [`packages/subagent/subagent/src/index.ts:82`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`emit`) | [`tool-subagent`](../packages/subagent/tool-subagent) | diff --git a/docs/i18n/translation-prompt.md b/docs/i18n/translation-prompt.md index 8bc15d6e8e..4dd877bea4 100644 --- a/docs/i18n/translation-prompt.md +++ b/docs/i18n/translation-prompt.md @@ -123,9 +123,9 @@ Follow the Good versions; these sentence-level examples illustrate error categor - Good: `A green gate does not mean the translation is correct.` ### Code block comments — never translate -- Source code block contains: `# REPL agent demo (needs DEEPSEEK_API_KEY)` -- Bad: `# REPL agent 演示(需要 DEEPSEEK_API_KEY)` -- Good: `# REPL agent demo (needs DEEPSEEK_API_KEY)` (byte-identical) +- Source code block contains: `# interactive pi-tui coding agent (needs DEEPSEEK_API_KEY)` +- Bad: `# 交互式 pi-tui 编码 agent(需要 DEEPSEEK_API_KEY)` +- Good: `# interactive pi-tui coding agent (needs DEEPSEEK_API_KEY)` (byte-identical) ### Language switcher — English to Chinese - Source: `English | [中文](README.zh.md)` diff --git a/docs/module-graph.md b/docs/module-graph.md index 82763bdc55..e5d56709a8 100644 --- a/docs/module-graph.md +++ b/docs/module-graph.md @@ -98,6 +98,7 @@ flowchart TD pkg_permission["permission"] pkg_stdio["stdio"] pkg_tool_ask_user["tool-ask-user"] + pkg_tui["tui"] pkg_user_approval["user-approval"] pkg_user_interaction["user-interaction"] end @@ -287,6 +288,11 @@ flowchart TD pkg_tool_ask_user --> pkg_agent pkg_tool_ask_user --> pkg_tools pkg_tool_ask_user --> pkg_user_interaction + pkg_tui --> pkg_agent + pkg_tui --> pkg_llm + pkg_tui --> pkg_session + pkg_tui --> pkg_tools + pkg_tui --> pkg_user_interaction pkg_repeat_tool_guard --> pkg_agent pkg_repeat_tool_guard --> pkg_tools pkg_mcp_client --> pkg_llm @@ -370,6 +376,7 @@ flowchart TD pkg_stdio_demo --> pkg_stdio pkg_stdio_demo --> pkg_tool_ask_user pkg_stdio_demo --> pkg_tools + pkg_stdio_demo --> pkg_tui pkg_stdio_demo --> pkg_user_interaction ``` @@ -437,6 +444,7 @@ flowchart TD | [`hooks-codex`](../packages/hooks/hooks-codex) | `hooks` | [`agent`](../packages/core/agent), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`tools`](../packages/core/tools) | | [`acp`](../packages/ui/acp) | `ui` | [`agent`](../packages/core/agent), [`bash`](../packages/bash/bash), [`llm`](../packages/llm/llm), [`permission`](../packages/ui/permission), [`sandbox`](../packages/sandbox/sandbox), [`session`](../packages/core/session), [`session-persistence`](../packages/session-persistence/session-persistence), [`tools`](../packages/core/tools), [`user-approval`](../packages/ui/user-approval), [`user-interaction`](../packages/ui/user-interaction) | | [`tool-ask-user`](../packages/ui/tool-ask-user) | `ui` | [`agent`](../packages/core/agent), [`tools`](../packages/core/tools), [`user-interaction`](../packages/ui/user-interaction) | +| [`tui`](../packages/ui/tui) | `ui` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`tools`](../packages/core/tools), [`user-interaction`](../packages/ui/user-interaction) | | [`repeat-tool-guard`](../packages/guard/repeat-tool-guard) | `guard` | [`agent`](../packages/core/agent), [`tools`](../packages/core/tools) | | [`mcp-client`](../packages/mcp/mcp-client) | `mcp` | [`llm`](../packages/llm/llm), [`tools`](../packages/core/tools) | | [`tool-tasks`](../packages/tasks/tool-tasks) | `tasks` | [`agent`](../packages/core/agent), [`system-prompt`](../packages/core/system-prompt), [`tasks`](../packages/tasks/tasks), [`tools`](../packages/core/tools) | @@ -452,4 +460,4 @@ flowchart TD | [`subagent-fork`](../packages/subagent/subagent-fork) | `subagent` | [`agent`](../packages/core/agent), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subagent-inprocess`](../packages/subagent/subagent-inprocess) | | [`subagent-spawn`](../packages/subagent/subagent-spawn) | `subagent` | [`subagent`](../packages/subagent/subagent), [`subagent-inprocess`](../packages/subagent/subagent-inprocess) | | [`acp-demo`](../packages/examples/acp-demo) | `examples` | [`acp`](../packages/ui/acp), [`agent-spine-demo`](../packages/examples/agent-spine-demo), [`app-boot`](../packages/ui/app-boot), [`session-persistence-jsonl`](../packages/session-persistence/session-persistence-jsonl), [`tools`](../packages/core/tools), [`user-interaction`](../packages/ui/user-interaction) | -| [`stdio-demo`](../packages/examples/stdio-demo) | `examples` | [`agent`](../packages/core/agent), [`agent-spine-demo`](../packages/examples/agent-spine-demo), [`app-boot`](../packages/ui/app-boot), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence-jsonl`](../packages/session-persistence/session-persistence-jsonl), [`stdio`](../packages/ui/stdio), [`tool-ask-user`](../packages/ui/tool-ask-user), [`tools`](../packages/core/tools), [`user-interaction`](../packages/ui/user-interaction) | +| [`stdio-demo`](../packages/examples/stdio-demo) | `examples` | [`agent`](../packages/core/agent), [`agent-spine-demo`](../packages/examples/agent-spine-demo), [`app-boot`](../packages/ui/app-boot), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence-jsonl`](../packages/session-persistence/session-persistence-jsonl), [`stdio`](../packages/ui/stdio), [`tool-ask-user`](../packages/ui/tool-ask-user), [`tools`](../packages/core/tools), [`tui`](../packages/ui/tui), [`user-interaction`](../packages/ui/user-interaction) | diff --git a/examples/README.md b/examples/README.md index 5db1e18372..25c017824f 100644 --- a/examples/README.md +++ b/examples/README.md @@ -15,7 +15,7 @@ Run with: `pnpm run demo:echo`. When prompted, type "echo " to trigge ## coding-agent -A REPL agent demo: DeepSeek V4 + the `read`/`write`/`edit` filesystem tools + the bash tool suite, `subagent` delegation, and the `todo_write` task tracker on the same `@deepseek-ai/dsh-stdio-demo` app. The UI is a terminal readline REPL. +A coding-agent demo: DeepSeek V4 + the `read`/`write`/`edit` filesystem tools + the bash tool suite, `subagent` delegation, and the `todo_write` task tracker on the same `@deepseek-ai/dsh-stdio-demo` app. Interactive runs use the pi-tui coding interface; pipes use readline. Run with: `pnpm run demo:repl` (needs `DEEPSEEK_API_KEY` in the environment or a gitignored repo-root `.env`). See [coding-agent/README.md](coding-agent/README.md) for details. diff --git a/examples/coding-agent/README.md b/examples/coding-agent/README.md index 9e627324d4..c1c4747a22 100644 --- a/examples/coding-agent/README.md +++ b/examples/coding-agent/README.md @@ -1,6 +1,6 @@ # coding-agent -The REPL agent demo wiring: DeepSeek V4 + the `read`/`write`/`edit` filesystem tools + the bash tool suite + subagent delegation + `todo_write` + stdio chat + JSONL persistence, loaded from `cordis.yml`. The UI is a terminal readline REPL. +The coding-agent demo wiring: DeepSeek V4 + the `read`/`write`/`edit` filesystem tools + the bash tool suite + subagent delegation + `todo_write` + terminal chat + JSONL persistence, loaded from `cordis.yml`. Interactive runs use the pi-tui coding interface; piped runs use readline. ## Run it @@ -11,15 +11,9 @@ The REPL agent demo wiring: DeepSeek V4 + the `read`/`write`/`edit` filesystem t pnpm run demo:repl ``` -Type a coding task. The agent works through the `read`/`write`/`edit` filesystem tools for ordinary file operations and `bash` (+ the generic `task_output` / `task_list` / `task_kill` for background tasks) for shell commands, searches, and test runs, each in a fresh `bash -c` (the system prompt tells the model to pass `workdir` instead of `cd`). Both the fs tools and bash resolve relative paths against the session workspace. It can also delegate with `subagent`/`subagent_fork` and track multi-step work with `todo_write` (a whole-list task tracker rendered as a checklist). Reasoning streams dimmed; tool calls/results render inline. +Type a coding task. The agent works through the `read`/`write`/`edit` filesystem tools for ordinary file operations and `bash` (+ the generic `task_output` / `task_list` / `task_kill` for background tasks) for shell commands, searches, and test runs, each in a fresh `bash -c` (the system prompt tells the model to pass `workdir` instead of `cd`). Both the fs tools and bash resolve relative paths against the session workspace. It can also delegate with `subagent`/`subagent_fork` and track multi-step work with `todo_write`. -``` -> fix the failing test in /path/to/project -[main turn 1] (reasoning…) - [tool call] bash({"command": "node --test", "workdir": "/path/to/project"}) - [tool result] … [exit code: 1] - … -``` +The TUI renders resumed Markdown history, reasoning, tool-owned terminal/diff/generic cards, token totals, and the latest todo list. Enter submits or steers while the agent is running; Ctrl+O expands cards, Ctrl+R toggles reasoning, Escape cancels, and `/help` lists commands. `ask_user_question` opens a keyboard-driven overlay rather than taking over the editor. ### Resuming a prior session @@ -29,7 +23,7 @@ Each run starts a fresh session by default (its event log lands under `./.sessio RESUME_SESSION_ID= pnpm run demo:repl ``` -The id is wired through `cordis.yml` (`resumeSessionId: !!js process.env.RESUME_SESSION_ID`); unset, the agent starts a new session. A missing/unreadable id is non-fatal — it logs a warning and starts no `main` agent. +The id is wired through `cordis.yml` (`resumeSessionId: !!js process.env.RESUME_SESSION_ID`); unset, the agent starts a new session. A missing or unreadable id starts no `main` agent, so unset it or choose an existing session id. ## Code Mode @@ -48,17 +42,17 @@ and watch the transcript: one `run_code` call, a program looping over tools, and ## What each leaf entry demonstrates -This example is a thin leaf `cordis.yml`: it picks the swappable backends, loads one app package, and adds product tools that are intentionally outside the shared spine. The spine (sessions, system-prompt, tools, agents, invariants, `agent-loop`) and the front-door cluster (console logger, JSONL persistence, readline UI, the pre-created `main` agent) live inside the [`@deepseek-ai/dsh-stdio-demo`](../../packages/examples/stdio-demo) app and the [`@deepseek-ai/dsh-agent-spine-demo`](../../packages/examples/agent-spine-demo) bundle it loads; the leaf wires the backends and model-facing optional tools: +This example is a thin leaf `cordis.yml`: it picks the swappable backends, loads one app package, and adds product tools that are intentionally outside the shared spine. The spine (sessions, system-prompt, tools, agents, invariants, `agent-loop`) and the front-door cluster (JSONL persistence, TTY-selected `dsh-tui`/`dsh-stdio` channels, the pre-created `main` agent) live inside the [`@deepseek-ai/dsh-stdio-demo`](../../packages/examples/stdio-demo) app and the [`@deepseek-ai/dsh-agent-spine-demo`](../../packages/examples/agent-spine-demo) bundle it loads; the leaf wires the backends and model-facing optional tools: | Entry | Demonstrates | |---|---| | `hmr` (`@cordisjs/plugin-hmr`) | the dev/demo edit-reload loop — a **leaf** entry (not baked into the app) because it is Loader-only and needs `node --expose-internals`, which `demo:repl` passes | | `llm-deepseek` | real `LlmAdapter` via config (`!!js process.env.…` secrets); swap one line to `@deepseek-ai/dsh-llm-pi-ai` for the library-backed twin | | `bash` (`dsh-bash-local`) | the executor implementation — the swappable half of the bash seam. The model-facing `bash` schema (`tool-bash`) and generic `task_*` controls (`tool-tasks`) come from `dsh-agent-spine-demo`, so only the executor is a leaf choice | -| `stdio-agent` (`@deepseek-ai/dsh-stdio-demo`) | the app bundle: the agent-spine demo + console logger + JSONL persistence + readline UI + a pre-created `main` agent. Its config carries the model, system prompt, `persistenceRoot` (`./.sessions`), and `resumeSessionId` — so persistence and the agent are configured here, not wired as separate leaf plugins | +| `stdio-agent` (`@deepseek-ai/dsh-stdio-demo`) | the app bundle: the agent-spine demo + JSONL persistence + TTY-selected `dsh-tui`/`dsh-stdio` channels + a pre-created `main` agent. Its config carries the model, system prompt, `persistenceRoot` (`./.sessions`), `resumeSessionId`, and optional `ui` presentation settings | | `subagent`, `subagent-spawn`, `subagent-fork` | the subagent provider registry plus the two in-process backends: a fresh child and a child seeded with the parent's completed-turn prefix | | `tool-subagent`, `tool-subagent-fork` | two model-facing `dsh-tool-subagent` loads, each bound to a different provider and exposed under a distinct tool name (`subagent`, `subagent_fork`) | -| `tool-todo` | the model-facing `todo_write` tool; writes the whole task list to the session log and renders as a checklist in stdio | +| `tool-todo` | the model-facing `todo_write` tool; writes the whole task list to the session log and renders as a persistent TUI plan or readline checklist | | `fs-local`, `fs-policy`, `tool-fs` | the filesystem stack: the local `ctx.fs` provider, the read-before-write/edit policy gate (on the `fs/*` event gate), and the model-facing `read`/`write`/`edit` tools. Relative paths resolve against the session workspace | ## End-to-end tests (`pnpm run test:e2e`, key-gated) diff --git a/examples/coding-agent/composition.md b/examples/coding-agent/composition.md index 90ff8222b9..35835953ac 100644 --- a/examples/coding-agent/composition.md +++ b/examples/coding-agent/composition.md @@ -3,7 +3,7 @@ # Coding Agent App Composition -The coding REPL demo adds the real DeepSeek adapter, filesystem tools, todo_write, compaction, and both subagent transports on top of the stdio app package. +The coding-agent demo adds the real DeepSeek adapter, filesystem tools, todo_write, compaction, and both subagent transports on top of the stdio app package. ```mermaid flowchart LR @@ -18,7 +18,7 @@ flowchart LR cfg --> plugin_coding_stdio_agent plugin_coding_stdio_agent --> bundle_agent_core["@deepseek-ai/dsh-agent-spine-demo"] plugin_coding_stdio_agent --> bundle_jsonl["@deepseek-ai/dsh-session-persistence-jsonl"] - plugin_coding_stdio_agent --> frontdoor_stdio["readline UI
console logger
pre-created main agent"] + plugin_coding_stdio_agent --> frontdoor_stdio["dsh-tui (TTY) / dsh-stdio (pipes)
pre-created main agent"] bundle_agent_core --> spine_llm["ctx.llm"] bundle_agent_core --> spine_sessions["ctx.sessions"] bundle_agent_core --> spine_tools["ctx.tools + tool-bash"] diff --git a/examples/coding-agent/cordis.yml b/examples/coding-agent/cordis.yml index 61e389ebc5..568988bd59 100644 --- a/examples/coding-agent/cordis.yml +++ b/examples/coding-agent/cordis.yml @@ -1,6 +1,6 @@ -# REPL agent with swappable DeepSeek and local-bash backends. `dsh-stdio-demo` -# supplies the agent spine, generic task controls, logging, JSONL persistence, -# readline UI, and `main` agent. +# Coding agent with swappable DeepSeek and local-bash backends. `dsh-stdio-demo` +# supplies the agent spine, generic task controls, JSONL persistence, TTY-selected +# `dsh-tui`/`dsh-stdio` terminal front doors, and `main` agent. # HMR remains a leaf because it requires Loader internals; `demo:repl` passes # `--expose-internals`. The app bin loads the gitignored root `.env`; this file # reads `DEEPSEEK_API_KEY` and optional `DEEPSEEK_BASE_URL` through `!!js`. @@ -38,6 +38,11 @@ resumeSessionId: !!js process.env.RESUME_SESSION_ID persistenceRoot: './.sessions' welcome: 'agent REPL ready. Give it a coding task.' + ui: + mode: auto + tui: + showReasoning: true + maxToolOutputLines: 12 # Keep the persona to identity and behavior; tool plugins own tool guidance. # The loop resolves {{model}} from this agent's configuration. persona: | diff --git a/examples/coding-agent/tests/tui-keyless-smoke.e2e.ts b/examples/coding-agent/tests/tui-keyless-smoke.e2e.ts new file mode 100644 index 0000000000..a1057ba02f --- /dev/null +++ b/examples/coding-agent/tests/tui-keyless-smoke.e2e.ts @@ -0,0 +1,101 @@ +import { spawn } from 'node:child_process' +import { mkdtemp, rm } from 'node:fs/promises' +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' + +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 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 = sys.argv[1:] +env = os.environ.copy() +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, +}) +pid, fd = pty.fork() +if pid == 0: + os.chdir(cwd) + os.execvpe(node, [node, "--expose-internals", "--import", tsx_loader, bin_script, config_path], env) + +output = bytearray() +sent_exit = False +deadline = time.monotonic() + 25 +status = None +while time.monotonic() < deadline: + ready, _, _ = select.select([fd], [], [], 0.05) + if ready: + try: + chunk = os.read(fd, 65536) + except OSError as error: + if error.errno != errno.EIO: + raise + chunk = b"" + if chunk: + output.extend(chunk) + if 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) + if waited == pid: + status = candidate + break + +if status is None: + os.kill(pid, signal.SIGKILL) + _, status = os.waitpid(pid, 0) +sys.stdout.buffer.write(output) +if not sent_exit: + sys.stderr.write("TUI did not render its welcome marker before timeout\n") + sys.exit(124) +if not os.WIFEXITED(status) or os.WEXITSTATUS(status) != 0: + sys.stderr.write("TUI child did not exit cleanly\n") + sys.exit(125) +` + +async function runTuiLoaderSmoke(): Promise { + const cwd = await mkdtemp(join(tmpdir(), 'coding-tui-smoke-')) + try { + return await new Promise((resolve, reject) => { + const child = spawn('python3', [ + '-c', + PTY_DRIVER, + process.execPath, + tsxLoader, + binScript, + configPath, + tsconfigPath, + cwd, + ], { stdio: ['ignore', 'pipe', 'pipe'] }) + let stdout = '' + let stderr = '' + child.stdout.setEncoding('utf8') + child.stdout.on('data', (chunk: string) => { stdout += chunk }) + child.stderr.setEncoding('utf8') + child.stderr.on('data', (chunk: string) => { stderr += chunk }) + child.once('error', reject) + child.once('exit', (code) => { + if (code === 0) resolve(stdout) + else reject(new Error(`TUI PTY smoke exited ${String(code)}. stdout:\n${stdout}\nstderr:\n${stderr}`)) + }) + }) + } finally { + await rm(cwd, { recursive: true, force: true }) + } +} + +describe('coding-agent TUI keyless smoke (real Loader tree in a PTY)', () => { + it('boots pi-tui, renders the configured banner, accepts /exit, and restores the terminal', async () => { + const output = await runTuiLoaderSmoke() + expect(output).toContain('DEEPSEEK') + expect(output).toContain('agent REPL ready.') + }, LOADER_SMOKE_TEST_TIMEOUT_MS) +}) diff --git a/examples/cordis-agent/composition.md b/examples/cordis-agent/composition.md index 6a08c379fc..499379482f 100644 --- a/examples/cordis-agent/composition.md +++ b/examples/cordis-agent/composition.md @@ -24,7 +24,7 @@ flowchart LR cfg --> plugin_cordis_stdio_agent plugin_cordis_stdio_agent --> bundle_agent_core["@deepseek-ai/dsh-agent-spine-demo"] plugin_cordis_stdio_agent --> bundle_jsonl["@deepseek-ai/dsh-session-persistence-jsonl"] - plugin_cordis_stdio_agent --> frontdoor_stdio["readline UI
console logger
pre-created main agent"] + plugin_cordis_stdio_agent --> frontdoor_stdio["dsh-tui (TTY) / dsh-stdio (pipes)
pre-created main agent"] bundle_agent_core --> spine_llm["ctx.llm"] bundle_agent_core --> spine_sessions["ctx.sessions"] bundle_agent_core --> spine_tools["ctx.tools + tool-bash"] diff --git a/examples/echo-agent/README.md b/examples/echo-agent/README.md index d6156130db..183268f5da 100644 --- a/examples/echo-agent/README.md +++ b/examples/echo-agent/README.md @@ -4,7 +4,7 @@ Runnable demo: stdin chat with a scripted mock model and an echo tool. The all-m ## What it shows -This example is just a leaf `cordis.yml`: it loads the [`@deepseek-ai/dsh-stdio-demo`](../../packages/examples/stdio-demo) app (which bundles the whole [`@deepseek-ai/dsh-agent-spine-demo`](../../packages/examples/agent-spine-demo) spine, the console logger, JSONL persistence, the readline UI, and a pre-created `main` agent), and swaps in two example-local backends plus `hmr`: +This example is just a leaf `cordis.yml`: it loads the [`@deepseek-ai/dsh-stdio-demo`](../../packages/examples/stdio-demo) app (which bundles the whole [`@deepseek-ai/dsh-agent-spine-demo`](../../packages/examples/agent-spine-demo) spine, JSONL persistence, the TTY-selected `dsh-tui`/`dsh-stdio` front doors, and a pre-created `main` agent), and swaps in two example-local backends plus `hmr`: - `mock-llm.ts` — a mock `LlmAdapter` that streams scripted responses and calls the `echo` tool when the user types "echo ". Registered with `ctx.llm.registerAdapter(['mock-echo'], …)`. - `echo-tool.ts` — a tool registered via `ctx.tools.register(defineTool(…))` with typed `execute` args; echoes text back uppercased. diff --git a/examples/echo-agent/composition.md b/examples/echo-agent/composition.md index 5160bd20b7..bdc4d7e21d 100644 --- a/examples/echo-agent/composition.md +++ b/examples/echo-agent/composition.md @@ -20,7 +20,7 @@ flowchart LR cfg --> plugin_echo_stdio_agent plugin_echo_stdio_agent --> bundle_agent_core["@deepseek-ai/dsh-agent-spine-demo"] plugin_echo_stdio_agent --> bundle_jsonl["@deepseek-ai/dsh-session-persistence-jsonl"] - plugin_echo_stdio_agent --> frontdoor_stdio["readline UI
console logger
pre-created main agent"] + plugin_echo_stdio_agent --> frontdoor_stdio["dsh-tui (TTY) / dsh-stdio (pipes)
pre-created main agent"] bundle_agent_core --> spine_llm["ctx.llm"] bundle_agent_core --> spine_sessions["ctx.sessions"] bundle_agent_core --> spine_tools["ctx.tools + tool-bash"] diff --git a/examples/echo-agent/cordis.yml b/examples/echo-agent/cordis.yml index fbf998e770..516c53522e 100644 --- a/examples/echo-agent/cordis.yml +++ b/examples/echo-agent/cordis.yml @@ -22,7 +22,7 @@ - id: bash name: '@deepseek-ai/dsh-bash-local' -# The app pre-creates `main` on the mock model and supplies logging, persistence, and readline UI. +# The app pre-creates `main` on the mock model and supplies persistence plus TTY-selected `dsh-tui`/`dsh-stdio` front doors. - id: stdio-agent name: '@deepseek-ai/dsh-stdio-demo' config: diff --git a/knip.json b/knip.json index b978da3a76..502ded1657 100644 --- a/knip.json +++ b/knip.json @@ -1,7 +1,7 @@ { "$schema": "https://unpkg.com/knip@5/schema.json", "exclude": ["duplicates"], - "ignoreBinaries": ["bwrap", "sandbox-exec"], + "ignoreBinaries": ["bwrap", "python3", "sandbox-exec"], "ignoreWorkspaces": ["vendor/*", "python/sdk-runtime"], "workspaces": { ".": { diff --git a/packages/examples/README.md b/packages/examples/README.md index 5ee0206d0f..558bcb205c 100644 --- a/packages/examples/README.md +++ b/packages/examples/README.md @@ -5,11 +5,11 @@ Pre-composed plugin bundles a thin leaf `cordis.yml` loads instead of assembling | Package | npm name | Role | |---|---|---| | `agent-spine-demo/` | `@deepseek-ai/dsh-agent-spine-demo` | The executor-less/UI-less agent spine as one bundle plugin (`timer` + `llm` + sessions + system-prompt + tools + skills + agents + invariants + `tool-bash` + `tool-skill` + `agent-loop`) | -| `stdio-demo/` | `@deepseek-ai/dsh-stdio-demo` | Terminal stdio chat app: the spine + console logger + readline UI + a pre-created `main` agent, with a boot `bin` | +| `stdio-demo/` | `@deepseek-ai/dsh-stdio-demo` | Terminal chat app: the spine + JSONL persistence + TTY-selected `dsh-tui`/`dsh-stdio` front door + a pre-created `main` agent, with a boot `bin` | | `acp-demo/` | `@deepseek-ai/dsh-acp-demo` | ACP server app: the spine + JSONL persistence + the [`acp`](../ui/acp/README.md) bridge (no stdout logger), with a boot `bin` | | `jsonrpc-demo/` | `@deepseek-ai/dsh-jsonrpc-demo` | Bin-only runtime that boots an external `cordis.yml` for the stdio JSON-RPC SDK client | -`agent-spine-demo` is the shared bundle; `stdio-demo` and `acp-demo` compose it with opposite front-door clusters (console logger + readline UI vs the stdout-owning ACP bridge) and own their boot bins. `jsonrpc-demo` mounts no composition of its own — it boots whatever tree the deployment's `cordis.yml` names, and is what the Python SDK runtime launches. +`agent-spine-demo` is the shared bundle; `stdio-demo` and `acp-demo` compose it with terminal and ACP front-door clusters and own their boot bins. `jsonrpc-demo` mounts no composition of its own — it boots whatever tree the deployment's `cordis.yml` names, and is what the Python SDK runtime launches. These are **not** product API. The spine pieces they bundle live in [`core/`](../core/README.md), the bridges/channels/boot-glue in [`ui/`](../ui/README.md), and the swappable backends (LLM adapter, bash executor) in their capability groups; a demo bundle just picks one concrete composition of them. Swap or fork one freely. diff --git a/packages/examples/agent-spine-demo/README.md b/packages/examples/agent-spine-demo/README.md index eb56f9515c..a1f98d8167 100644 --- a/packages/examples/agent-spine-demo/README.md +++ b/packages/examples/agent-spine-demo/README.md @@ -33,7 +33,7 @@ The spine is everything COMMON to every front door. The swappable and front-door - **the LLM adapter** — the bundle ships the abstract `llm` service; the leaf registers a concrete adapter on `ctx.llm` (`llm-deepseek`, `llm-pi-ai`, `llm-replay`). - **the bash executor** — the bundle ships `tool-bash` (the consumer schema); the leaf provides `ctx.bash` (`bash-local` or a sandboxed impl). - **non-local skill providers** — the bundle ships the skill registry, the local filesystem provider, and the `skill` tool; deployments can add other providers such as embedded or remote catalogs as siblings. -- **presentation + per-app infra** — the stdio UI / ACP bridge, a console logger, `hmr`. These form the coupled "front-door cluster" that the app packages ([`dsh-stdio-demo`](../../examples/stdio-demo/README.md), [`dsh-acp-demo`](../../examples/acp-demo/README.md)) bake in. `timer` is in the spine (common to both, stdout-silent); a console logger is NOT (it writes to stdout, which the ACP bridge reserves for JSON-RPC). +- **presentation + per-app infra** — the terminal (`dsh-tui` / `dsh-stdio`) or ACP front door and `hmr`. These form the coupled front-door cluster that the app packages ([`dsh-stdio-demo`](../../examples/stdio-demo/README.md), [`dsh-acp-demo`](../../examples/acp-demo/README.md)) bake in. `timer` is in the spine because it is common and stdout-silent; front doors own stdout and remain outside. This is the [interface/implementation/consumer seam](../../../docs/rfc/implemented/architecture/2026-06-13-capability-seams.md) raised to the composition level: the bundle owns the shared spine, the leaf owns the backends, the app package owns the front door. diff --git a/packages/examples/stdio-demo/README.md b/packages/examples/stdio-demo/README.md index 98f639491e..96704998c4 100644 --- a/packages/examples/stdio-demo/README.md +++ b/packages/examples/stdio-demo/README.md @@ -1,8 +1,8 @@ # @deepseek-ai/dsh-stdio-demo -The **terminal stdio chat app**: a Cordis app plugin that composes the default agent spine ([`@deepseek-ai/dsh-agent-spine-demo`](../../examples/agent-spine-demo/README.md)) with the front-door cluster a terminal chat needs, and a `bin` that boots a leaf `cordis.yml`. +The **terminal chat app**: a Cordis app plugin that composes the default agent spine ([`@deepseek-ai/dsh-agent-spine-demo`](../../examples/agent-spine-demo/README.md)) with JSONL persistence, human interaction, a pre-created `main` agent, and a TTY-selected pi-tui/readline terminal front door. Its `bin` boots a leaf `cordis.yml`. -It is the readline counterpart to [`@deepseek-ai/dsh-acp-demo`](../acp-demo/README.md): both consume the same spine, but each bakes in the OPPOSITE front-door cluster. +It is the terminal counterpart to [`@deepseek-ai/dsh-acp-demo`](../acp-demo/README.md): both consume the same spine, while ACP reserves stdout for JSON-RPC and creates sessions from the client. ## What it bakes in @@ -10,14 +10,15 @@ A terminal chat always wants the same cluster, so the package owns it rather tha | Plugin | Why it is here | |---|---| -| `@cordisjs/plugin-logger-console` | the console logger — stdout is just the terminal here, so logging to it is correct (the ACP app must NOT have this) | | `@deepseek-ai/dsh-agent-spine-demo` | the spine, pre-creating a `main` agent from this app's `model` with `process.cwd()` as the fresh session cwd and carrying its `persona` | | `@deepseek-ai/dsh-session-persistence-jsonl` | durable JSONL session log under `persistenceRoot` | | `@deepseek-ai/dsh-user-interaction` | the human question/answer seam used by confirmation tools | | `@deepseek-ai/dsh-tool-ask-user` | the model-facing `ask_user_question` tool | -| `@deepseek-ai/dsh-stdio` | the readline UI, bound to the `main` agent | +| `@cordisjs/plugin-logger-console` | readline diagnostics for non-TTY operation; omitted from the fullscreen TUI path | +| `@deepseek-ai/dsh-stdio` | the line-oriented terminal channel, bound to `main` for pipes and automation | +| `@deepseek-ai/dsh-tui` | the interactive pi-tui channel, bound to `main` for TTY pairs | -`@cordisjs/plugin-hmr` (the dev/demo edit-reload loop) is deliberately a **leaf** entry, NOT baked in here: it is a Loader-only, subprocess-only dev plugin — its constructor throws without `node --expose-internals` + a live `loader`, and the in-process test tier cannot even import it (so a package whose `apply` statically pulled it in could never carry the per-file coverage gate). Unlike the console logger, a stray `hmr` is not a stdout-purity footgun, so leaving it at the leaf costs no safety. The `demo:echo` / `demo:repl` leaves load it and pass `--expose-internals`. +`@cordisjs/plugin-hmr` (the dev/demo edit-reload loop) is deliberately a **leaf** entry, not baked in here: it is a Loader-only, subprocess-only dev plugin whose constructor needs `node --expose-internals` plus a live `loader`. The `demo:echo` / `demo:repl` leaves load it and pass `--expose-internals`. The leaf `cordis.yml` supplies only the **swappable backends** — an LLM adapter (`llm-deepseek` for the real model, or the mock `mock-llm` for a demo) and a bash executor (`bash-local`) — `hmr`, plus this app's [`Config`](#config). The whole plugin tree a run loads is therefore: this app's cluster, the spine inside `agent-core`, `hmr`, and the two leaf backends. @@ -33,7 +34,8 @@ The leaf `cordis.yml` supplies only the **swappable backends** — an LLM adapte | `toolBash` | owner defaults | model-facing bash config routed through `dsh-agent-spine-demo`, including bash's producer-local `enableRunInBackground` | | `toolTasks` | owner defaults | generic `task_output` wait bounds routed through `dsh-agent-spine-demo` | | `persistenceRoot` | `./.sessions` | the JSONL backend's root directory | -| `welcome` | `ready.` | the stdin-chat banner | +| `welcome` | `ready.` | terminal banner / TUI subtitle | +| `ui` | owner defaults | app mode selection and nested `dsh-tui` presentation config | | `resumeSessionId` | — | resume a persisted session id instead of starting fresh (sourced from an env var in the leaf) | Fresh stdio sessions use the process launch directory as `session.header.cwd`, so project-scoped features such as skill discovery and default bash workdir follow the directory where `dsh-stdio-demo` was started. Resumed sessions keep the cwd stored in the persisted session header. @@ -45,7 +47,7 @@ Fresh stdio sessions use the process launch directory as `session.header.cwd`, s ## Example leaf `cordis.yml` ```yaml -# A REPL agent demo: hmr + the DeepSeek adapter + local bash, then this app. +# A coding-agent demo: hmr + the DeepSeek adapter + local bash, then this app. - id: hmr name: '@cordisjs/plugin-hmr' config: @@ -64,6 +66,8 @@ Fresh stdio sessions use the process launch directory as `session.header.cwd`, s config: model: deepseek-v4-flash persona: 'You are a coding assistant powered by the {{model}} model.' + ui: + mode: auto ``` Swap `llm-deepseek` for a `mock-llm` leaf plugin and you have the echo demo — "swap the backend, keep the app". @@ -72,9 +76,9 @@ Swap `llm-deepseek` for a `mock-llm` leaf plugin and you have the echo demo — ### Composed terminal agent request -**What the model sees**: Through `dsh-agent-spine-demo`, the `main` agent receives the harness identity, configured persona, skill catalog, and visible tools; this app also composes the generated [`ask_user_question` schema](../../../docs/tool-catalog.md#deepseek-aidsh-tool-ask-user). Each readline submission becomes a user message. +**What the model sees**: Through `dsh-agent-spine-demo`, the `main` agent receives the harness identity, configured persona, skill catalog, and visible tools; this app also composes the generated [`ask_user_question` schema](../../../docs/tool-catalog.md#deepseek-aidsh-tool-ask-user). Each terminal submission becomes a user message; submissions made while the agent runs steer the active turn. -**Token effect**: Child prompt and schema costs repeat per request; user input and tool history grow until compaction. The welcome banner, logger output, and rendered transcript are terminal-only and add zero model tokens. +**Token effect**: Child prompt and schema costs repeat per request; user input and tool history grow until compaction. The TUI/readline banners and rendered transcripts are terminal-only and add zero model tokens. ### Human-answer result @@ -84,6 +88,6 @@ Swap `llm-deepseek` for a `mock-llm` leaf plugin and you have the echo demo — ## Known Limitations and Deferred Work -- **One pre-created `main` agent drives the readline UI** — there is no multi-session or concurrent-agent surface in this app; a run is one conversation. +- **One pre-created `main` agent drives the terminal UI** — there is no multi-session or concurrent-agent surface in this app; a run is one conversation. - **The front-door cluster is fixed in code** — the JSONL persistence backend and the ask-user tooling are baked; a different composition is a leaf-level sibling entry or another app package. - **The question tool is not an approval answerer** — this app mounts `user-interaction` and `ask_user_question`, but not `ctx.approval`; a `tools/pre-execute` `ask` therefore fails closed unless the leaf composes an approval service and terminal answerer. diff --git a/packages/examples/stdio-demo/package.json b/packages/examples/stdio-demo/package.json index 3fe11f47a2..d20e210b8f 100644 --- a/packages/examples/stdio-demo/package.json +++ b/packages/examples/stdio-demo/package.json @@ -1,6 +1,6 @@ { "name": "@deepseek-ai/dsh-stdio-demo", - "description": "Terminal stdio chat app: the agent-spine-demo bundle + console logger + readline UI + a pre-created main agent, with a bin to boot a leaf cordis.yml", + "description": "Terminal chat app: agent spine + JSONL persistence + TTY pi-tui/readline front-door selection + pre-created main agent", "version": "0.0.1", "private": true, "type": "module", @@ -32,14 +32,15 @@ "peerDependencies": { "@cordisjs/plugin-include": "^1.0.4", "@cordisjs/plugin-loader": "^1.0.0-rc.5", - "@deepseek-ai/dsh-app-boot": "^0.0.1", "@cordisjs/plugin-logger-console": "^1.0.0", + "@deepseek-ai/dsh-app-boot": "^0.0.1", "@deepseek-ai/dsh-agent": "^0.0.1", "@deepseek-ai/dsh-llm": "^0.0.1", "@deepseek-ai/dsh-agent-spine-demo": "^0.0.1", "@deepseek-ai/dsh-session": "^0.0.1", "@deepseek-ai/dsh-session-persistence-jsonl": "^0.0.1", "@deepseek-ai/dsh-stdio": "^0.0.1", + "@deepseek-ai/dsh-tui": "^0.0.1", "@deepseek-ai/dsh-tool-ask-user": "^0.0.1", "@deepseek-ai/dsh-tools": "^0.0.1", "@deepseek-ai/dsh-user-interaction": "^0.0.1", @@ -49,8 +50,8 @@ "devDependencies": { "@cordisjs/plugin-include": "workspace:^", "@cordisjs/plugin-loader": "workspace:^", - "@deepseek-ai/dsh-app-boot": "workspace:^", "@cordisjs/plugin-logger-console": "workspace:^", + "@deepseek-ai/dsh-app-boot": "workspace:^", "@deepseek-ai/dsh-agent": "workspace:^", "@deepseek-ai/dsh-llm": "workspace:^", "@deepseek-ai/dsh-agent-spine-demo": "workspace:^", @@ -58,6 +59,7 @@ "@deepseek-ai/dsh-session": "workspace:^", "@deepseek-ai/dsh-session-persistence-jsonl": "workspace:^", "@deepseek-ai/dsh-stdio": "workspace:^", + "@deepseek-ai/dsh-tui": "workspace:^", "@deepseek-ai/dsh-tool-ask-user": "workspace:^", "@deepseek-ai/dsh-tools": "workspace:^", "@deepseek-ai/dsh-user-interaction": "workspace:^", diff --git a/packages/examples/stdio-demo/src/bin.ts b/packages/examples/stdio-demo/src/bin.ts index 462e821e50..278bb5faca 100644 --- a/packages/examples/stdio-demo/src/bin.ts +++ b/packages/examples/stdio-demo/src/bin.ts @@ -2,7 +2,7 @@ /** * Boot a stdio app from a leaf `cordis.yml`; usage is `dsh-stdio-demo [config]`, defaulting to the * cwd file. Shared `.env` loading, fail-loud Loader guards, and settled-tree boot live in - * dsh-app-boot. The echo and REPL demos invoke this bin with their own leaf configs. + * dsh-app-boot. The echo and coding-agent demos invoke this bin with their own leaf configs. * @module @deepseek-ai/dsh-stdio-demo/bin */ diff --git a/packages/examples/stdio-demo/src/index.ts b/packages/examples/stdio-demo/src/index.ts index 73f163d7f4..055e266498 100644 --- a/packages/examples/stdio-demo/src/index.ts +++ b/packages/examples/stdio-demo/src/index.ts @@ -1,8 +1,9 @@ /** * The stdio chat app: the default agent spine ({@link @deepseek-ai/dsh-agent-spine-demo}) plus the - * coupled front-door cluster a terminal chat needs — a console logger, the independently - * packaged readline UI, JSONL session persistence, the user-interaction seam with its - * `ask_user_question` tool, and a pre-created `main` agent the UI drives. + * coupled front-door cluster a terminal chat needs — the independently packaged + * pi-tui and readline front doors, JSONL session persistence, the user-interaction + * seam with its `ask_user_question` tool, and a pre-created `main` agent the UI + * drives. Interactive terminals use `dsh-tui`; pipes use `dsh-stdio` plus logging. * Swappable adapters, executors, optional tools, and HMR stay in the leaf. This * Loader plugin intentionally exposes named exports only; a default export * would hide its `Config` schema (see docs/postmortem/0001). @@ -20,9 +21,45 @@ import SessionPersistenceJsonl from '@deepseek-ai/dsh-session-persistence-jsonl' import UserInteractionService from '@deepseek-ai/dsh-user-interaction' import * as toolAskUser from '@deepseek-ai/dsh-tool-ask-user' import * as uiStdio from '@deepseek-ai/dsh-stdio' +import * as uiTui from '@deepseek-ai/dsh-tui' export const name = 'stdio-demo' +/** Terminal front door selected by the app bundle. */ +export type TerminalMode = 'auto' | 'readline' | 'tui' + +/** App-level terminal selection with nested TUI presentation settings. */ +export interface UiConfig { + /** Select a concrete front door or infer it from the process streams. */ + mode?: TerminalMode + /** Settings forwarded only when the pi-tui front door is selected. */ + tui?: uiTui.TuiConfig +} + +const terminalModeSchema = z.union(['auto', 'readline', 'tui'] as const).default('auto') + +/** Schemastery schema for the app-level terminal selection. */ +export const UiConfigSchema: z = z.object({ + mode: terminalModeSchema, + tui: uiTui.TuiConfigSchema, +}) + +/** + * Resolve the app's terminal front door. + * + * @param config - App-level terminal selection. + * @param isTTY - Whether both process streams are interactive TTYs. + * @returns The concrete UI package to mount. + */ +export function resolveTerminalMode(config: UiConfig | undefined, isTTY: boolean): Exclude { + const mode = config?.mode ?? 'auto' + if (mode === 'auto') return isTTY ? 'tui' : 'readline' + if (mode === 'tui' && !isTTY) { + throw new Error('stdio-demo: TUI mode requires both stdin and stdout to be TTYs; use mode "readline" for pipes') + } + return mode +} + /** * App config: the swappable per-demo values, each routed to where the app wires * it. `model`/`resumeSessionId` configure the pre-created `main` agent (through @@ -31,7 +68,7 @@ export const name = 'stdio-demo' * is the explicit model-facing tool order (forwarded to the system-prompt plugin); * fresh sessions use `process.cwd()` as their workspace cwd; resumed sessions * keep their persisted cwd. `persistenceRoot` is the JSONL backend's directory; - * `welcome` is the UI banner. + * `welcome` is the UI banner and `ui` configures terminal mode/presentation. */ export interface Config { /** Model name for the `main` agent (must have a registered adapter). */ @@ -44,8 +81,10 @@ export interface Config { tools?: ToolsConfig /** Directory the JSONL session backend writes under. Defaults to `./.sessions`. */ persistenceRoot?: string - /** stdin-chat banner printed once on start. Defaults to `'ready.'`. */ + /** Terminal banner printed once on start. Defaults to `'ready.'`. */ welcome?: string + /** Terminal front-door selection and pi-tui presentation settings. */ + ui?: UiConfig /** Skill registry, local-provider, and model-facing consumer config forwarded to agent-spine-demo. */ skills?: agentCore.SkillConfig /** Model-facing bash tool config forwarded through agent-core. */ @@ -72,6 +111,7 @@ export const Config: z = z.object({ // apply() fallbacks through named constants while retaining both boundaries. persistenceRoot: z.string().default('./.sessions'), welcome: z.string().default('ready.'), + ui: UiConfigSchema, skills: agentCore.SkillConfigSchema, toolBash: agentCore.ToolBashConfigSchema, toolTasks: agentCore.ToolTasksConfigSchema, @@ -79,14 +119,18 @@ export const Config: z = z.object({ }) /** - * Compose the spine with the stdio front door. The console logger comes first - * (infra), then the agent-spine-demo bundle pre-creating the `main` agent from this - * app's `model`/`resumeSessionId` with the deployment `persona`, then the JSONL - * backend, then the readline UI bound to `main`. The `hmr` dev-reload plugin is - * a leaf concern (see the module doc), so it is not mounted here. + * Compose the spine with one terminal front door. Interactive TTY pairs mount + * `dsh-tui` without a console exporter; pipes mount the readline `dsh-stdio` + * channel with the console logger. The `hmr` dev-reload plugin remains a leaf + * concern. + * + * @param ctx - Context receiving the app's child plugins. + * @param config - App configuration routed to the spine and front door. + * @param isTTY - Whether both terminal streams are interactive TTYs. */ -export function apply(ctx: Context, config: Config): void { - ctx.plugin(ConsoleExporter) +export function composeTerminalApp(ctx: Context, config: Config, isTTY: boolean): void { + const mode = resolveTerminalMode(config.ui, isTTY) + if (mode === 'readline') ctx.plugin(ConsoleExporter) ctx.plugin(agentCore, { ...config.persona !== undefined ? { persona: config.persona } : {}, ...config.toolOrder !== undefined ? { toolOrder: config.toolOrder } : {}, @@ -104,5 +148,21 @@ export function apply(ctx: Context, config: Config): void { ctx.plugin(SessionPersistenceJsonl, { root: config.persistenceRoot ?? './.sessions' }) ctx.plugin(UserInteractionService) ctx.plugin(toolAskUser) - ctx.plugin(uiStdio, { welcome: config.welcome ?? 'ready.', agent: 'main' }) + if (mode === 'tui') { + ctx.plugin(uiTui, { + ...config.ui?.tui, + welcome: config.welcome ?? 'ready.', + agent: 'main', + }) + } else { + ctx.plugin(uiStdio, { welcome: config.welcome ?? 'ready.', agent: 'main' }) + } } + +/** Compose the configured terminal front door with the agent app. */ +/* v8 ignore start -- production stream capability wiring; composeTerminalApp is unit-covered, + and the coding-agent PTY smoke covers the interactive process path */ +export function apply(ctx: Context, config: Config): void { + composeTerminalApp(ctx, config, process.stdin.isTTY && process.stdout.isTTY) +} +/* v8 ignore stop */ diff --git a/packages/examples/stdio-demo/tests/stdio-agent.spec.ts b/packages/examples/stdio-demo/tests/stdio-agent.spec.ts index b5fb0e0207..472afaa0b7 100644 --- a/packages/examples/stdio-demo/tests/stdio-agent.spec.ts +++ b/packages/examples/stdio-demo/tests/stdio-agent.spec.ts @@ -10,8 +10,8 @@ import { TOOL_ORDER_REST } from '@deepseek-ai/dsh-system-prompt' import * as stdioAgent from '../src/index.ts' /** - * Unit coverage for app composition and config forwarding: console logger, pre-created main agent, - * agent-core spine, JSONL backend, and readline UI. HMR is a Loader-only leaf concern covered by the + * Unit coverage for app composition and config forwarding: pre-created main agent, agent-core spine, + * JSONL backend, and adaptive terminal UI. HMR is a Loader-only leaf concern covered by the * keyless echo smoke; this tier pins the export shape because an inject-less app could otherwise * survive namespace collapse while silently losing its schema. */ @@ -65,6 +65,47 @@ async function withIsolatedSkillHomes(run: () => Promise): Promise { } describe('dsh-stdio-demo app', () => { + it('selects readline for pipes and dsh-tui for interactive terminal pairs', () => { + expect(stdioAgent.resolveTerminalMode(undefined, false)).toBe('readline') + expect(stdioAgent.resolveTerminalMode(undefined, true)).toBe('tui') + expect(stdioAgent.resolveTerminalMode({ mode: 'readline' }, true)).toBe('readline') + expect(stdioAgent.resolveTerminalMode({ mode: 'tui' }, true)).toBe('tui') + expect(() => stdioAgent.resolveTerminalMode({ mode: 'tui' }, false)).toThrow('requires both stdin and stdout') + }) + + it('composes only the selected terminal package and keeps TUI settings in dsh-tui', () => { + const calls: Array<{ name: string; config: unknown }> = [] + const ctx = { + plugin(plugin: { name?: string }, config?: unknown) { + calls.push({ name: plugin.name ?? '', config }) + }, + } as unknown as Context + + stdioAgent.composeTerminalApp(ctx, { + model: 'mock', + welcome: 'TUI ready', + ui: { mode: 'tui', tui: { color: false, maxToolOutputLines: 3 } }, + }, true) + expect(calls.map(call => call.name)).toContain('ui-tui') + expect(calls.map(call => call.name)).not.toContain('ui-stdio') + expect(calls.map(call => call.name)).not.toContain('ConsoleExporter') + expect(calls.find(call => call.name === 'ui-tui')?.config).toMatchObject({ + agent: 'main', welcome: 'TUI ready', color: false, maxToolOutputLines: 3, + }) + + calls.length = 0 + stdioAgent.composeTerminalApp(ctx, { model: 'mock', ui: { mode: 'tui' } }, true) + expect(calls.find(call => call.name === 'ui-tui')?.config).toMatchObject({ + agent: 'main', welcome: 'ready.', + }) + + calls.length = 0 + stdioAgent.composeTerminalApp(ctx, { model: 'mock', ui: { mode: 'readline' } }, false) + expect(calls.map(call => call.name)).toContain('ui-stdio') + expect(calls.map(call => call.name)).toContain('ConsoleExporter') + expect(calls.map(call => call.name)).not.toContain('ui-tui') + }) + it('composes the spine + front-door cluster and pre-creates the main agent', async () => { const ctx = await mount({ model: 'mock', persona: 'hi', persistenceRoot: '/tmp/dsh-stdio-demo-spec', skills: await isolatedSkillsConfig() }) // The spine services (brought up by the agent-core bundle) are all present. diff --git a/packages/examples/stdio-demo/tsconfig.json b/packages/examples/stdio-demo/tsconfig.json index bb360810a5..3145047c03 100644 --- a/packages/examples/stdio-demo/tsconfig.json +++ b/packages/examples/stdio-demo/tsconfig.json @@ -38,6 +38,9 @@ { "path": "../../ui/stdio" }, + { + "path": "../../ui/tui" + }, { "path": "../../ui/tool-ask-user" }, diff --git a/packages/todo/README.md b/packages/todo/README.md index b6a9ce2fc5..bfe5ec7503 100644 --- a/packages/todo/README.md +++ b/packages/todo/README.md @@ -6,4 +6,4 @@ The model-facing todo tool. A single **product** package — there is no interfa |---|---|---| | `tool-todo/` | Model-facing `todo_write` tool; writes the whole list to the session log (`todo/write`) | (registers on `ctx.tools`) | -The list lives on the event-sourced session log (`SessionEventMap['todo/write']`, owned by [`dsh-session`](../core/session)); this package is the thin consumer that appends the snapshot. UIs render off `session/event`: the [stdio app's readline UI](../examples/stdio-demo) prints the list, the [ACP bridge](../ui/acp) maps it to a `plan` sessionUpdate. +The list lives on the event-sourced session log (`SessionEventMap['todo/write']`, owned by [`dsh-session`](../core/session)); this package is the thin consumer that appends the snapshot. UIs render off `session/event`: the [terminal app](../examples/stdio-demo) shows a persistent TUI plan or readline checklist, while the [ACP bridge](../ui/acp) maps it to a `plan` sessionUpdate. diff --git a/packages/todo/tool-todo/README.md b/packages/todo/tool-todo/README.md index 0b07539f3d..f6097abe96 100644 --- a/packages/todo/tool-todo/README.md +++ b/packages/todo/tool-todo/README.md @@ -18,7 +18,7 @@ Beyond the schema's type/required/enum checks, `execute` rejects an empty or dup ## Rendering -The tool writes only the session event; it does not render. UIs subscribe to `session/event` and render the `todo/write` data themselves: the [stdio app's readline UI](../../examples/stdio-demo) prints a glyphed checklist, and the [ACP bridge](../../ui/acp) maps the list to a `plan` sessionUpdate (synthesizing the `priority` ACP requires). +The tool writes only the session event; it does not render. UIs subscribe to `session/event` and render the `todo/write` data themselves: the [terminal app](../../examples/stdio-demo) shows a persistent TUI plan or readline checklist, and the [ACP bridge](../../ui/acp) maps the list to a `plan` sessionUpdate (synthesizing the `priority` ACP requires). ## Export shape diff --git a/packages/ui/README.md b/packages/ui/README.md index 699a11bdb4..3dd26d80a8 100644 --- a/packages/ui/README.md +++ b/packages/ui/README.md @@ -9,12 +9,13 @@ Integrations that expose the agent to an external editor or client. These are ** | `permission/` | User-facing permission presets (`workspace-write`/`danger-full-access`): one product-level select bundling the sandbox-mode and approval-policy knobs, written through to their session events | `ctx.permission` | | `user-interaction/` | Abstract human question/answer seam used by UI-backed confirmation tools | `ctx.userInteraction` | | `tool-ask-user/` | Model-facing `ask_user_question` tool over `ctx.userInteraction` | (registers on `ctx.tools`) | -| `stdio/` | Terminal readline channel over `ctx.agents`, `session/event`, and `ctx.userInteraction`; agent lifecycle stays with app/developer code | (drives `ctx.agents`) | +| `stdio/` | Line-oriented terminal channel for pipes and automation; drives `ctx.agents`, renders `session/event`, and answers `ctx.userInteraction` | (drives `ctx.agents`) | +| `tui/` | Interactive pi-tui terminal channel for TTY sessions; renders `session/event`, tool presentation intents, and answers `ctx.userInteraction` | (drives `ctx.agents`) | | `jsonrpc/` | Stdio JSON-RPC server for out-of-process SDK clients | (drives `ctx.agents`) | | `app-boot/` | Shared boot glue for the app bins: `.env` loading, fail-loud Loader guards, snapshot-aware config resolution, the settle-the-tree boot sequence | (library for the bins) | -A UI integration is a client-driver plugin, not a loop change and not a capability seam: it consumes the existing `agent/*` event taxonomy and the `dsh-agent` factory. The `jsonrpc` plugin is the SDK-client sibling of the `acp` bridge (a JSON-RPC server over `ctx.agents` for out-of-process SDK clients rather than editors). The [`stdio`](stdio/README.md) plugin is the unstructured readline analogue of the `acp` bridge; app bundles and SDK projects compose it explicitly with the services and tools their product profile selects. +A UI integration is a client-driver plugin, not a loop change and not a capability seam: it consumes the existing `agent/*` event taxonomy and the `dsh-agent` factory. The `jsonrpc` plugin is the SDK-client sibling of the `acp` bridge (a JSON-RPC server over `ctx.agents` for out-of-process SDK clients rather than editors). The [`stdio`](stdio/README.md) and [`tui`](tui/README.md) plugins are the two terminal front doors: one is line-oriented for pipes, the other is interactive for TTYs. App bundles and SDK projects compose the appropriate channel explicitly with the services and tools their product profile selects. `user-approval`, `user-interaction`, and `tool-ask-user` live here because asking a human is a UI-backed product affordance, not part of the providerless core spine. `user-approval` owns the one-shot `ctx.approval` decision mechanism and its policy tier; answerers remain with their UI channel owners. `user-interaction` remains provider-neutral (`ctx.userInteraction`), while `tool-ask-user` is its model-facing consumer and the app/bridge packages provide concrete providers. -The runnable app bundles that bake these bridges into boot bins — the stdio chat app, the ACP server app, and the JSON-RPC SDK-runtime bin — live in [`examples/`](../examples/README.md) (`stdio-demo`, `acp-demo`, `jsonrpc-demo`), each composed over the [`agent-spine-demo`](../examples/agent-spine-demo/README.md) bundle. `ui/` keeps the reusable bridge/channel plugins and the `app-boot` glue; each front door owns its stdout policy, and a leaf `cordis.yml` supplies backends and optional tools. +The runnable app bundles that bake these bridges into boot bins — the terminal chat app, the ACP server app, and the JSON-RPC SDK-runtime bin — live in [`examples/`](../examples/README.md) (`stdio-demo`, `acp-demo`, `jsonrpc-demo`), each composed over the [`agent-spine-demo`](../examples/agent-spine-demo/README.md) bundle. `ui/` keeps the reusable bridge/channel plugins and the `app-boot` glue; each front door owns its stdout policy, and a leaf `cordis.yml` supplies backends and optional tools. diff --git a/packages/ui/acp/README.md b/packages/ui/acp/README.md index 3650cf27d4..27346efcec 100644 --- a/packages/ui/acp/README.md +++ b/packages/ui/acp/README.md @@ -2,7 +2,7 @@ Agent Client Protocol bridge over JSON-RPC stdio. Editors can create or resume agents, stream their events, answer questions and approvals, and render tool calls. One connection supports multiple isolated sessions; Zed is the primary compatibility target. -It is a **client-driver / UI plugin**, the structured analogue of the readline `stdio-chat` plugin — NOT a loop change and NOT a [capability seam](../../../docs/rfc/implemented/architecture/2026-06-13-capability-seams.md). It consumes the existing `agent/*` event taxonomy, the `dsh-agent` create/resume factory, and `dsh-session-persistence`. +It is a **client-driver / UI plugin**, the structured analogue of the terminal `dsh-tui`/`dsh-stdio` channels — NOT a loop change and NOT a [capability seam](../../../docs/rfc/implemented/architecture/2026-06-13-capability-seams.md). It consumes the existing `agent/*` event taxonomy, the `dsh-agent` create/resume factory, and `dsh-session-persistence`. ## Service / plugin diff --git a/packages/ui/tui/README.md b/packages/ui/tui/README.md new file mode 100644 index 0000000000..1e678beac8 --- /dev/null +++ b/packages/ui/tui/README.md @@ -0,0 +1,56 @@ +# @deepseek-ai/dsh-tui + +The interactive terminal front door for DeepSeek Harness agents, built on [`@earendil-works/pi-tui`](https://www.npmjs.com/package/@earendil-works/pi-tui). It requires stdin and stdout TTYs; scripts and Loader pipes should compose [`@deepseek-ai/dsh-stdio`](../stdio/README.md) instead. + +This package owns interactive terminal presentation and input only. It injects `agents`, `tools`, and `userInteraction`, then drives an agent created or resumed by app or developer code. Agent lifecycle, persistence, and the model-facing [`ask_user_question`](../tool-ask-user/README.md) tool remain separate composition entries. + +The TUI rebuilds resumed history from the active session surface, renders Markdown responses and reasoning, applies each tool's `presentCall` / `presentResult` intent to terminal, diff, or generic cards, keeps the latest `todo/write` plan above the editor, and presents `ctx.userInteraction` questions as keyboard-driven overlays. Surface replacement events rebuild the transcript so compacted history does not reappear. + +While the agent is running, editor submissions call `agent.steer()`; otherwise they call `agent.send()`. Ctrl+C or Escape cancels a running turn. Ctrl+O expands tool cards, Ctrl+R toggles reasoning, Ctrl+L redraws, and Ctrl+D exits while idle. `/help`, `/clear`, `/cancel`, `/reasoning`, `/tools`, `/redraw`, and `/exit` provide the same actions without key chords. + +## Config + +| Key | Default | Meaning | +|---|---|---| +| `welcome` | `ready.` | Header subtitle | +| `agent` | `main` | Agent id driven by the terminal | +| `showReasoning` | `true` | Render reasoning blocks | +| `maxToolOutputLines` | `12` | Collapsed tool-card output limit | +| `maxQuestionOptions` | `8` | Visible options in a question overlay | +| `questionDialogWidth` | `72` | Question-overlay width in columns | +| `questionDialogMaxHeight` | `20` | Question-overlay maximum rows | +| `showHardwareCursor` | `false` | Show the hardware cursor at pi-tui's IME marker | +| `color` | `true` | Apply the built-in ANSI palette | +| `title` | `DeepSeek Harness` | Terminal window title | + +```yaml +- id: terminal + name: '@deepseek-ai/dsh-tui' + config: + welcome: 'Coding agent ready.' + agent: main + showReasoning: true + maxToolOutputLines: 12 +``` + +Startup fails before mounting when either process stream is not a TTY. Disposal stops loaders, rejects pending questions, drains terminal input, restores terminal state, unregisters event listeners and the user-interaction provider, and never exits a replacement process during HMR. + +## Model Experience + +### Interactive prompt input + +**What the model sees**: Each non-empty editor submission becomes one text block, sent with `agent.send()` while the target agent is idle and `agent.steer()` while it is running. Slash commands and keybindings are TUI-only. + +**Token effect**: Submitted text is retained under the agent loop's normal session-history and compaction rules. Headers, cards, Markdown rendering, status lines, plans, and help text add no tokens. + +### Interactive user-question answers + +**What the model sees**: When a consumer calls `ctx.userInteraction.ask()`, this provider presents each question in order and returns selected option labels or `custom` text. Abort, cancellation, or UI disposal becomes `Error: ask_user_question was interrupted before the user answered` through `dsh-tool-ask-user`. + +**Token effect**: Waiting and terminal overlays add no tokens; the resolved answer or error is model-visible only through the calling tool or plugin's result. + +## Known Limitations and Deferred Work + +- **One configured agent owns the transcript and editor** — questions from other agents can still use the shared overlay provider, but session rendering and prompt input remain bound to `agent`. +- **Tool cards are text terminal presentations** — terminal, diff, and generic cards use tool-owned titles/content, but session content currently has no image block for inline image rendering. +- **Non-TTY operation is intentionally unsupported** — app bundles that need automation must select `dsh-stdio` before mounting this plugin rather than expecting an internal fallback. diff --git a/packages/ui/tui/package.json b/packages/ui/tui/package.json new file mode 100644 index 0000000000..e960f33e65 --- /dev/null +++ b/packages/ui/tui/package.json @@ -0,0 +1,45 @@ +{ + "name": "@deepseek-ai/dsh-tui", + "description": "Interactive pi-tui terminal front door for DeepSeek Harness agents", + "version": "0.0.1", + "private": true, + "type": "module", + "main": "lib/index.js", + "types": "lib/types/index.d.ts", + "exports": { + ".": { + "types": "./lib/types/index.d.ts", + "default": "./lib/index.js" + }, + "./src/*": "./src/*", + "./package.json": "./package.json" + }, + "files": [ + "lib/index.js", + "lib/types/**/*.d.ts", + "lib/types/**/*.d.ts.map", + "src" + ], + "license": "BSD-3-Clause", + "peerDependencies": { + "@deepseek-ai/dsh-agent": "^0.0.1", + "@deepseek-ai/dsh-llm": "^0.0.1", + "@deepseek-ai/dsh-session": "^0.0.1", + "@deepseek-ai/dsh-tools": "^0.0.1", + "@deepseek-ai/dsh-user-interaction": "^0.0.1", + "cordis": "^4.0.0-rc.6" + }, + "dependencies": { + "@earendil-works/pi-tui": "0.80.7", + "schemastery": "^3.18.0" + }, + "devDependencies": { + "@cordisjs/plugin-loader": "workspace:^", + "@deepseek-ai/dsh-agent": "workspace:^", + "@deepseek-ai/dsh-llm": "workspace:^", + "@deepseek-ai/dsh-session": "workspace:^", + "@deepseek-ai/dsh-tools": "workspace:^", + "@deepseek-ai/dsh-user-interaction": "workspace:^", + "cordis": "^4.0.0-rc.6" + } +} diff --git a/packages/ui/tui/src/index.ts b/packages/ui/tui/src/index.ts new file mode 100644 index 0000000000..14f45242d7 --- /dev/null +++ b/packages/ui/tui/src/index.ts @@ -0,0 +1,1284 @@ +/** + * Interactive pi-tui front door for DeepSeek Harness agents. It renders the + * durable session transcript, drives one configured agent, and provides + * keyboard-driven user-interaction dialogs without owning agent lifecycle. + * @module @deepseek-ai/dsh-tui + */ + +import { homedir } from 'node:os' +import { relative, resolve, sep } from 'node:path' +import { + Box, + CombinedAutocompleteProvider, + Container, + Editor, + Input, + Key, + Loader, + Markdown, + Spacer, + Text, + TUI, + ProcessTerminal, + matchesKey, + truncateToWidth, + visibleWidth, + wrapTextWithAnsi, + type Component, + type EditorTheme, + type Focusable, + type MarkdownTheme, + type OverlayHandle, + type SelectListTheme, + type Terminal, +} from '@earendil-works/pi-tui' +import type { Context } from 'cordis' +import z from 'schemastery' +import { AgentId, type Agent, type AgentStatus } from '@deepseek-ai/dsh-agent' +import type { ContentBlock, StreamChunk } from '@deepseek-ai/dsh-llm' +import type { Session, SessionEvent, TodoItem } from '@deepseek-ai/dsh-session' +import type { + FileDiff, + TerminalCallView, + ToolCallView, + ToolDefinition, + ToolResultView, +} from '@deepseek-ai/dsh-tools' +import { + UserInteractionError, + type AskUserQuestionAnswer, + type AskUserQuestionAnswerItem, + type AskUserQuestionItem, + type AskUserQuestionRequest, +} from '@deepseek-ai/dsh-user-interaction' + +export const name = 'ui-tui' +export const inject = ['agents', 'userInteraction', 'tools'] + +/** Presentation settings for the pi-tui terminal mode. */ +export interface TuiConfig { + /** Render model reasoning blocks. */ + showReasoning?: boolean + /** Maximum tool-output lines shown before the card is collapsed. */ + maxToolOutputLines?: number + /** Maximum options visible at once in a user-question dialog. */ + maxQuestionOptions?: number + /** User-question dialog width in terminal columns. */ + questionDialogWidth?: number + /** User-question dialog maximum height in terminal rows. */ + questionDialogMaxHeight?: number + /** Show the terminal's hardware cursor at the pi editor's IME marker. */ + showHardwareCursor?: boolean + /** Apply the built-in ANSI color palette. */ + color?: boolean + /** Terminal window title while the UI is mounted. */ + title?: string +} + +const showReasoningSchema = z.boolean().default(true) +const maxToolOutputLinesSchema = z.number().step(1).min(1).default(12) +const maxQuestionOptionsSchema = z.number().step(1).min(1).default(8) +const questionDialogWidthSchema = z.number().step(1).min(20).default(72) +const questionDialogMaxHeightSchema = z.number().step(1).min(6).default(20) +const showHardwareCursorSchema = z.boolean().default(false) +const colorSchema = z.boolean().default(true) +const titleSchema = z.string().default('DeepSeek Harness') + +/** Schemastery schema for presentation settings embedded by app bundles. */ +export const TuiConfigSchema: z = z.object({ + showReasoning: showReasoningSchema, + maxToolOutputLines: maxToolOutputLinesSchema, + maxQuestionOptions: maxQuestionOptionsSchema, + questionDialogWidth: questionDialogWidthSchema, + questionDialogMaxHeight: questionDialogMaxHeightSchema, + showHardwareCursor: showHardwareCursorSchema, + color: colorSchema, + title: titleSchema, +}) + +/** Serializable plugin configuration. */ +export interface Config extends TuiConfig { + /** Header subtitle. Defaults to `ready.`. */ + welcome?: string + /** Agent id driven by this terminal. Defaults to `main`. */ + agent?: string +} + +export const Config: z = z.object({ + welcome: z.string().default('ready.'), + agent: z.string().default('main'), + showReasoning: showReasoningSchema, + maxToolOutputLines: maxToolOutputLinesSchema, + maxQuestionOptions: maxQuestionOptionsSchema, + questionDialogWidth: questionDialogWidthSchema, + questionDialogMaxHeight: questionDialogMaxHeightSchema, + showHardwareCursor: showHardwareCursorSchema, + color: colorSchema, + title: titleSchema, +}) + +/** Fully defaulted TUI presentation settings. */ +export interface ResolvedTuiConfig { + showReasoning: boolean + maxToolOutputLines: number + maxQuestionOptions: number + questionDialogWidth: number + questionDialogMaxHeight: number + showHardwareCursor: boolean + color: boolean + title: string +} + +/** Runtime boundary used by the interactive TUI. */ +export interface TuiRuntime { + /** Terminal implementation; production uses pi-tui's `ProcessTerminal`. */ + terminal: Terminal + /** Exit hook used by `/exit`, Ctrl+D, or Ctrl+C while idle. */ + exit(code: number): void +} + +/** + * Apply direct-call defaults after Loader schema validation has normally run. + * + * @param config - Deployment-provided terminal presentation settings. + * @returns Complete settings consumed by the TUI renderer. + */ +export function resolveTuiConfig(config: TuiConfig | undefined): ResolvedTuiConfig { + return { + showReasoning: config?.showReasoning ?? true, + maxToolOutputLines: config?.maxToolOutputLines ?? 12, + maxQuestionOptions: config?.maxQuestionOptions ?? 8, + questionDialogWidth: config?.questionDialogWidth ?? 72, + questionDialogMaxHeight: config?.questionDialogMaxHeight ?? 20, + showHardwareCursor: config?.showHardwareCursor ?? false, + color: config?.color ?? true, + title: config?.title ?? 'DeepSeek Harness', + } +} + +interface Palette { + accent: (text: string) => string + accent2: (text: string) => string + text: (text: string) => string + muted: (text: string) => string + dim: (text: string) => string + success: (text: string) => string + warning: (text: string) => string + error: (text: string) => string + code: (text: string) => string + added: (text: string) => string + removed: (text: string) => string + bold: (text: string) => string + italic: (text: string) => string + underline: (text: string) => string + strike: (text: string) => string + userBg: (text: string) => string + pendingBg: (text: string) => string + successBg: (text: string) => string + errorBg: (text: string) => string + selectedBg: (text: string) => string +} + +function ansi(open: string, close: string, enabled: boolean): (text: string) => string { + return enabled ? text => `\x1b[${open}m${text}\x1b[${close}m` : text => text +} + +function createPalette(enabled: boolean): Palette { + return { + accent: ansi('38;5;39', '39', enabled), + accent2: ansi('38;5;141', '39', enabled), + text: ansi('38;5;252', '39', enabled), + muted: ansi('38;5;245', '39', enabled), + dim: ansi('38;5;240', '39', enabled), + success: ansi('38;5;78', '39', enabled), + warning: ansi('38;5;221', '39', enabled), + error: ansi('38;5;203', '39', enabled), + code: ansi('38;5;215', '39', enabled), + added: ansi('38;5;78', '39', enabled), + removed: ansi('38;5;203', '39', enabled), + bold: ansi('1', '22', enabled), + italic: ansi('3', '23', enabled), + underline: ansi('4', '24', enabled), + strike: ansi('9', '29', enabled), + userBg: ansi('48;5;236', '49', enabled), + pendingBg: ansi('48;5;235', '49', enabled), + successBg: ansi('48;5;22', '49', enabled), + errorBg: ansi('48;5;52', '49', enabled), + selectedBg: ansi('48;5;24', '49', enabled), + } +} + +function markdownTheme(palette: Palette): MarkdownTheme { + return { + heading: text => palette.accent(text), + link: text => palette.accent(text), + // pi-tui requires this URL slot but its current Markdown renderer does not invoke it. + /* v8 ignore next */ + linkUrl: text => palette.dim(text), + code: text => palette.code(text), + codeBlock: text => palette.text(text), + codeBlockBorder: text => palette.dim(text), + quote: text => palette.muted(text), + quoteBorder: text => palette.accent2(text), + hr: text => palette.dim(text), + listBullet: text => palette.accent(text), + bold: text => palette.bold(text), + italic: text => palette.italic(text), + strikethrough: text => palette.strike(text), + underline: text => palette.underline(text), + } +} + +function selectTheme(palette: Palette): SelectListTheme { + return { + selectedPrefix: palette.accent, + selectedText: palette.accent, + description: palette.muted, + scrollInfo: palette.dim, + noMatch: palette.warning, + } +} + +function contentText(content: readonly ContentBlock[]): string { + const parts: string[] = [] + for (const block of content) { + switch (block.type) { + case 'text': + case 'reasoning': + parts.push(block.text) + break + case 'tool-call': + parts.push(`${block.name}(${block.arguments})`) + break + case 'tool-result': + parts.push(contentText(block.content)) + break + default: { + const rawType = (block as { type?: unknown }).type + parts.push(`[${typeof rawType === 'string' ? rawType : 'content'}]`) + break + } + } + } + return parts.join('') +} + +function textBlocks(content: readonly ContentBlock[], type: 'text' | 'reasoning'): string { + return content + .filter((block): block is Extract => block.type === type) + .map(block => block.text) + .join('\n\n') +} + +class HeaderComponent implements Component { + constructor( + private readonly agent: Agent, + private readonly welcome: string, + private readonly palette: Palette, + ) {} + + invalidate(): void {} + + render(width: number): string[] { + const usable = Math.max(1, width - 4) + const title = `${this.palette.bold(this.palette.accent('DEEPSEEK'))} ${this.palette.bold('HARNESS')}` + const model = this.agent.options.model ?? 'model unset' + const detail = `${this.agent.id} • ${model} • ${this.agent.session.id}` + const top = this.palette.accent(`╭${'─'.repeat(Math.max(0, width - 2))}╮`) + const bottom = this.palette.accent(`╰${'─'.repeat(Math.max(0, width - 2))}╯`) + const lines = [title, this.palette.muted(this.welcome), this.palette.dim(detail)] + .flatMap(line => wrapTextWithAnsi(line, usable)) + .map((line) => { + const clipped = truncateToWidth(line, usable, '') + return `${this.palette.accent('│')} ${clipped}${' '.repeat(Math.max(0, usable - visibleWidth(clipped)))} ${this.palette.accent('│')}` + }) + return [top, ...lines, bottom] + } +} + +class UserMessageComponent extends Box { + constructor(text: string, palette: Palette, mdTheme: MarkdownTheme, label = 'You') { + super(1, 1, value => palette.userBg(value)) + this.addChild(new Text(palette.bold(palette.accent(label)), 0, 0)) + this.addChild(new Markdown(text, 0, 0, mdTheme, { color: value => palette.text(value) }, { + preserveOrderedListMarkers: true, + preserveBackslashEscapes: true, + })) + } +} + +class AssistantMessageComponent extends Container { + constructor(content: readonly ContentBlock[], showReasoning: boolean, palette: Palette, mdTheme: MarkdownTheme) { + super() + const reasoning = textBlocks(content, 'reasoning').trim() + const text = textBlocks(content, 'text').trim() + if (reasoning && showReasoning) { + this.addChild(new Spacer(1)) + this.addChild(new Text(palette.italic(palette.muted('Reasoning')), 1, 0)) + this.addChild(new Markdown(reasoning, 1, 0, mdTheme, { + color: value => palette.muted(value), + italic: true, + })) + } + if (text) { + this.addChild(new Spacer(1)) + this.addChild(new Text(palette.bold(palette.accent2('Assistant')), 1, 0)) + this.addChild(new Markdown(text, 1, 0, mdTheme, { color: value => palette.text(value) })) + } + } +} + +interface StreamingBlock { + type: string + text: string +} + +class StreamingAssistantComponent extends Container { + private readonly blocks = new Map() + + constructor( + private showReasoning: boolean, + private readonly palette: Palette, + private readonly mdTheme: MarkdownTheme, + ) { + super() + } + + update(chunk: StreamChunk): void { + if (chunk.type === 'block-start') { + this.blocks.set(chunk.index, { type: chunk.blockType, text: '' }) + } else if (chunk.type === 'text-delta' || chunk.type === 'reasoning-delta') { + const type = chunk.type === 'text-delta' ? 'text' : 'reasoning' + const block = this.blocks.get(chunk.index) ?? { type, text: '' } + block.text += chunk.text + this.blocks.set(chunk.index, block) + } else if (chunk.type === 'block-end' && (chunk.block.type === 'text' || chunk.block.type === 'reasoning')) { + this.blocks.set(chunk.index, { type: chunk.block.type, text: chunk.block.text }) + } + this.rebuild() + } + + setShowReasoning(show: boolean): void { + this.showReasoning = show + this.rebuild() + } + + private rebuild(): void { + this.clear() + const content: ContentBlock[] = [...this.blocks.entries()] + .sort(([left], [right]) => left - right) + .flatMap(([, block]) => { + if (block.type === 'text') return [{ type: 'text', text: block.text }] + if (block.type === 'reasoning') return [{ type: 'reasoning', text: block.text }] + return [] + }) + const component = new AssistantMessageComponent(content, this.showReasoning, this.palette, this.mdTheme) + for (const child of component.children) this.addChild(child) + } +} + +interface ParsedArguments { + value: unknown + valid: boolean +} + +function parseArguments(raw: string): ParsedArguments { + try { + return { value: JSON.parse(raw), valid: true } + } catch { + return { value: raw, valid: false } + } +} + +function pretty(value: unknown): string { + if (typeof value === 'string') return value + // The lib declaration narrows `unknown` to a string-returning overload, but + // JSON.stringify returns undefined for runtime values such as symbols. + const serialized = JSON.stringify(value, null, 2) as string | undefined + return serialized ?? String(value) +} + +function diffLines(diff: FileDiff, palette: Palette): string[] { + const lines = [palette.bold(diff.path)] + if (diff.oldText !== null) { + for (const line of diff.oldText.split('\n')) lines.push(palette.removed(`- ${line}`)) + } + for (const line of diff.newText.split('\n')) lines.push(palette.added(`+ ${line}`)) + return lines +} + +class ToolCardComponent implements Component { + private result: { content: ContentBlock[]; isError: boolean; meta?: unknown } | undefined + private expanded = false + private callView: ToolCallView + private resultView: ToolResultView | undefined + + constructor( + private readonly name: string, + private readonly parsed: ParsedArguments, + private readonly definition: ToolDefinition | undefined, + private readonly maxOutputLines: number, + private readonly palette: Palette, + ) { + this.callView = this.presentCall() + } + + private presentCall(): ToolCallView { + if (this.parsed.valid && this.definition?.presentCall) { + try { + const view = this.definition.presentCall(this.parsed.value) + if (view !== undefined) return view + } catch (error: unknown) { + return { card: 'generic', title: this.name, rawInput: `Presenter failed: ${String(error)}` } + } + } + return { card: 'generic', title: this.name, rawInput: this.parsed.value } + } + + updateResult(event: Extract['data']): void { + this.result = { + content: [...event.content], + isError: event.isError, + ...event.meta !== undefined ? { meta: event.meta } : {}, + } + if (this.parsed.valid && this.definition?.presentResult) { + try { + const view = this.definition.presentResult(this.parsed.value, this.result) + if (view !== undefined) this.resultView = view + } catch (error: unknown) { + this.resultView = { card: 'generic', content: [{ type: 'text', text: `Presenter failed: ${String(error)}` }] } + } + } + } + + setExpanded(expanded: boolean): void { + this.expanded = expanded + } + + invalidate(): void {} + + render(width: number): string[] { + const isError = this.result?.isError ?? false + const glyph = this.result === undefined ? this.palette.warning('◌') : isError ? this.palette.error('✕') : this.palette.success('✓') + const body = this.renderBody() + const title = truncateToWidth(`${glyph} ${this.title()}`, Math.max(1, width - 4), '') + const visibleBody = this.expanded || body.length <= this.maxOutputLines + ? body + : [...body.slice(0, this.maxOutputLines), this.palette.dim(`… ${body.length - this.maxOutputLines} more lines (Ctrl+O to expand)`)] + const box = new Box(1, visibleBody.length > 0 ? 1 : 0, (value) => { + if (this.result === undefined) return this.palette.pendingBg(value) + return isError ? this.palette.errorBg(value) : this.palette.successBg(value) + }) + box.addChild(new Text(this.palette.bold(title), 0, 0)) + if (visibleBody.length > 0) box.addChild(new Text(visibleBody.join('\n'), 0, 0)) + return box.render(width) + } + + private title(): string { + return this.resultView?.title ?? this.callView.title + } + + private renderBody(): string[] { + const view = this.resultView ?? this.callView + if (view.card === 'terminal') { + const pending = this.callView.card === 'terminal' ? this.callView : undefined + const lines: string[] = [] + if (pending?.description) lines.push(this.palette.muted(pending.description)) + if (pending?.cwd) lines.push(this.palette.dim(pending.cwd)) + if (this.resultView?.card === 'terminal') { + if (this.resultView.output) lines.push(...this.resultView.output.split('\n')) + if (this.resultView.exitCode !== undefined) lines.push(this.palette.dim(`[exit ${this.resultView.exitCode}]`)) + if (this.resultView.signal !== undefined) lines.push(this.palette.error(`[signal ${this.resultView.signal}]`)) + } else if (this.result === undefined) { + // A pending terminal view is the call view itself; TerminalCallView requires a title. + lines.push(this.palette.code(`$ ${(pending as TerminalCallView).title}`)) + } else { + lines.push(...contentText(this.result.content).split('\n')) + } + return lines.filter(Boolean) + } + if (view.card === 'diff') { + return view.diffs.flatMap((diff, index) => [ + ...index > 0 ? [''] : [], + ...diffLines(diff, this.palette), + ]) + } + const content = view.content ?? this.result?.content + const lines: string[] = [] + if (content !== undefined) lines.push(...contentText(content).split('\n')) + const rawInput = this.result === undefined && this.callView.card === 'generic' + ? this.callView.rawInput + : undefined + if (rawInput !== undefined) lines.push(...pretty(rawInput).split('\n')) + return lines.filter((line, index, all) => line.length > 0 || (index > 0 && index < all.length - 1)) + } +} + +class TodoComponent implements Component { + private todos: readonly TodoItem[] = [] + + constructor(private readonly palette: Palette) {} + + update(todos: readonly TodoItem[]): void { + this.todos = todos + } + + invalidate(): void {} + + render(width: number): string[] { + if (this.todos.length === 0) return [] + const lines = [this.palette.bold(this.palette.accent('Plan'))] + for (const todo of this.todos) { + const prefix = todo.status === 'completed' + ? this.palette.success('✓') + : todo.status === 'in_progress' + ? this.palette.warning('●') + : this.palette.dim('○') + const text = todo.status === 'completed' ? this.palette.muted(todo.content) : todo.content + lines.push(truncateToWidth(` ${prefix} ${text}`, width, '')) + } + return ['', ...lines] + } +} + +function formatTokens(value: number): string { + if (value < 1_000) return String(value) + if (value < 10_000) return `${(value / 1_000).toFixed(1)}k` + if (value < 1_000_000) return `${Math.round(value / 1_000)}k` + return `${(value / 1_000_000).toFixed(1)}m` +} + +function formatCwd(cwd: string | undefined): string { + if (cwd === undefined) return 'cwd unset' + const home = homedir() + const rel = relative(resolve(home), resolve(cwd)) + if (rel === '') return '~' + if (rel !== '..' && !rel.startsWith(`..${sep}`)) return `~${sep}${rel}` + return cwd +} + +function sessionTokens(session: Session): { input: number; output: number } { + let input = 0 + let output = 0 + for (const event of session.events) { + if (event.type !== 'assistant/message' || event.data.usage === undefined) continue + input += event.data.usage.inputTokens + output += event.data.usage.outputTokens + } + return { input, output } +} + +class FooterComponent implements Component { + constructor( + private readonly agent: Agent, + private readonly palette: Palette, + private readonly toolsExpanded: () => boolean, + private readonly showReasoning: () => boolean, + private readonly tokens: () => { input: number; output: number }, + ) {} + + invalidate(): void {} + + render(width: number): string[] { + const { input, output } = this.tokens() + const left = `${formatCwd(this.agent.session.header.cwd)} ↑${formatTokens(input)} ↓${formatTokens(output)}` + const right = `${this.agent.status} reasoning:${this.showReasoning() ? 'on' : 'off'} tools:${this.toolsExpanded() ? 'expanded' : 'compact'}` + const leftStyled = this.palette.dim(left) + const available = Math.max(0, width - visibleWidth(left) - 2) + const rightClipped = truncateToWidth(right, available, '') + const gap = ' '.repeat(Math.max(1, width - visibleWidth(left) - visibleWidth(rightClipped))) + return [truncateToWidth(`${leftStyled}${gap}${this.palette.dim(rightClipped)}`, width, '')] + } +} + +interface QuestionSelection { + selected: string[] + custom?: string +} + +class QuestionDialog implements Component, Focusable { + private selectedIndex = 0 + private selected = new Set() + private mode: 'options' | 'custom' + private error = '' + private readonly input = new Input() + private readonly options: NonNullable + focused = false + + constructor( + private readonly question: AskUserQuestionItem, + private readonly maxVisible: number, + private readonly palette: Palette, + private readonly done: (selection: QuestionSelection) => void, + private readonly cancel: () => void, + ) { + this.options = question.options ?? [] + this.mode = this.options.length > 0 ? 'options' : 'custom' + this.input.onSubmit = (value) => { this.submitCustom(value) } + this.input.onEscape = () => { + if (this.options.length > 0) { + this.mode = 'options' + this.error = '' + } else { + this.cancel() + } + } + } + + invalidate(): void { + this.input.invalidate() + } + + handleInput(data: string): void { + this.invalidate() + if (this.mode === 'custom') { + this.input.focused = this.focused + this.input.handleInput(data) + return + } + const options = this.options + if (matchesKey(data, Key.up)) { + this.selectedIndex = this.selectedIndex === 0 ? options.length - 1 : this.selectedIndex - 1 + } else if (matchesKey(data, Key.down)) { + this.selectedIndex = this.selectedIndex === options.length - 1 ? 0 : this.selectedIndex + 1 + } else if (matchesKey(data, Key.space) && this.question.multiSelect) { + if (this.selected.has(this.selectedIndex)) this.selected.delete(this.selectedIndex) + else this.selected.add(this.selectedIndex) + } else if (matchesKey(data, Key.enter)) { + const indices = this.question.multiSelect ? [...this.selected].sort((a, b) => a - b) : [this.selectedIndex] + if (indices.length === 0) { + this.error = 'Select at least one option, or press C for a custom answer.' + return + } + this.done({ selected: indices.map(index => options[index]?.label).filter((label): label is string => label !== undefined) }) + } else if (data.toLowerCase() === 'c') { + this.mode = 'custom' + this.error = '' + } else if (matchesKey(data, Key.escape) || matchesKey(data, Key.ctrl('c'))) { + this.cancel() + } + } + + private submitCustom(value: string): void { + const custom = value.trim() + if (custom === '') { + this.error = 'Enter an answer before submitting.' + return + } + this.done({ selected: [], custom }) + } + + render(width: number): string[] { + this.input.focused = this.focused + const innerWidth = Math.max(1, width - 4) + const title = this.question.header ?? 'Question' + const topLabel = ` ${title} ` + const top = `╭${topLabel}${'─'.repeat(Math.max(0, width - visibleWidth(topLabel) - 2))}╮` + const lines: string[] = [this.palette.accent(top)] + const push = (line: string): void => { + const clipped = truncateToWidth(line, innerWidth, '') + lines.push(`${this.palette.accent('│')} ${clipped}${' '.repeat(Math.max(0, innerWidth - visibleWidth(clipped)))} ${this.palette.accent('│')}`) + } + for (const line of wrapTextWithAnsi(this.palette.bold(this.question.question), innerWidth)) push(line) + push('') + if (this.mode === 'custom') { + for (const line of this.input.render(innerWidth)) push(line) + push(this.palette.dim(this.options.length > 0 ? 'Enter submit • Esc options' : 'Enter submit • Esc cancel')) + } else { + const options = this.options + const start = Math.max(0, Math.min( + this.selectedIndex - Math.floor(this.maxVisible / 2), + options.length - this.maxVisible, + )) + const end = Math.min(options.length, start + this.maxVisible) + for (let index = start; index < end; index += 1) { + // `index < end <= options.length`; the options array is borrowed immutably for this dialog. + const option = options[index] as NonNullable[number] + const cursor = index === this.selectedIndex ? this.palette.accent('›') : ' ' + const mark = this.question.multiSelect + ? this.selected.has(index) ? this.palette.success('[x]') : '[ ]' + : index === this.selectedIndex ? this.palette.accent('●') : this.palette.dim('○') + const description = option.description ? this.palette.muted(` — ${option.description}`) : '' + const line = `${cursor} ${mark} ${option.label}${description}` + push(index === this.selectedIndex ? this.palette.selectedBg(line) : line) + } + if (options.length > this.maxVisible) push(this.palette.dim(`${this.selectedIndex + 1}/${options.length}`)) + push(this.palette.dim(this.question.multiSelect + ? '↑↓ navigate • Space toggle • Enter submit • C custom • Esc cancel' + : '↑↓ navigate • Enter select • C custom • Esc cancel')) + } + if (this.error) push(this.palette.error(this.error)) + lines.push(this.palette.accent(`╰${'─'.repeat(Math.max(0, width - 2))}╯`)) + return lines + } +} + +interface PendingQuestion { + request: AskUserQuestionRequest + index: number + answers: AskUserQuestionAnswerItem[] + resolve(answer: AskUserQuestionAnswer): void + reject(error: unknown): void + onAbort: () => void + overlay: OverlayHandle | undefined +} + +/** Lifecycle handle for a mounted interactive terminal channel. */ +export interface TuiController { + /** Stop rendering, restore the terminal, and reject pending questions. */ + dispose(): Promise +} + +function activeSurfaceSeqs(session: Session): Set { + return new Set(session.surface.nodes.map(node => node.seq)) +} + +function activeToolCallIds(session: Session, active: ReadonlySet): Set { + const ids = new Set() + for (const event of session.events) { + if (event.type !== 'assistant/message' || !active.has(event.seq)) continue + for (const block of event.data.content) { + if (block.type === 'tool-call') ids.add(block.id) + } + } + return ids +} + +/** + * Start the interactive pi-tui channel for an already-created target agent. + * @param ctx - agent, tools, session-event, and user-interaction context. + * @param config - target agent, banner, and TUI presentation config. + * @param runtime - terminal and process-exit boundary. + * @returns lifecycle controller used by the Cordis effect disposer. + */ +export function createTuiChat( + ctx: Context, + config: Config, + runtime: TuiRuntime, +): TuiController { + const agentId = AgentId(config.agent ?? 'main') + const agent = ctx.agents.get(agentId) + if (agent === undefined) throw new Error(`ui-tui: agent "${agentId}" is not running`) + const resolved = resolveTuiConfig(config) + const palette = createPalette(resolved.color) + const mdTheme = markdownTheme(palette) + const ui = new TUI(runtime.terminal, resolved.showHardwareCursor) + const chat = new Container() + const todoContainer = new Container() + const statusContainer = new Container() + const editor = new Editor(ui, { + borderColor: palette.dim, + selectList: selectTheme(palette), + } satisfies EditorTheme, { paddingX: 1 }) + const todo = new TodoComponent(palette) + let showReasoning = resolved.showReasoning + let toolsExpanded = false + let streaming: StreamingAssistantComponent | undefined + let statusLoader: Loader | undefined + let disposed = false + let shuttingDown: Promise | undefined + const tokens = sessionTokens(agent.session) + const toolCards = new Map() + const allToolCards = new Set() + const liveErrors = new Set() + const questionQueue: PendingQuestion[] = [] + let activeQuestion: PendingQuestion | undefined + + const welcome = config.welcome ?? 'ready.' + const header = new HeaderComponent(agent, welcome, palette) + const footer = new FooterComponent(agent, palette, () => toolsExpanded, () => showReasoning, () => tokens) + ui.addChild(header) + ui.addChild(chat) + ui.addChild(statusContainer) + todoContainer.addChild(todo) + ui.addChild(todoContainer) + ui.addChild(editor) + ui.addChild(footer) + ui.setFocus(editor) + runtime.terminal.setTitle(resolved.title) + + const requestRender = (): void => { + footer.invalidate() + ui.requestRender() + } + + const appendNotice = (message: string, kind: 'info' | 'warning' | 'error' = 'info'): void => { + const color = kind === 'error' ? palette.error : kind === 'warning' ? palette.warning : palette.muted + chat.addChild(new Spacer(1)) + chat.addChild(new Text(color(message), 1, 0)) + requestRender() + } + + const clearStatus = (): void => { + statusLoader?.stop() + statusLoader = undefined + statusContainer.clear() + runtime.terminal.setProgress(false) + } + + const setStatus = (status: AgentStatus): void => { + clearStatus() + editor.borderColor = status === 'running' ? text => palette.accent(text) : text => palette.dim(text) + if (status === 'running') { + statusLoader = new Loader(ui, text => palette.accent(text), text => palette.muted(text), 'Working — Enter sends steering, Esc cancels') + statusContainer.addChild(statusLoader) + runtime.terminal.setProgress(true) + } + requestRender() + } + + const parsedTool = (event: Extract): ToolCardComponent => { + const parsed = parseArguments(event.data.arguments) + const card = new ToolCardComponent( + event.data.name, + parsed, + ctx.tools.get(event.data.name, agent), + resolved.maxToolOutputLines, + palette, + ) + card.setExpanded(toolsExpanded) + toolCards.set(event.data.callId, card) + allToolCards.add(card) + return card + } + + const renderEvent = (event: SessionEvent, options: { addHistory: boolean; renderChunks: boolean }): void => { + switch (event.type) { + case 'user/message': { + const text = contentText(event.data.content).trim() + if (text) { + chat.addChild(new Spacer(1)) + chat.addChild(new UserMessageComponent(text, palette, mdTheme)) + if (options.addHistory) editor.addToHistory(text) + } + break + } + case 'steering/message': { + const text = contentText(event.data.content).trim() + if (text) { + chat.addChild(new Spacer(1)) + chat.addChild(new UserMessageComponent(text, palette, mdTheme, 'Steering')) + } + break + } + case 'context/message': { + const text = contentText(event.data.content).trim() + if (text) { + const source = event.data.source.kind === 'plugin' ? event.data.source.plugin : event.data.source.kind + chat.addChild(new Spacer(1)) + chat.addChild(new Text(palette.dim(`Context · ${source}`), 1, 0)) + chat.addChild(new Text(palette.muted(text), 1, 0)) + } + break + } + case 'prompt/blocked': + appendNotice(`Prompt blocked: ${event.data.reason}`, 'warning') + break + case 'assistant/chunk': + if (options.renderChunks) { + if (streaming === undefined) { + streaming = new StreamingAssistantComponent(showReasoning, palette, mdTheme) + chat.addChild(streaming) + } + streaming.update(event.data.chunk) + } + break + case 'assistant/message': { + if (streaming !== undefined) { + const index = chat.children.indexOf(streaming) + if (index >= 0) chat.children.splice(index, 1) + streaming = undefined + } + const component = new AssistantMessageComponent(event.data.content, showReasoning, palette, mdTheme) + if (component.children.length > 0) chat.addChild(component) + break + } + case 'tool/call': + chat.addChild(new Spacer(1)) + chat.addChild(parsedTool(event)) + break + case 'tool/result': { + let card = toolCards.get(event.data.callId) + if (card === undefined) { + card = new ToolCardComponent('tool', { value: {}, valid: true }, undefined, resolved.maxToolOutputLines, palette) + chat.addChild(new Spacer(1)) + chat.addChild(card) + allToolCards.add(card) + } + card.updateResult(event.data) + toolCards.delete(event.data.callId) + break + } + case 'todo/write': + todo.update(event.data.todos) + break + case 'turn/end': + if (event.data.reason.kind === 'error') { + const key = `${event.data.turn}:${event.data.reason.step}` + if (!liveErrors.delete(key)) appendNotice(event.data.reason.message, 'error') + } else if (event.data.reason.kind === 'aborted') { + appendNotice(event.data.reason.reason ?? 'Turn cancelled.', 'warning') + } else if (event.data.reason.kind === 'max-tokens') { + appendNotice('The model reached its output-token limit.', 'warning') + } else if (event.data.reason.kind === 'rejected') { + appendNotice(`Turn rejected: ${event.data.reason.reason}`, 'warning') + } else if (event.data.reason.kind === 'interrupted') { + appendNotice('The previous process ended during this turn.', 'warning') + } + break + default: + break + } + } + + const rebuildTranscript = (populateHistory: boolean): void => { + chat.clear() + toolCards.clear() + allToolCards.clear() + streaming = undefined + const active = activeSurfaceSeqs(agent.session) + const activeCalls = activeToolCallIds(agent.session, active) + for (const event of agent.session.events) { + const isSurface = event.type === 'user/message' + || event.type === 'assistant/message' + || event.type === 'tool/result' + || event.type === 'context/message' + || event.type === 'steering/message' + if (isSurface && !active.has(event.seq)) continue + if (event.type === 'tool/call' && !activeCalls.has(event.data.callId)) continue + renderEvent(event, { addHistory: populateHistory, renderChunks: false }) + } + requestRender() + } + + const removeAbortListener = (pending: PendingQuestion): void => { + pending.request.signal?.removeEventListener('abort', pending.onAbort) + } + + const rejectQuestion = (pending: PendingQuestion): void => { + pending.overlay?.hide() + pending.overlay = undefined + removeAbortListener(pending) + pending.reject(new UserInteractionError( + 'ask_user_question was interrupted before the user answered', + 'ASK_ABORTED', + )) + } + + const startNextQuestion = (): void => { + if (activeQuestion !== undefined || disposed) return + const pending = questionQueue.shift() + if (pending === undefined) return + activeQuestion = pending + const show = (): void => { + const question = pending.request.questions[pending.index] + if (question === undefined) { + activeQuestion = undefined + removeAbortListener(pending) + pending.resolve({ answers: pending.answers }) + startNextQuestion() + return + } + const dialog = new QuestionDialog( + question, + resolved.maxQuestionOptions, + palette, + (selection) => { + pending.overlay?.hide() + pending.overlay = undefined + pending.answers.push({ id: question.id, ...selection }) + pending.index += 1 + show() + }, + () => { + activeQuestion = undefined + rejectQuestion(pending) + startNextQuestion() + }, + ) + pending.overlay = ui.showOverlay(dialog, { + width: resolved.questionDialogWidth, + maxHeight: resolved.questionDialogMaxHeight, + anchor: 'center', + margin: 1, + }) + requestRender() + } + show() + } + + const disposeUserInteraction = ctx.userInteraction.registerProvider({ + ask(request) { + return new Promise((resolveAnswer, reject) => { + const pending: PendingQuestion = { + request, + index: 0, + answers: [], + resolve: resolveAnswer, + reject, + overlay: undefined, + onAbort: () => { + if (activeQuestion === pending) { + activeQuestion = undefined + rejectQuestion(pending) + startNextQuestion() + return + } + // A non-active pending ask remains in the queue until this listener settles it. + questionQueue.splice(questionQueue.indexOf(pending), 1) + rejectQuestion(pending) + }, + } + request.signal?.addEventListener('abort', pending.onAbort, { once: true }) + questionQueue.push(pending) + startNextQuestion() + }) + }, + }) + + const shutdown = (exitProcess: boolean): Promise => { + shuttingDown ??= (async () => { + disposed = true + clearStatus() + if (activeQuestion !== undefined) { + const pending = activeQuestion + activeQuestion = undefined + rejectQuestion(pending) + } + for (const pending of questionQueue.splice(0)) rejectQuestion(pending) + disposeUserInteraction() + await runtime.terminal.drainInput(100, 20) + ui.stop() + if (exitProcess) runtime.exit(0) + })() + return shuttingDown + } + + const requestExit = (): void => { + if (agent.status === 'running') { + agent.cancel('terminal exit requested') + appendNotice('Cancelling the active turn before exit…', 'warning') + void agent.whenIdle().then(() => shutdown(true)) + return + } + void shutdown(true) + } + + editor.setAutocompleteProvider(new CombinedAutocompleteProvider([ + { name: 'help', description: 'Show keyboard shortcuts and commands' }, + { name: 'clear', description: 'Clear the transcript view (session history is unchanged)' }, + { name: 'cancel', description: 'Cancel the active turn' }, + { name: 'reasoning', description: 'Toggle reasoning blocks' }, + { name: 'tools', description: 'Expand or collapse all tool cards' }, + { name: 'redraw', description: 'Invalidate components and redraw the terminal' }, + { name: 'exit', description: 'Exit after the active turn reaches idle' }, + ], agent.session.header.cwd ?? process.cwd())) + + const toggleTools = (): void => { + toolsExpanded = !toolsExpanded + for (const card of allToolCards) card.setExpanded(toolsExpanded) + appendNotice(`Tool cards ${toolsExpanded ? 'expanded' : 'collapsed'}.`) + } + + const toggleReasoning = (): void => { + showReasoning = !showReasoning + const activeStreaming = streaming + rebuildTranscript(false) + if (activeStreaming !== undefined) { + streaming = activeStreaming + streaming.setShowReasoning(showReasoning) + chat.addChild(activeStreaming) + } + appendNotice(`Reasoning blocks ${showReasoning ? 'shown' : 'hidden'}.`) + } + + const showHelp = (): void => { + chat.addChild(new Spacer(1)) + chat.addChild(new Text(palette.bold(palette.accent('Keyboard shortcuts')), 1, 0)) + chat.addChild(new Text([ + 'Enter send • Shift/Alt+Enter newline • Up/Down prompt history', + 'Esc cancel active turn • Ctrl+O expand tool cards • Ctrl+R toggle reasoning', + 'Ctrl+C cancel while running; clear input or exit while idle • Ctrl+D exit', + '/help /clear /cancel /reasoning /tools /redraw /exit', + ].map(line => palette.muted(line)).join('\n'), 1, 0)) + requestRender() + } + + editor.onSubmit = (value: string) => { + const text = value.trim() + if (text === '') return + editor.addToHistory(text) + editor.setText('') + switch (text) { + case '/help': + showHelp() + return + case '/clear': + chat.clear() + requestRender() + return + case '/cancel': + if (agent.status === 'running') agent.cancel('cancelled from terminal') + else appendNotice('The agent is already idle.') + return + case '/reasoning': + toggleReasoning() + return + case '/tools': + toggleTools() + return + case '/redraw': + ui.invalidate() + ui.requestRender(true) + return + case '/exit': + requestExit() + return + default: + if (text.startsWith('/')) { + appendNotice(`Unknown command: ${text}`, 'warning') + return + } + } + if (agent.status === 'disposed') { + appendNotice(`Agent "${agent.id}" is disposed.`, 'error') + } else if (agent.status === 'running') { + agent.steer([{ type: 'text', text }]) + } else { + agent.send([{ type: 'text', text }]) + } + } + + const removeInputListener = ui.addInputListener((data) => { + if (activeQuestion !== undefined) return undefined + if (matchesKey(data, Key.ctrl('o'))) { + toggleTools() + return { consume: true } + } + if (matchesKey(data, Key.ctrl('r'))) { + toggleReasoning() + return { consume: true } + } + if (matchesKey(data, Key.ctrl('l'))) { + ui.invalidate() + ui.requestRender(true) + return { consume: true } + } + if (matchesKey(data, Key.escape) && agent.status === 'running') { + agent.cancel('cancelled from terminal') + return { consume: true } + } + if (matchesKey(data, Key.ctrl('c'))) { + if (agent.status === 'running') { + agent.cancel('cancelled from terminal') + } else if (editor.getText() !== '') { + editor.setText('') + } else { + requestExit() + } + return { consume: true } + } + if (matchesKey(data, Key.ctrl('d'))) { + if (agent.status === 'running') appendNotice('Cancel the active turn before exiting.', 'warning') + else requestExit() + return { consume: true } + } + return undefined + }) + + const disposeSessionEvents = ctx.on('session/event', (session, event) => { + if (session !== agent.session) return + if (event.type === 'assistant/message' && event.data.usage !== undefined) { + tokens.input += event.data.usage.inputTokens + tokens.output += event.data.usage.outputTokens + } + if ('surfaceOp' in event && typeof event.surfaceOp === 'object') { + rebuildTranscript(false) + return + } + renderEvent(event, { addHistory: false, renderChunks: true }) + requestRender() + }) + const disposeStatus = ctx.on('agent/status', (subject, status) => { + if (subject !== agent) return + setStatus(status) + }) + const disposeError = ctx.on('agent/error', (subject, turn, step, error) => { + if (subject !== agent) return + liveErrors.add(`${turn}:${step}`) + appendNotice(error.message, 'error') + }) + const disposeAgent = ctx.on('agent/disposed', (subject) => { + if (subject !== agent) return + clearStatus() + appendNotice(`Agent "${agent.id}" was disposed.`, 'warning') + }) + + const detachListeners = (): void => { + removeInputListener() + disposeSessionEvents() + disposeStatus() + disposeError() + disposeAgent() + } + + rebuildTranscript(true) + setStatus(agent.status) + try { + ui.start() + } catch (error: unknown) { + disposed = true + detachListeners() + clearStatus() + disposeUserInteraction() + ui.stop() + throw error + } + + return { + async dispose(): Promise { + detachListeners() + await shutdown(false) + }, + } +} + +/** + * Open the pi-tui channel once its configured agent exists. + * + * @param ctx - Context supplying the agent registry, tools, and event stream. + * @param config - Target agent and presentation configuration. + * @param runtime - Terminal and process-exit boundary. + */ +export function mountTui(ctx: Context, config: Config, runtime: TuiRuntime): void { + const agentId = AgentId(config.agent ?? 'main') + const start = (): void => { + ctx.effect(() => { + const controller = createTuiChat(ctx, config, runtime) + return () => controller.dispose() + }, 'ui-tui') + } + if (ctx.agents.get(agentId) !== undefined) { + start() + return + } + const dispose = ctx.on('agent/created', (agent) => { + if (agent.id !== agentId) return + dispose() + start() + }) +} + +/** Cordis entry point using the process terminal; explicit TUI composition requires a TTY pair. */ +/* v8 ignore start -- production process wiring; fake-terminal tests cover mountTui/createTuiChat, + and the coding-agent PTY smoke covers the real entry */ +export function apply(ctx: Context, config: Config): void { + if (!process.stdin.isTTY || !process.stdout.isTTY) { + throw new Error('ui-tui: both stdin and stdout must be TTYs; use @deepseek-ai/dsh-stdio for pipes') + } + mountTui(ctx, config, { + terminal: new ProcessTerminal(), + exit: code => process.exit(code), + }) +} +/* v8 ignore stop */ diff --git a/packages/ui/tui/tests/plugin-shape.spec.ts b/packages/ui/tui/tests/plugin-shape.spec.ts new file mode 100644 index 0000000000..d1e4b92f3d --- /dev/null +++ b/packages/ui/tui/tests/plugin-shape.spec.ts @@ -0,0 +1,19 @@ +import { describe, expect, it } from 'vitest' +import Loader from '@cordisjs/plugin-loader' +import * as tui from '../src/index.ts' + +/** Real Loader export-path guard for the namespace TUI plugin. */ +describe('dsh-tui plugin export shape', () => { + it('preserves name, inject, Config, and apply through Loader unwrapping', () => { + expect('default' in tui).toBe(false) + expect(typeof tui.apply).toBe('function') + + const loader = Object.create(Loader.prototype) as Loader + const unwrapped = loader.unwrapExports(tui) as Record + expect(unwrapped).toBe(tui) + expect(unwrapped.name).toBe('ui-tui') + expect(unwrapped.inject).toEqual(['agents', 'userInteraction', 'tools']) + expect(unwrapped.Config).toBeDefined() + expect(typeof unwrapped.apply).toBe('function') + }) +}) diff --git a/packages/ui/tui/tests/tui.spec.ts b/packages/ui/tui/tests/tui.spec.ts new file mode 100644 index 0000000000..e4ffe2efd0 --- /dev/null +++ b/packages/ui/tui/tests/tui.spec.ts @@ -0,0 +1,952 @@ +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 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' + +class FakeTerminal implements Terminal { + columns = 88 + rows = 32 + kittyProtocolActive = false + output = '' + title = '' + progress: boolean[] = [] + started = 0 + stopped = 0 + drainInput = vi.fn(() => Promise.resolve()) + private onInput: (data: string) => void = () => {} + private onResize: () => void = () => {} + + 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 { + this.output += data + } + + moveBy(lines: number): void { + this.output += `[move:${lines}]` + } + + hideCursor(): void { + this.output += '[hide]' + } + + showCursor(): void { + this.output += '[show]' + } + + clearLine(): void { + this.output += '[clear-line]' + } + + clearFromCursor(): void { + this.output += '[clear-rest]' + } + + clearScreen(): void { + this.output += '[clear-screen]' + } + + setTitle(title: string): void { + this.title = title + } + + setProgress(active: boolean): void { + this.progress.push(active) + } + + send(data: string): void { + this.onInput(data) + } + + resize(columns: number, rows = this.rows): void { + this.columns = columns + this.rows = rows + this.onResize() + } +} + +interface FakeAgent extends Agent { + status: AgentStatus + sent: ContentBlock[][] + steered: ContentBlock[][] + cancelled: string[] +} + +async function tick(): Promise { + await new Promise(resolve => setTimeout(resolve, 25)) +} + +async function setup(options: { + status?: AgentStatus + config?: Config + tools?: Record + 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) + 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 }) + await tick() + return { ctx, session, agent, terminal, exit, controller } +} + +async function dispose(setupResult: Awaited>): Promise { + 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, + content, + ...usage === undefined ? {} : { usage }, + }, { surfaceOp: 'append' }) +} + +describe('TUI config', () => { + it('defaults every direct-call TUI option', () => { + expect(resolveTuiConfig(undefined)).toEqual({ + showReasoning: true, + maxToolOutputLines: 12, + maxQuestionOptions: 8, + questionDialogWidth: 72, + questionDialogMaxHeight: 20, + showHardwareCursor: false, + color: true, + title: 'DeepSeek Harness', + }) + expect(resolveTuiConfig({ + showReasoning: false, + maxToolOutputLines: 2, + maxQuestionOptions: 3, + questionDialogWidth: 60, + questionDialogMaxHeight: 14, + showHardwareCursor: true, + color: false, + title: 'DSH', + })).toEqual({ + showReasoning: false, + maxToolOutputLines: 2, + maxQuestionOptions: 3, + questionDialogWidth: 60, + questionDialogMaxHeight: 14, + showHardwareCursor: true, + color: false, + title: 'DSH', + }) + }) +}) + +describe('pi-tui chat lifecycle and transcript', () => { + it('renders its header, footer, replay, streaming answer, todos, and status', async () => { + const result = await setup({ + beforeMount(session) { + appendUser(session, 'restored prompt') + appendAssistant(session, [ + { type: 'reasoning', text: 'restored thought' }, + { type: 'text', text: '**restored answer**' }, + ], { inputTokens: 1_250, outputTokens: 42 }) + session.append('todo/write', { + todos: [ + { content: 'read code', status: 'completed' }, + { content: 'write tests', status: 'in_progress' }, + { content: 'ship', status: 'pending' }, + ], + }) + }, + }) + + expect(result.terminal.started).toBe(1) + expect(result.terminal.title).toBe('DeepSeek Harness') + expect(result.terminal.output).toContain('DEEPSEEK') + expect(result.terminal.output).toContain('Coding agent ready.') + expect(result.terminal.output).toContain('restored prompt') + expect(result.terminal.output).toContain('restored thought') + expect(result.terminal.output).toContain('restored answer') + expect(result.terminal.output).toContain('write tests') + expect(result.terminal.output).toContain('↑1.3k ↓42') + + result.agent.status = 'running' + result.ctx.emit('agent/status', result.agent, 'running') + result.session.append('user/message', { content: [{ type: 'text', text: ' ' }], source: { kind: 'user' } }, { surfaceOp: 'append' }) + result.session.append('steering/message', { turn: 2, content: [{ type: 'text', text: 'steering note' }], source: { kind: 'user' } }, { surfaceOp: 'append' }) + result.session.append('steering/message', { turn: 2, content: [{ type: 'text', text: '' }], source: { kind: 'user' } }, { surfaceOp: 'append' }) + result.session.append('context/message', { content: [{ type: 'text', text: 'user context' }], source: { kind: 'user' } }, { surfaceOp: 'append' }) + result.session.append('context/message', { content: [{ type: 'text', text: '' }], source: { kind: 'user' } }, { surfaceOp: 'append' }) + result.session.append('prompt/blocked', { content: [{ type: 'text', text: 'blocked' }], source: { kind: 'user' }, reason: 'test policy' }) + appendAssistant(result.session, []) + result.session.append('turn/end', { turn: 9, reason: { kind: 'aborted' } }) + result.session.append('turn/end', { turn: 10, reason: { kind: 'completed' } }) + result.session.append('step/start', { turn: 11, step: 0 }) + result.session.append('assistant/chunk', { + turn: 2, + step: 0, + chunk: { type: 'block-start', index: 0, blockType: 'reasoning' }, + }) + result.session.append('assistant/chunk', { + turn: 2, + step: 0, + chunk: { type: 'reasoning-delta', index: 0, text: 'live thought' }, + }) + result.session.append('assistant/chunk', { + turn: 2, + step: 0, + chunk: { type: 'reasoning-delta', index: 9, text: 'unannounced thought' }, + }) + result.session.append('assistant/chunk', { + turn: 2, + step: 0, + chunk: { type: 'block-end', index: 0, block: { type: 'reasoning', text: 'live thought complete' } }, + }) + result.session.append('assistant/chunk', { + turn: 2, + step: 0, + chunk: { type: 'block-start', index: 1, blockType: 'text' }, + }) + result.session.append('assistant/chunk', { + turn: 2, + step: 0, + chunk: { type: 'text-delta', index: 1, text: 'live answer' }, + }) + result.session.append('assistant/chunk', { + turn: 2, + step: 0, + chunk: { type: 'block-end', index: 1, block: { type: 'text', text: 'live answer done' } }, + }) + result.session.append('assistant/chunk', { + turn: 2, + step: 0, + chunk: { type: 'block-start', index: 2, blockType: 'tool-call' }, + }) + result.session.append('assistant/chunk', { + turn: 2, + step: 0, + chunk: { type: 'block-end', index: 2, block: { type: 'tool-call', id: 'stream-tool' as never, name: 'tool', arguments: '{}' } }, + }) + result.session.append('assistant/chunk', { + turn: 2, + step: 0, + chunk: { type: 'tool-call-delta', index: 2, id: 'stream-tool' as never, argumentsDelta: '{}' }, + }) + result.session.append('assistant/chunk', { + turn: 2, + step: 0, + chunk: { type: 'usage', usage: { inputTokens: 1, outputTokens: 2 } }, + }) + await tick() + expect(result.terminal.output).toContain('live thought') + result.terminal.send('\x12') + await tick() + appendAssistant(result.session, [{ type: 'text', text: 'final live answer' }], { inputTokens: 500, outputTokens: 8 }) + await tick() + + expect(result.terminal.output).toContain('Working') + expect(result.terminal.output).toContain('Steering') + expect(result.terminal.output).toContain('user context') + expect(result.terminal.output).toContain('Prompt blocked') + expect(result.terminal.output).toContain('Turn cancelled') + expect(result.terminal.output).toContain('final live answer') + expect(result.terminal.output).toContain('↑1.8k ↓50') + expect(result.terminal.progress).toContain(true) + + result.session.append('assistant/chunk', { + turn: 3, + step: 0, + chunk: { type: 'text-delta', index: 0, text: 'cleared stream' }, + }) + result.terminal.send('/clear') + result.terminal.send('\r') + appendAssistant(result.session, [{ type: 'text', text: 'answer after clear' }]) + await tick() + expect(result.terminal.output).toContain('answer after clear') + + result.agent.status = 'idle' + result.ctx.emit('agent/status', result.agent, 'idle') + await tick() + expect(result.terminal.progress.at(-1)).toBe(false) + await dispose(result) + expect(result.terminal.stopped).toBe(1) + expect(result.terminal.drainInput).toHaveBeenCalledWith(100, 20) + }) + + it('renders the ANSI palette and every markdown/content style', async () => { + const result = await setup({ + config: { color: true }, + beforeMount(session) { + session.append('user/message', { + content: [ + { type: 'text', text: '# Heading\n\n[link](https://example.com) `code`\n\n```ts\nconst x = 1\n```\n\n> quote\n\n---\n\n- item\n\n**bold** *italic* ~~strike~~' }, + { type: 'tool-call', id: 'nested' as never, name: 'nested_tool', arguments: '{}' }, + { type: 'tool-result', toolCallId: 'nested' as never, content: [{ type: 'reasoning', text: 'nested result' }] }, + { type: 'future-block' } as never, + {} as never, + ], + source: { kind: 'user' }, + }, { surfaceOp: 'append' }) + appendAssistant(session, [ + { type: 'reasoning', text: 'styled reasoning' }, + { type: 'text', text: 'styled answer' }, + ], { inputTokens: 2_000_000, outputTokens: 1_500_000 }) + session.append('todo/write', { todos: [ + { content: 'done', status: 'completed' }, + { content: 'active', status: 'in_progress' }, + { content: 'later', status: 'pending' }, + ] }) + }, + }) + result.terminal.send('/') + await tick() + result.terminal.send('zz') + await tick() + result.terminal.send('\x0c') + await tick() + + expect(result.terminal.output).toContain('\x1b[') + expect(result.terminal.output).toContain('Heading') + expect(result.terminal.output).toContain('nested_tool({})') + expect(result.terminal.output).toContain('nested result') + expect(result.terminal.output).toContain('[future-block]') + expect(result.terminal.output).toContain('[content]') + expect(result.terminal.output).toContain('↑2.0m ↓1.5m') + await dispose(result) + }) + + it('suppresses stale replay chunks and does not duplicate editor history on rebuild', async () => { + const result = await setup({ + beforeMount(session) { + appendUser(session, 'first prompt') + appendUser(session, 'second prompt') + session.append('assistant/chunk', { + turn: 2, + step: 0, + chunk: { type: 'text-delta', index: 0, text: 'stale partial response' }, + }) + }, + }) + + expect(result.terminal.output).not.toContain('stale partial response') + result.terminal.send('/reasoning') + result.terminal.send('\r') + result.terminal.send('\x1b[A') + result.terminal.send('\x1b[A') + result.terminal.send('\x1b[A') + result.terminal.send('\r') + expect(result.agent.sent).toEqual([[{ type: 'text', text: 'first prompt' }]]) + await dispose(result) + }) + + it('formats large token totals and cwd variants', async () => { + const homeResult = await setup({ + cwd: process.env.HOME ?? process.cwd(), + beforeMount(session) { + appendAssistant(session, [{ type: 'text', text: 'home' }], { inputTokens: 25_000, outputTokens: 10_000 }) + }, + }) + expect(homeResult.terminal.output).toContain('~ ↑25k ↓10k') + await dispose(homeResult) + + const unsetResult = await setup({ cwd: null }) + expect(unsetResult.terminal.output).toContain('cwd unset') + await dispose(unsetResult) + + const outsideResult = await setup({ cwd: '/opt' }) + expect(outsideResult.terminal.output).toContain('/opt') + await dispose(outsideResult) + }) + + it('sends, steers, handles commands, global keys, and disposed-agent input', async () => { + const result = await setup() + + result.terminal.send('do the work') + result.terminal.send('\r') + expect(result.agent.sent).toEqual([[{ type: 'text', text: 'do the work' }]]) + + result.terminal.send(' ') + result.terminal.send('\r') + + result.agent.status = 'running' + result.terminal.send('steer it') + result.terminal.send('\r') + expect(result.agent.steered).toEqual([[{ type: 'text', text: 'steer it' }]]) + + result.terminal.send('\x1b') + result.terminal.send('\x04') + result.terminal.send('\x03') + result.terminal.send('\x12') + result.terminal.send('\x0f') + result.terminal.send('/cancel') + result.terminal.send('\r') + expect(result.agent.cancelled).toContain('cancelled from terminal') + + result.agent.status = 'idle' + for (const command of ['/help', '/reasoning', '/tools', '/redraw']) { + result.terminal.send(command) + result.terminal.send('\r') + await tick() + } + for (const command of ['/clear', '/cancel', '/wat']) { + result.terminal.send(command) + result.terminal.send('\r') + } + await tick() + result.terminal.send('draft') + result.terminal.send('\x03') + result.terminal.send('\x04') + await tick() + + expect(result.terminal.output).toContain('Keyboard shortcuts') + expect(result.terminal.output).toContain('Reasoning blocks') + expect(result.terminal.output).toContain('Tool cards') + expect(result.terminal.output).toContain('already idle') + expect(result.terminal.output).toContain('Unknown command') + expect(result.exit).toHaveBeenCalledWith(0) + await result.controller.dispose() + await result.ctx.fiber.dispose() + + const ctrlCExit = await setup() + ctrlCExit.terminal.send('\x03') + await tick() + expect(ctrlCExit.exit).toHaveBeenCalledWith(0) + await ctrlCExit.controller.dispose() + await ctrlCExit.ctx.fiber.dispose() + + const disposedAgent = await setup() + disposedAgent.agent.status = 'disposed' + disposedAgent.terminal.send('late input') + disposedAgent.terminal.send('\r') + await tick() + expect(disposedAgent.terminal.output).toContain('is disposed') + await dispose(disposedAgent) + }) + + it('cancels before /exit while running and handles agent errors/disposal', async () => { + const result = await setup({ status: 'running' }) + result.terminal.send('/exit') + result.terminal.send('\r') + await tick() + expect(result.agent.cancelled).toContain('terminal exit requested') + expect(result.exit).toHaveBeenCalledWith(0) + + const events = await setup() + const unrelatedSession = events.ctx.sessions.create(SessionId('unrelated-session')) + const unrelatedAgent = { ...events.agent, id: AgentId('unrelated'), session: unrelatedSession } + unrelatedSession.append('todo/write', { todos: [{ content: 'hidden', status: 'pending' }] }) + events.ctx.emit('agent/status', unrelatedAgent, 'running') + events.ctx.emit('agent/error', unrelatedAgent, 1, 1, new Error('hidden error')) + events.ctx.emit('agent/disposed', unrelatedAgent) + events.ctx.emit('agent/error', events.agent, 3, 2, new Error('live failure')) + events.session.append('turn/end', { turn: 3, reason: { kind: 'error', step: 2, message: 'live failure' } }) + events.session.append('turn/end', { turn: 4, reason: { kind: 'error', step: 1, message: 'durable failure' } }) + events.session.append('turn/end', { turn: 5, reason: { kind: 'aborted', reason: 'stopped' } }) + events.session.append('turn/end', { turn: 6, reason: { kind: 'max-tokens' } }) + events.session.append('turn/end', { turn: 7, reason: { kind: 'rejected', reason: 'policy' } }) + events.session.append('turn/end', { turn: 8, reason: { kind: 'interrupted' } }) + events.ctx.emit('agent/disposed', events.agent) + await tick() + expect(events.terminal.output).toContain('live failure') + expect(events.terminal.output).toContain('durable failure') + expect(events.terminal.output).toContain('stopped') + expect(events.terminal.output).toContain('output-token limit') + expect(events.terminal.output).toContain('Turn rejected') + expect(events.terminal.output).toContain('previous process ended') + expect(events.terminal.output).toContain('was disposed') + await dispose(events) + }) +}) + +describe('tool cards and surface replay', () => { + const tools: Record = { + bash: { + name: 'bash', description: '', parameters: {}, execute: async () => [], + presentCall: () => ({ card: 'terminal', title: 'printf hello', description: 'Run command', cwd: '/tmp' }), + presentResult: () => ({ card: 'terminal', output: 'hello\nworld\nthird', exitCode: 0 }), + }, + signal: { + name: 'signal', description: '', parameters: {}, execute: async () => [], + presentCall: () => ({ card: 'terminal', title: 'sleep 10' }), + presentResult: () => ({ card: 'terminal', signal: 'SIGTERM' }), + }, + edit: { + name: 'edit', description: '', parameters: {}, execute: async () => [], + presentCall: () => ({ + card: 'diff', + title: 'Edit files', + diffs: [ + { path: 'a.txt', oldText: 'old', newText: 'new' }, + { path: 'b.txt', oldText: 'before', newText: 'after' }, + ], + }), + presentResult: () => ({ card: 'diff', diffs: [{ path: 'a.txt', oldText: null, newText: 'created' }] }), + }, + generic: { + name: 'generic', description: '', parameters: {}, execute: async () => [], + presentCall: () => ({ card: 'generic', title: 'Inspect value', rawInput: { alpha: 1 } }), + presentResult: () => ({ card: 'generic', title: 'Inspected', content: [{ type: 'text', text: 'result text' }] }), + }, + throwing: { + name: 'throwing', description: '', parameters: {}, execute: async () => [], + presentCall: () => { throw new Error('call presenter boom') }, + presentResult: () => { throw new Error('result presenter boom') }, + }, + rawTerminal: { + name: 'rawTerminal', description: '', parameters: {}, execute: async () => [], + presentCall: () => ({ card: 'terminal', title: 'raw command' }), + }, + undefinedViews: { + name: 'undefinedViews', description: '', parameters: {}, execute: async () => [], + presentCall: () => undefined, + presentResult: () => undefined, + }, + empty: { + name: 'empty', description: '', parameters: {}, execute: async () => [], + presentCall: () => ({ card: 'generic', title: 'Empty card' }), + }, + terminalResult: { + name: 'terminalResult', description: '', parameters: {}, execute: async () => [], + presentCall: () => ({ card: 'generic', title: 'Becomes terminal' }), + presentResult: () => ({ card: 'terminal', output: 'converted terminal' }), + }, + symbolic: { + name: 'symbolic', description: '', parameters: {}, execute: async () => [], + presentCall: () => ({ card: 'generic', title: 'Symbol input', rawInput: Symbol('input') }), + }, + } + + it('uses terminal, diff, generic, fallback, and collapsed tool presentations', async () => { + const result = await setup({ tools, config: { maxToolOutputLines: 1 } }) + const calls = [ + ['c1', 'bash', '{"command":"printf hello"}'], + ['c2', 'signal', '{}'], + ['c3', 'edit', '{}'], + ['c4', 'generic', '{}'], + ['c5', 'throwing', '{}'], + ['c6', 'unknown', 'not-json'], + ['c7', 'rawTerminal', '{"value":"raw"}'], + ['c8', 'undefinedViews', '{"value":8}'], + ['c10', 'empty', '{}'], + ['c11', 'terminalResult', '{}'], + ['c12', 'symbolic', '{}'], + ] as const + appendAssistant(result.session, [ + { type: 'text', text: 'Calling tools' }, + ...calls.map(([id, name, args]) => ({ + type: 'tool-call' as const, id: id as never, name, arguments: args, + })), + ]) + for (const [id, name, args] of calls) { + result.session.append('tool/call', { turn: 1, step: 0, callId: id as never, name, arguments: args }) + } + await tick() + expect(result.terminal.output).toContain('$ raw command') + result.terminal.send('/reasoning') + result.terminal.send('\r') + await tick() + expect(result.terminal.output).toContain('call presenter boom') + expect(result.terminal.output).toContain('Symbol(input)') + result.session.append('tool/result', { + turn: 1, step: 0, callId: 'c1' as never, content: [{ type: 'text', text: 'raw bash' }], isError: false, + }, { surfaceOp: 'append' }) + result.session.append('tool/result', { + turn: 1, step: 0, callId: 'c2' as never, content: [{ type: 'text', text: 'stopped' }], isError: true, + }, { surfaceOp: 'append' }) + result.session.append('tool/result', { + turn: 1, step: 0, callId: 'c3' as never, content: [{ type: 'text', text: 'done' }], isError: false, + }, { surfaceOp: 'append' }) + result.session.append('tool/result', { + turn: 1, step: 0, callId: 'c4' as never, content: [{ type: 'text', text: 'raw generic' }], isError: false, + }, { surfaceOp: 'append' }) + result.session.append('tool/result', { + turn: 1, step: 0, callId: 'c5' as never, content: [{ type: 'text', text: 'raw throwing' }], isError: false, + meta: { value: 1 }, + }, { surfaceOp: 'append' }) + result.session.append('tool/result', { + turn: 1, step: 0, callId: 'c7' as never, + content: [ + { type: 'tool-call', id: 'inner' as never, name: 'inner', arguments: '{}' }, + { type: 'tool-result', toolCallId: 'inner' as never, content: [{ type: 'text', text: 'nested output' }] }, + { type: 'future-result' } as never, + ], + isError: false, + }, { surfaceOp: 'append' }) + result.session.append('tool/result', { + turn: 1, step: 0, callId: 'c8' as never, content: [{ type: 'text', text: '\nundefined presenter output\n\nkept tail\n' }], isError: false, + }, { surfaceOp: 'append' }) + result.session.append('tool/result', { + turn: 1, step: 0, callId: 'c11' as never, content: [{ type: 'text', text: '\nconverted terminal\n\nfinished\n' }], isError: false, + }, { surfaceOp: 'append' }) + result.session.append('tool/result', { + turn: 1, step: 0, callId: 'orphan' as never, content: [{ type: 'text', text: 'orphan result' }], isError: false, + }, { surfaceOp: 'append' }) + await tick() + + const output = result.terminal.output + expect(output).toContain('Run command') + expect(output).toContain('printf hello') + expect(output).toContain('more lines') + expect(output).toContain('SIGTERM') + expect(output).toContain('Edit files') + expect(output).toContain('Inspected') + expect(output).toContain('result text') + expect(output).toContain('Presenter failed') + expect(output).toContain('not-json') + expect(output).toContain('nested output') + expect(output).toContain('[future-result]') + expect(output).toContain('undefined presenter output') + expect(output).toContain('Empty card') + expect(output).toContain('converted terminal') + expect(output).toContain('orphan result') + + result.terminal.send('/redraw') + result.terminal.send('\r') + await tick() + result.terminal.send('\x0f') + await tick() + expect(result.terminal.output).toContain('world') + expect(result.terminal.output).toContain('+ created') + await dispose(result) + }) + + it('rebuilds after a surface replacement and hides shadowed tool calls', async () => { + const result = await setup({ tools }) + appendUser(result.session, 'old prompt') + const assistant = result.session.append('assistant/message', { + turn: 1, + step: 0, + content: [{ type: 'tool-call', id: 'old-call' as never, name: 'bash', arguments: '{}' }], + }, { surfaceOp: 'append' }) + result.session.append('tool/call', { + turn: 1, step: 0, callId: 'old-call' as never, name: 'bash', arguments: '{}', + }) + const toolResult = result.session.append('tool/result', { + turn: 1, step: 0, callId: 'old-call' as never, content: [{ type: 'text', text: 'old output' }], isError: false, + }, { surfaceOp: 'append' }) + const start = result.session.surface.nodes[0]?.seq as number + result.session.append('context/message', { + content: [{ type: 'text', text: 'summary replacement' }], + source: { kind: 'plugin', plugin: 'compact' }, + }, { + surfaceOp: { op: 'replace', start, end: toolResult.seq }, + sourceEventSeqs: [start, assistant.seq, toolResult.seq], + }) + await tick() + + result.terminal.resize(89) + await tick() + const lastFullRender = result.terminal.output.slice(result.terminal.output.lastIndexOf('\x1b[2J')) + expect(lastFullRender).toContain('summary replacement') + expect(lastFullRender).not.toContain('old output') + await dispose(result) + }) +}) + +describe('TUI user-interaction dialogs', () => { + it('answers single-select, multi-select, custom, and optionless questions', async () => { + const result = await setup({ config: { maxQuestionOptions: 1 } }) + + const single = result.ctx.userInteraction.ask({ + questions: [{ + id: 'mode', header: 'Mode', question: 'Choose a mode', + options: [{ label: 'Safe', description: 'Use checks' }, { label: 'Fast' }], + }], + }) + await tick() + expect(result.terminal.output).toContain('Choose a mode') + expect(result.terminal.output).toContain('1/2') + result.terminal.send('\x1b[B') + result.terminal.send('\r') + await expect(single).resolves.toEqual({ answers: [{ id: 'mode', selected: ['Fast'] }] }) + + const multi = result.ctx.userInteraction.ask({ + questions: [{ id: 'targets', question: 'Pick targets', multiSelect: true, options: [{ label: 'Code' }, { label: 'Docs' }] }], + }) + await tick() + result.terminal.send(' ') + result.terminal.send('\x1b[B') + result.terminal.send(' ') + result.terminal.send('\r') + await expect(multi).resolves.toEqual({ answers: [{ id: 'targets', selected: ['Code', 'Docs'] }] }) + + const custom = result.ctx.userInteraction.ask({ + questions: [{ id: 'other', question: 'Choose or type', options: [{ label: 'Default' }] }], + }) + await tick() + result.terminal.send('c') + result.terminal.send('my choice') + result.terminal.send('\r') + await expect(custom).resolves.toEqual({ answers: [{ id: 'other', selected: [], custom: 'my choice' }] }) + + const free = result.ctx.userInteraction.ask({ questions: [{ id: 'note', question: 'Add a note' }] }) + await tick() + result.terminal.send('\r') + await tick() + expect(result.terminal.output).toContain('Enter an answer before submitting') + result.terminal.send('ship it') + result.terminal.send('\r') + await expect(free).resolves.toEqual({ answers: [{ id: 'note', selected: [], custom: 'ship it' }] }) + await dispose(result) + }) + + it('handles option wrapping, deselection errors, and returning from custom input', async () => { + const result = await setup({ config: { color: true } }) + const single = result.ctx.userInteraction.ask({ + questions: [{ id: 'single', question: 'Single options', options: [{ label: 'One' }, { label: 'Two' }] }], + }) + const singleRejected = expect(single).rejects.toMatchObject({ code: 'ASK_ABORTED' }) + await tick() + expect(result.terminal.output).toContain('Two') + result.terminal.send('\x03') + await singleRejected + + const answer = result.ctx.userInteraction.ask({ + questions: [{ + id: 'options', + question: 'Exercise options', + multiSelect: true, + options: [{ label: 'One', description: 'first' }, { label: 'Two' }], + }], + }) + const rejected = expect(answer).rejects.toMatchObject({ code: 'ASK_ABORTED' }) + await tick() + result.terminal.send('\x1b[A') + result.terminal.send('\x1b[B') + result.terminal.send('\x1b[B') + result.terminal.send('\x1b[A') + result.terminal.send(' ') + await tick() + result.terminal.send('x') + result.terminal.send(' ') + result.terminal.send('\r') + await tick() + expect(result.terminal.output).toContain('Select at least one option') + result.terminal.send('c') + await tick() + result.terminal.send('\x1b') + await tick() + expect(result.terminal.output).toContain('Space toggle') + result.terminal.send('\x03') + await rejected + await dispose(result) + }) + + it('asks batches in order and rejects cancelled or aborted work', async () => { + const result = await setup() + const preAborted = new AbortController() + preAborted.abort() + await expect(result.ctx.userInteraction.ask({ + questions: [{ id: 'pre-aborted', question: 'Already cancelled?' }], + signal: preAborted.signal, + })).rejects.toMatchObject({ code: 'ASK_ABORTED' }) + + const batch = result.ctx.userInteraction.ask({ + questions: [ + { id: 'first', question: 'First?', options: [{ label: 'Yes' }] }, + { id: 'second', question: 'Second?' }, + ], + }) + await tick() + result.terminal.send('\r') + await tick() + expect(result.terminal.output).toContain('Second?') + result.terminal.send('done') + result.terminal.send('\r') + await expect(batch).resolves.toEqual({ answers: [ + { id: 'first', selected: ['Yes'] }, + { id: 'second', selected: [], custom: 'done' }, + ] }) + + const cancelled = result.ctx.userInteraction.ask({ questions: [{ id: 'cancel', question: 'Cancel?' }] }) + const cancelledExpectation = expect(cancelled).rejects.toMatchObject({ code: 'ASK_ABORTED' }) + await tick() + result.terminal.send('\x1b') + await cancelledExpectation + + const controller = new AbortController() + const active = result.ctx.userInteraction.ask({ questions: [{ id: 'active', question: 'Active?' }], signal: controller.signal }) + const queuedController = new AbortController() + const queued = result.ctx.userInteraction.ask({ questions: [{ id: 'queued', question: 'Queued?' }], signal: queuedController.signal }) + const activeExpectation = expect(active).rejects.toMatchObject({ code: 'ASK_ABORTED' }) + const queuedExpectation = expect(queued).rejects.toMatchObject({ code: 'ASK_ABORTED' }) + await tick() + queuedController.abort() + controller.abort() + await activeExpectation + await queuedExpectation + await dispose(result) + }) + + it('rejects active and queued dialogs on disposal', async () => { + const result = await setup() + const active = result.ctx.userInteraction.ask({ questions: [{ id: 'active', question: 'Active?' }] }) + const queued = result.ctx.userInteraction.ask({ questions: [{ id: 'queued', question: 'Queued?' }] }) + const activeExpectation = expect(active).rejects.toMatchObject({ code: 'ASK_ABORTED' }) + const queuedExpectation = expect(queued).rejects.toMatchObject({ code: 'ASK_ABORTED' }) + await tick() + await result.controller.dispose() + await activeExpectation + await queuedExpectation + await expect(result.ctx.userInteraction.ask({ questions: [{ id: 'late', question: 'Late?' }] })) + .rejects.toMatchObject({ code: 'NO_PROVIDER' }) + await result.ctx.fiber.dispose() + }) +}) + +describe('terminal mounting', () => { + it('starts immediately when the configured agent already exists', async () => { + const ctx = new Context() + await ctx.plugin(SessionStore) + await ctx.plugin(AgentRegistry) + await ctx.plugin(UserInteractionService) + ctx.provide('tools', { get: () => undefined } as never) + const session = ctx.sessions.create(SessionId('mounted-session')) + ctx.agents.register({ + id: AgentId('main'), options: {}, session, status: 'idle', ctx, + send() {}, steer() {}, inject() {}, cancel() {}, whenIdle: () => Promise.resolve(), + }) + const terminal = new FakeTerminal() + mountTui(ctx, { color: false }, { terminal, exit: vi.fn() }) + await tick() + expect(terminal.started).toBe(1) + await ctx.fiber.dispose() + }) + + it('waits for its configured agent before starting the TUI', async () => { + const ctx = new Context() + await ctx.plugin(SessionStore) + await ctx.plugin(AgentRegistry) + await ctx.plugin(UserInteractionService) + ctx.provide('tools', { get: () => undefined } as never) + const terminal = new FakeTerminal() + mountTui(ctx, { agent: 'main', color: false }, { terminal, exit: vi.fn() }) + expect(terminal.started).toBe(0) + + const otherSession = ctx.sessions.create(SessionId('other-session')) + ctx.agents.register({ + id: AgentId('other'), options: {}, session: otherSession, status: 'idle', ctx, + send() {}, steer() {}, inject() {}, cancel() {}, whenIdle: () => Promise.resolve(), + }) + expect(terminal.started).toBe(0) + + const session = ctx.sessions.create(SessionId('late-session')) + const agent = { + id: AgentId('main'), options: {}, session, status: 'idle', ctx, + send() {}, steer() {}, inject() {}, cancel() {}, whenIdle: () => Promise.resolve(), + } as Agent + ctx.agents.register(agent) + await tick() + expect(terminal.started).toBe(1) + await ctx.fiber.dispose() + }) + + it('rolls back providers, listeners, and terminal state when startup fails', async () => { + const ctx = new Context() + await ctx.plugin(SessionStore) + await ctx.plugin(AgentRegistry) + await ctx.plugin(UserInteractionService) + ctx.provide('tools', { get: () => undefined } as never) + const session = ctx.sessions.create(SessionId('failed-start-session')) + ctx.agents.register({ + id: AgentId('main'), options: {}, session, status: 'running', ctx, + send() {}, steer() {}, inject() {}, cancel() {}, whenIdle: () => Promise.resolve(), + }) + const terminal = new FakeTerminal() + terminal.start = () => { throw new Error('terminal startup failed') } + + expect(() => createTuiChat(ctx, { color: false }, { terminal, exit: vi.fn() })) + .toThrow('terminal startup failed') + expect(terminal.stopped).toBe(1) + expect(terminal.progress).toEqual([false, true, false]) + await expect(ctx.userInteraction.ask({ questions: [{ id: 'late', question: 'Late?' }] })) + .rejects.toMatchObject({ code: 'NO_PROVIDER' }) + session.append('assistant/chunk', { + turn: 1, + step: 0, + chunk: { type: 'text-delta', index: 0, text: 'must not render' }, + }) + await tick() + expect(terminal.output).not.toContain('must not render') + await ctx.fiber.dispose() + }) + + it('throws when createTuiChat is called without the configured agent', async () => { + const ctx = new Context() + await ctx.plugin(AgentRegistry) + await ctx.plugin(UserInteractionService) + ctx.provide('tools', { get: () => undefined } as never) + const runtime: TuiRuntime = { terminal: new FakeTerminal(), exit: vi.fn() } + expect(() => createTuiChat(ctx, { agent: 'missing' }, runtime)).toThrow('is not running') + await ctx.fiber.dispose() + }) +}) diff --git a/packages/ui/tui/tsconfig.json b/packages/ui/tui/tsconfig.json new file mode 100644 index 0000000000..f3a85f76e4 --- /dev/null +++ b/packages/ui/tui/tsconfig.json @@ -0,0 +1,33 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "lib/types" + }, + "include": [ + "src" + ], + "references": [ + { + "path": "../../../vendor/cordis" + }, + { + "path": "../../../vendor/schemastery" + }, + { + "path": "../../core/agent" + }, + { + "path": "../../core/session" + }, + { + "path": "../../llm/llm" + }, + { + "path": "../../core/tools" + }, + { + "path": "../user-interaction" + } + ] +} diff --git a/packages/ui/user-interaction/README.md b/packages/ui/user-interaction/README.md index bd5c5281ca..b758ad09e0 100644 --- a/packages/ui/user-interaction/README.md +++ b/packages/ui/user-interaction/README.md @@ -21,7 +21,7 @@ When an answer includes `custom`, `selected` is empty; custom text is an overrid ## Role -This is the interface package. Model-facing consumers such as `@deepseek-ai/dsh-tool-ask-user` depend on this seam; UI front doors such as the `stdio-agent` readline module and the `acp` bridge provide the provider. The loop stays unchanged: a tool call awaits a promise, and the tool result resumes the normal agent loop. +This is the interface package. Model-facing consumers such as `@deepseek-ai/dsh-tool-ask-user` depend on this seam; UI front doors such as the interactive `dsh-tui`, line-oriented `dsh-stdio`, and structured `dsh-acp` channels provide the provider. The loop stays unchanged: a tool call awaits a promise, and the tool result resumes the normal agent loop. ## Model Experience diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b1a3a78030..e4a9019a7f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -583,6 +583,9 @@ importers: '@deepseek-ai/dsh-tools': specifier: workspace:^ version: link:../../core/tools + '@deepseek-ai/dsh-tui': + specifier: workspace:^ + version: link:../../ui/tui '@deepseek-ai/dsh-user-interaction': specifier: workspace:^ version: link:../../ui/user-interaction @@ -1673,6 +1676,37 @@ importers: specifier: ^4.0.0-rc.7 version: 4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.5) + packages/ui/tui: + dependencies: + '@earendil-works/pi-tui': + specifier: 0.80.7 + version: 0.80.7 + schemastery: + specifier: ^3.18.0 + version: 3.18.0 + devDependencies: + '@cordisjs/plugin-loader': + specifier: workspace:^ + version: link:../../../vendor/loader + '@deepseek-ai/dsh-agent': + specifier: workspace:^ + version: link:../../core/agent + '@deepseek-ai/dsh-llm': + specifier: workspace:^ + version: link:../../llm/llm + '@deepseek-ai/dsh-session': + specifier: workspace:^ + version: link:../../core/session + '@deepseek-ai/dsh-tools': + specifier: workspace:^ + version: link:../../core/tools + '@deepseek-ai/dsh-user-interaction': + specifier: workspace:^ + version: link:../user-interaction + cordis: + specifier: ^4.0.0-rc.6 + version: 4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@vendor+loader) + packages/ui/user-approval: dependencies: schemastery: @@ -2519,6 +2553,10 @@ packages: engines: {node: '>=22.19.0'} hasBin: true + '@earendil-works/pi-tui@0.80.7': + resolution: {integrity: sha512-1B2++fLZfgI3XMzW2BTpuDuam2uyHnUUEmsOvi5R0Ne9RAt59WjFV0G8ozX6l1Xafa9P5Y3eT4aDtRr/v/CUTA==} + engines: {node: '>=22.19.0'} + '@emnapi/core@1.10.0': resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} @@ -4293,6 +4331,10 @@ packages: resolution: {integrity: sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==} engines: {node: '>=18'} + get-east-asian-width@1.6.0: + resolution: {integrity: sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==} + engines: {node: '>=18'} + get-intrinsic@1.3.0: resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} engines: {node: '>= 0.4'} @@ -4773,6 +4815,11 @@ packages: engines: {node: '>= 20'} hasBin: true + marked@18.0.5: + resolution: {integrity: sha512-S6GcvALHg6K4ohtu4E7x0a1AqhAjp6cV8KhLSyN9qVapnzJkusVBxZRcIU9AeYsbe6P1hKDusSbEOzGyyuce6w==} + engines: {node: '>= 20'} + hasBin: true + math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} @@ -6157,6 +6204,11 @@ snapshots: - ws - zod + '@earendil-works/pi-tui@0.80.7': + dependencies: + get-east-asian-width: 1.6.0 + marked: 18.0.5 + '@emnapi/core@1.10.0': dependencies: '@emnapi/wasi-threads': 1.2.1 @@ -7861,6 +7913,8 @@ snapshots: transitivePeerDependencies: - supports-color + get-east-asian-width@1.6.0: {} + get-intrinsic@1.3.0: dependencies: call-bind-apply-helpers: 1.0.2 @@ -8308,6 +8362,8 @@ snapshots: marked@16.4.2: {} + marked@18.0.5: {} + math-intrinsics@1.1.0: {} mdast-util-find-and-replace@3.0.2: diff --git a/scripts/gen-doc-graphs.ts b/scripts/gen-doc-graphs.ts index 31df485e1d..cbe9272ac1 100644 --- a/scripts/gen-doc-graphs.ts +++ b/scripts/gen-doc-graphs.ts @@ -407,7 +407,7 @@ const APP_EXAMPLES = [ title: 'Coding Agent App Composition', label: 'examples/coding-agent', config: 'examples/coding-agent/cordis.yml', - summary: 'The coding REPL demo adds the real DeepSeek adapter, filesystem tools, todo_write, compaction, and both subagent transports on top of the stdio app package.', + summary: 'The coding-agent demo adds the real DeepSeek adapter, filesystem tools, todo_write, compaction, and both subagent transports on top of the stdio app package.', }, { id: 'cordis', @@ -435,7 +435,7 @@ function renderAppExpansion(lines: string[], appNode: string, pluginName: string lines.push(` ${appNode} --> ${agentCore}["@deepseek-ai/dsh-agent-spine-demo"]`) lines.push(` ${appNode} --> ${jsonl}["@deepseek-ai/dsh-session-persistence-jsonl"]`) if (pluginName === '@deepseek-ai/dsh-stdio-demo') { - lines.push(` ${appNode} --> ${nodeId('frontdoor', 'stdio')}["readline UI
console logger
pre-created main agent"]`) + lines.push(` ${appNode} --> ${nodeId('frontdoor', 'stdio')}["dsh-tui (TTY) / dsh-stdio (pipes)
pre-created main agent"]`) } else if (pluginName === '@deepseek-ai/dsh-acp-demo') { lines.push(` ${appNode} --> ${nodeId('frontdoor', 'acp')}["@deepseek-ai/dsh-acp
JSON-RPC stdio bridge
sessions created by client"]`) } diff --git a/tsconfig.build.json b/tsconfig.build.json index 3a57169005..7655069a29 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -61,6 +61,7 @@ { "path": "./packages/ui/app-boot" }, { "path": "./packages/ui/jsonrpc" }, { "path": "./packages/examples/jsonrpc-demo" }, + { "path": "./packages/ui/tui" }, { "path": "./packages/ui/stdio" }, { "path": "./packages/examples/stdio-demo" }, { "path": "./packages/support/llm-replay" }, diff --git a/tsconfig.json b/tsconfig.json index 6585a987d6..1f18ea3a55 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -72,6 +72,7 @@ { "path": "./packages/ui/app-boot" }, { "path": "./packages/ui/jsonrpc" }, { "path": "./packages/examples/jsonrpc-demo" }, + { "path": "./packages/ui/tui" }, { "path": "./packages/ui/stdio" }, { "path": "./packages/examples/stdio-demo" }, { "path": "./packages/support/llm-replay" }, From 12b7ac67bacd1b05e76b124163d195d9bbbde72a Mon Sep 17 00:00:00 2001 From: Turtle Date: Fri, 17 Jul 2026 16:44:25 +0800 Subject: [PATCH 02/13] fix(ui): surface declarative startup failures --- docs/cordis-catalog/events.md | 32 ++++-- docs/cordis-catalog/services.md | 4 +- docs/event-producer-consumer.md | 25 +++-- examples/coding-agent/README.md | 2 +- .../tests/tui-keyless-smoke.e2e.ts | 33 ++++-- .../cordis/tool-cordis/src/api-catalog.ts | 8 ++ packages/core/agent-loop/README.md | 2 +- packages/core/agent-loop/src/index.ts | 17 ++- .../tests/config-session-id.spec.ts | 80 ++++++++++++- packages/core/agent/README.md | 7 +- packages/core/agent/src/index.ts | 95 ++++++++++++++++ packages/core/agent/src/types.ts | 9 ++ packages/core/agent/tests/agent.spec.ts | 106 +++++++++++++++++- packages/examples/stdio-demo/README.md | 4 +- packages/ui/stdio/README.md | 2 +- packages/ui/stdio/src/index.ts | 16 ++- packages/ui/stdio/tests/stdio.spec.ts | 38 ++++++- packages/ui/tui/README.md | 2 +- packages/ui/tui/src/index.ts | 29 +++-- packages/ui/tui/tests/tui.spec.ts | 45 ++++++++ 20 files changed, 484 insertions(+), 72 deletions(-) diff --git a/docs/cordis-catalog/events.md b/docs/cordis-catalog/events.md index ede449cb1c..ed0a5554e5 100644 --- a/docs/cordis-catalog/events.md +++ b/docs/cordis-catalog/events.md @@ -47,7 +47,7 @@ A step or turn errored. The loop reports a failure here (plus the logger) even w Types: [Agent](../core-data-structures/core.md) -Source: [`packages/core/agent/src/types.ts:283`](../../packages/core/agent/src/types.ts) +Source: [`packages/core/agent/src/types.ts:292`](../../packages/core/agent/src/types.ts) ### `agent/pre-step` — serial @@ -59,7 +59,7 @@ Awaited serial checkpoint for session-surface mutation after prompt assembly and Types: [Agent](../core-data-structures/core.md) · [Message](../core-data-structures/core.md) -Source: [`packages/core/agent/src/types.ts:202`](../../packages/core/agent/src/types.ts) +Source: [`packages/core/agent/src/types.ts:211`](../../packages/core/agent/src/types.ts) ### `agent/prompt-submit` — waterfall @@ -71,7 +71,7 @@ Allow, rewrite, or block one drained prompt before it becomes a user message. Ca Types: [Agent](../core-data-structures/core.md) · [ContentBlock](../core-data-structures/core.md) · [MessageSource](../core-data-structures/core.md) -Source: [`packages/core/agent/src/types.ts:212`](../../packages/core/agent/src/types.ts) +Source: [`packages/core/agent/src/types.ts:221`](../../packages/core/agent/src/types.ts) ### `agent/queued` — emit @@ -83,7 +83,7 @@ Detached, frozen content entered the agent's inbox. Source defaults have already Types: [Agent](../core-data-structures/core.md) · [ContentBlock](../core-data-structures/core.md) · [MessageSource](../core-data-structures/core.md) -Source: [`packages/core/agent/src/types.ts:167`](../../packages/core/agent/src/types.ts) +Source: [`packages/core/agent/src/types.ts:176`](../../packages/core/agent/src/types.ts) ### `agent/request` — waterfall @@ -95,7 +95,7 @@ Replace the frozen call configuration. Model-visible content must use logged cha Types: [Agent](../core-data-structures/core.md) · [LlmCallConfig](../core-data-structures/core.md) -Source: [`packages/core/agent/src/types.ts:224`](../../packages/core/agent/src/types.ts) +Source: [`packages/core/agent/src/types.ts:233`](../../packages/core/agent/src/types.ts) ### `agent/session-prefix` — waterfall @@ -107,7 +107,7 @@ Compose request-only messages placed before derived history. The frozen result i Types: [Agent](../core-data-structures/core.md) · [Message](../core-data-structures/core.md) -Source: [`packages/core/agent/src/types.ts:239`](../../packages/core/agent/src/types.ts) +Source: [`packages/core/agent/src/types.ts:248`](../../packages/core/agent/src/types.ts) ### `agent/session-start` — emit @@ -119,7 +119,17 @@ The session lifecycle began, once before the first turn. Use `agent.inject()` to Types: [Agent](../core-data-structures/core.md) · [SessionStartSource](../core-data-structures/core.md) -Source: [`packages/core/agent/src/types.ts:180`](../../packages/core/agent/src/types.ts) +Source: [`packages/core/agent/src/types.ts:189`](../../packages/core/agent/src/types.ts) + +### `agent/start-failed` — emit + +Declarative startup failed before an agent could be published. Programmatic `ctx.agents.create()` / `resume()` calls report failure through rejection; this event covers the fire-and-forget config path. + +```ts cordis-catalog +'agent/start-failed'(agentId: AgentId, error: Error): void +``` + +Source: [`packages/core/agent/src/types.ts:157`](../../packages/core/agent/src/types.ts) ### `agent/status` — emit @@ -131,7 +141,7 @@ Agent status changed (`idle` ⇄ `running`, or → `disposed`). `send()` does no Types: [Agent](../core-data-structures/core.md) -Source: [`packages/core/agent/src/types.ts:157`](../../packages/core/agent/src/types.ts) +Source: [`packages/core/agent/src/types.ts:166`](../../packages/core/agent/src/types.ts) ### `agent/step-result` — waterfall @@ -143,7 +153,7 @@ Waterfall: post-process the assembled assistant Message before tool dispatch (va Types: [Agent](../core-data-structures/core.md) · [Message](../core-data-structures/core.md) -Source: [`packages/core/agent/src/types.ts:250`](../../packages/core/agent/src/types.ts) +Source: [`packages/core/agent/src/types.ts:259`](../../packages/core/agent/src/types.ts) ### `agent/turn-continuation` — waterfall @@ -155,7 +165,7 @@ Override whether the turn continues. The default continues after tool calls or s Types: [Agent](../core-data-structures/core.md) -Source: [`packages/core/agent/src/types.ts:260`](../../packages/core/agent/src/types.ts) +Source: [`packages/core/agent/src/types.ts:269`](../../packages/core/agent/src/types.ts) ### `agent/turn-stop` — serial @@ -167,7 +177,7 @@ Monotonic terminal-stop checkpoint after continuation and steering are folded; a Types: [Agent](../core-data-structures/core.md) -Source: [`packages/core/agent/src/types.ts:270`](../../packages/core/agent/src/types.ts) +Source: [`packages/core/agent/src/types.ts:279`](../../packages/core/agent/src/types.ts) ## `approval/*` diff --git a/docs/cordis-catalog/services.md b/docs/cordis-catalog/services.md index 18e00b7904..aa2289ff0d 100644 --- a/docs/cordis-catalog/services.md +++ b/docs/cordis-catalog/services.md @@ -32,13 +32,15 @@ async resume(options: ResumeAgentOptions): Promise register(agent: Agent): () => void enter(agent: Agent): () => void announce(agent: Agent): void +reportStartFailure(id: AgentId, error: Error): () => void +getStartFailure(id: AgentId): Error | undefined get(id: AgentId): Agent | undefined list(): Agent[] ``` Types: [Agent](../core-data-structures/core.md) -Source: [`packages/core/agent/src/index.ts:133`](../../packages/core/agent/src/index.ts) +Source: [`packages/core/agent/src/index.ts:187`](../../packages/core/agent/src/index.ts) ## `ctx.approval` — `ApprovalService` diff --git a/docs/event-producer-consumer.md b/docs/event-producer-consumer.md index b67788ea6b..de932e0999 100644 --- a/docs/event-producer-consumer.md +++ b/docs/event-producer-consumer.md @@ -7,19 +7,20 @@ This matrix shows which packages dispatch each harness-owned event and which pac | Event | Mode | Declared in | Dispatchers | Listeners | | --- | --- | --- | --- | --- | -| `agent/created` | `emit` | [`packages/core/agent/src/types.ts:139`](../packages/core/agent/src/types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`jsonrpc`](../packages/ui/jsonrpc), [`stdio`](../packages/ui/stdio), [`tui`](../packages/ui/tui) | +| `agent/created` | `emit` | [`packages/core/agent/src/types.ts:139`](../packages/core/agent/src/types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent`](../packages/core/agent), [`jsonrpc`](../packages/ui/jsonrpc), [`stdio`](../packages/ui/stdio) | | `agent/disposed` | `emit` | [`packages/core/agent/src/types.ts:148`](../packages/core/agent/src/types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`stdio`](../packages/ui/stdio), [`tui`](../packages/ui/tui) | -| `agent/error` | `emit` | [`packages/core/agent/src/types.ts:283`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`tui`](../packages/ui/tui) | -| `agent/pre-step` | `serial` | [`packages/core/agent/src/types.ts:202`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`compact-basic`](../packages/compact/compact-basic), [`user-approval`](../packages/ui/user-approval) | -| `agent/prompt-submit` | `waterfall` | [`packages/core/agent/src/types.ts:212`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`acp`](../packages/ui/acp), [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex), [`repeat-tool-guard`](../packages/guard/repeat-tool-guard) | -| `agent/queued` | `emit` | [`packages/core/agent/src/types.ts:167`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | - | -| `agent/request` | `waterfall` | [`packages/core/agent/src/types.ts:224`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | - | -| `agent/session-prefix` | `waterfall` | [`packages/core/agent/src/types.ts:239`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`tool-skill`](../packages/skill/tool-skill) | -| `agent/session-start` | `emit` | [`packages/core/agent/src/types.ts:180`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex) | -| `agent/status` | `emit` | [`packages/core/agent/src/types.ts:157`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`invariants`](../packages/support/invariants), [`repeat-tool-guard`](../packages/guard/repeat-tool-guard), [`stdio`](../packages/ui/stdio), [`tui`](../packages/ui/tui) | -| `agent/step-result` | `waterfall` | [`packages/core/agent/src/types.ts:250`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | - | -| `agent/turn-continuation` | `waterfall` | [`packages/core/agent/src/types.ts:260`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex) | -| `agent/turn-stop` | `serial` | [`packages/core/agent/src/types.ts:270`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`subagent-inprocess`](../packages/subagent/subagent-inprocess) | +| `agent/error` | `emit` | [`packages/core/agent/src/types.ts:292`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`tui`](../packages/ui/tui) | +| `agent/pre-step` | `serial` | [`packages/core/agent/src/types.ts:211`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`compact-basic`](../packages/compact/compact-basic), [`user-approval`](../packages/ui/user-approval) | +| `agent/prompt-submit` | `waterfall` | [`packages/core/agent/src/types.ts:221`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`acp`](../packages/ui/acp), [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex), [`repeat-tool-guard`](../packages/guard/repeat-tool-guard) | +| `agent/queued` | `emit` | [`packages/core/agent/src/types.ts:176`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | - | +| `agent/request` | `waterfall` | [`packages/core/agent/src/types.ts:233`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | - | +| `agent/session-prefix` | `waterfall` | [`packages/core/agent/src/types.ts:248`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`tool-skill`](../packages/skill/tool-skill) | +| `agent/session-start` | `emit` | [`packages/core/agent/src/types.ts:189`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex) | +| `agent/start-failed` | `emit` | [`packages/core/agent/src/types.ts:157`](../packages/core/agent/src/types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent`](../packages/core/agent) | +| `agent/status` | `emit` | [`packages/core/agent/src/types.ts:166`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`invariants`](../packages/support/invariants), [`repeat-tool-guard`](../packages/guard/repeat-tool-guard), [`stdio`](../packages/ui/stdio), [`tui`](../packages/ui/tui) | +| `agent/step-result` | `waterfall` | [`packages/core/agent/src/types.ts:259`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | - | +| `agent/turn-continuation` | `waterfall` | [`packages/core/agent/src/types.ts:269`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex) | +| `agent/turn-stop` | `serial` | [`packages/core/agent/src/types.ts:279`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`subagent-inprocess`](../packages/subagent/subagent-inprocess) | | `approval/request` | `waterfall` | [`packages/ui/user-approval/src/index.ts:31`](../packages/ui/user-approval/src/index.ts) | [`user-approval`](../packages/ui/user-approval) (`waterfall`) | [`acp`](../packages/ui/acp) | | `fs/edit-intent` | `waterfall` | [`packages/fs/fs/src/index.ts:59`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`waterfall`) | [`fs-policy`](../packages/fs/fs-policy) | | `fs/observed` | `emit` | [`packages/fs/fs/src/index.ts:68`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`emit`) | [`fs-policy`](../packages/fs/fs-policy) | diff --git a/examples/coding-agent/README.md b/examples/coding-agent/README.md index c1c4747a22..9fdadf809d 100644 --- a/examples/coding-agent/README.md +++ b/examples/coding-agent/README.md @@ -23,7 +23,7 @@ Each run starts a fresh session by default (its event log lands under `./.sessio RESUME_SESSION_ID= pnpm run demo:repl ``` -The id is wired through `cordis.yml` (`resumeSessionId: !!js process.env.RESUME_SESSION_ID`); unset, the agent starts a new session. A missing or unreadable id starts no `main` agent, so unset it or choose an existing session id. +The id is wired through `cordis.yml` (`resumeSessionId: !!js process.env.RESUME_SESSION_ID`); unset, the agent starts a new session. A missing or unreadable id starts no `main` agent and emits `agent/start-failed`; both terminal front doors print the failure and exit nonzero, so unset it or choose an existing session id. ## Code Mode diff --git a/examples/coding-agent/tests/tui-keyless-smoke.e2e.ts b/examples/coding-agent/tests/tui-keyless-smoke.e2e.ts index a1057ba02f..f5f9c5db36 100644 --- a/examples/coding-agent/tests/tui-keyless-smoke.e2e.ts +++ b/examples/coding-agent/tests/tui-keyless-smoke.e2e.ts @@ -13,7 +13,7 @@ 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 = sys.argv[1:] +node, tsx_loader, bin_script, config_path, tsconfig_path, cwd, resume_session_id = sys.argv[1:] env = os.environ.copy() env.update({ "DEEPSEEK_API_KEY": "keyless-tui-no-call", @@ -21,6 +21,8 @@ env.update({ "DSH_AGENTS_HOME": os.path.join(cwd, ".agents"), "TSX_TSCONFIG_PATH": tsconfig_path, }) +if resume_session_id: + env["RESUME_SESSION_ID"] = resume_session_id pid, fd = pty.fork() if pid == 0: os.chdir(cwd) @@ -53,15 +55,23 @@ if status is None: os.kill(pid, signal.SIGKILL) _, status = os.waitpid(pid, 0) sys.stdout.buffer.write(output) -if not sent_exit: - sys.stderr.write("TUI did not render its welcome marker before timeout\n") - sys.exit(124) -if not os.WIFEXITED(status) or os.WEXITSTATUS(status) != 0: - sys.stderr.write("TUI child did not exit cleanly\n") - sys.exit(125) +if resume_session_id: + 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) +else: + if not sent_exit: + sys.stderr.write("TUI did not render its welcome marker before timeout\n") + sys.exit(124) + if not os.WIFEXITED(status) or os.WEXITSTATUS(status) != 0: + sys.stderr.write("TUI child did not exit cleanly\n") + sys.exit(125) ` -async function runTuiLoaderSmoke(): Promise { +async function runTuiLoaderSmoke(resumeSessionId = ''): Promise { const cwd = await mkdtemp(join(tmpdir(), 'coding-tui-smoke-')) try { return await new Promise((resolve, reject) => { @@ -74,6 +84,7 @@ async function runTuiLoaderSmoke(): Promise { configPath, tsconfigPath, cwd, + resumeSessionId, ], { stdio: ['ignore', 'pipe', 'pipe'] }) let stdout = '' let stderr = '' @@ -98,4 +109,10 @@ describe('coding-agent TUI keyless smoke (real Loader tree in a PTY)', () => { expect(output).toContain('DEEPSEEK') expect(output).toContain('agent REPL ready.') }, 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') + expect(output).toContain('ui-tui: agent "main" failed to start:') + expect(output).toContain('missing-session') + }, LOADER_SMOKE_TEST_TIMEOUT_MS) }) diff --git a/packages/cordis/tool-cordis/src/api-catalog.ts b/packages/cordis/tool-cordis/src/api-catalog.ts index 51b4a5155f..1eef429d96 100644 --- a/packages/cordis/tool-cordis/src/api-catalog.ts +++ b/packages/cordis/tool-cordis/src/api-catalog.ts @@ -71,6 +71,8 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [ 'register(agent: Agent): () => void', 'enter(agent: Agent): () => void', 'announce(agent: Agent): void', + 'reportStartFailure(id: AgentId, error: Error): () => void', + 'getStartFailure(id: AgentId): Error | undefined', 'get(id: AgentId): Agent | undefined', 'list(): Agent[]', ], @@ -317,6 +319,12 @@ export const EVENT_API: readonly EventApiEntry[] = [ signature: '\'agent/session-start\'(this: Scoped, agent: Agent, source: SessionStartSource): void', summary: 'The session lifecycle began, once before the first turn.', }, + { + name: 'agent/start-failed', + mode: 'emit', + signature: '\'agent/start-failed\'(agentId: AgentId, error: Error): void', + summary: 'Declarative startup failed before an agent could be published.', + }, { name: 'agent/status', mode: 'emit', diff --git a/packages/core/agent-loop/README.md b/packages/core/agent-loop/README.md index a07aa5fa70..d46aabac0d 100644 --- a/packages/core/agent-loop/README.md +++ b/packages/core/agent-loop/README.md @@ -79,6 +79,6 @@ Everything that goes beyond "call the model, run the tools, repeat" belongs to p ## Known Limitations and Deferred Work - **Tool calls within a step execute sequentially** — parallel execution waits on concurrency-safety metadata in the tool contract (see `dsh-tools`). -- **No resume-or-create policy on the config path** — config-driven `create()` starts a fresh `${id}-session-` every run (`TODO(demo)`), and a config `resumeSessionId` whose resume fails logs a warning and creates no agent. +- **No resume-or-create policy on the config path** — config-driven `create()` starts a fresh `${id}-session-` every run (`TODO(demo)`), and a config `resumeSessionId` whose resume fails logs a warning, emits and retains `agent/start-failed` while that declaration remains loaded, and creates no agent. - **Config agents have no per-agent persona field or setup hook** — they use the deployment persona; scoped persona/tool composition is available only through the programmatic `ctx.agents.create()` / `resume()` factory options. - **No built-in turn budget** — the default continuation is `continue` whenever a step had tool calls or steering; bounding a runaway turn requires an `agent/turn-continuation` force-stop plugin. diff --git a/packages/core/agent-loop/src/index.ts b/packages/core/agent-loop/src/index.ts index 6cd66f622c..6d3ef8851a 100644 --- a/packages/core/agent-loop/src/index.ts +++ b/packages/core/agent-loop/src/index.ts @@ -363,18 +363,29 @@ export class AgentLoop extends Service implements AgentFactory { this.create(id, options, cwd === undefined ? {} : { cwd }) continue } - ctx.effect(() => { + ctx.effect(function* (this: AgentLoop) { + let active = true + let releaseFailure = (): void => {} const fiber = ctx.inject(['sessionPersistence'], (childCtx: Context) => { void this.resumeWith(ctx, childCtx.sessionPersistence, { agentId: id, resumeSessionId, agentOptions: options, }).catch((error: unknown) => { + if (!active) return + const failure = new Error(error instanceof Error ? error.message : String(error), { cause: error }) ctx.logger.warn(`agent "${id}": config-driven resume of "${resumeSessionId}" failed: ${String(error)}`) + releaseFailure = ctx.agents.reportStartFailure(id, failure) }) }) - return fiber.dispose - }, `agentLoop.resume(${id})`) + yield fiber.dispose + // Yielded last, disposed first: suppress teardown rejection before the + // deferred persistence child wakes and clear any retained record. + yield () => { + active = false + releaseFailure() + } + }.bind(this), `agentLoop.resume(${id})`) } } diff --git a/packages/core/agent-loop/tests/config-session-id.spec.ts b/packages/core/agent-loop/tests/config-session-id.spec.ts index 8a5d9ce264..f91c7f6585 100644 --- a/packages/core/agent-loop/tests/config-session-id.spec.ts +++ b/packages/core/agent-loop/tests/config-session-id.spec.ts @@ -4,10 +4,11 @@ import { mkdtemp, rm } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' import LlmService from '@deepseek-ai/dsh-llm' -import SessionStore, { SessionId } from '@deepseek-ai/dsh-session' +import SessionStore, { SessionId, type SessionEvent, type SessionHeader } from '@deepseek-ai/dsh-session' import SystemPrompt from '@deepseek-ai/dsh-system-prompt' import ToolRegistry from '@deepseek-ai/dsh-tools' import AgentRegistry, { AgentId } from '@deepseek-ai/dsh-agent' +import SessionPersistence from '@deepseek-ai/dsh-session-persistence' import SessionPersistenceJsonl from '@deepseek-ai/dsh-session-persistence-jsonl' import AgentLoop, { ReactLoopAgent } from '@deepseek-ai/dsh-agent-loop' import { MockAdapter, textResponse } from './mock-adapter.ts' @@ -42,6 +43,72 @@ describe('config-driven session id', () => { await loopFiber.dispose() }) + it('drops an in-flight declarative resume when its owner is disposed', async () => { + const loadStarted = Promise.withResolvers() + const pendingLoad = Promise.withResolvers<{ meta: SessionHeader; events: SessionEvent[] }>() + class DeferredSessionPersistence extends SessionPersistence { + create(_meta: SessionHeader): Promise { return Promise.resolve() } + append(_id: SessionId, _events: readonly SessionEvent[]): Promise { return Promise.resolve() } + load(_id: SessionId): Promise<{ meta: SessionHeader; events: SessionEvent[] }> { + loadStarted.resolve(undefined) + return pendingLoad.promise + } + list(): Promise { return Promise.resolve([]) } + } + + const ctx = new Context() + await ctx.plugin(LlmService) + await ctx.plugin(SessionStore) + await ctx.plugin(SystemPrompt) + await ctx.plugin(ToolRegistry) + await ctx.plugin(AgentRegistry) + const failures: Error[] = [] + ctx.on('agent/start-failed', (_id, error) => { failures.push(error) }) + const loopFiber = await ctx.plugin(AgentLoop, { + agents: [{ id: AgentId('main'), model: 'mock', resumeSessionId: SessionId('deferred') }], + }) + await ctx.plugin(DeferredSessionPersistence) + await loadStarted.promise + + await loopFiber.dispose() + await Promise.resolve() + expect(failures).toEqual([]) + expect(ctx.agents.getStartFailure(AgentId('main'))).toBeUndefined() + await ctx.fiber.dispose() + }) + + it('normalizes a non-Error declarative resume rejection without duplicating an Error prefix', async () => { + class RejectingSessionPersistence extends SessionPersistence { + create(_meta: SessionHeader): Promise { return Promise.resolve() } + append(_id: SessionId, _events: readonly SessionEvent[]): Promise { return Promise.resolve() } + load(_id: SessionId): Promise<{ meta: SessionHeader; events: SessionEvent[] }> { + // Third-party backends can reject arbitrary values; this exercises normalization. + // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors + return Promise.reject('plain failure') + } + list(): Promise { return Promise.resolve([]) } + } + + const ctx = new Context() + await ctx.plugin(LlmService) + await ctx.plugin(SessionStore) + await ctx.plugin(SystemPrompt) + await ctx.plugin(ToolRegistry) + await ctx.plugin(AgentRegistry) + const failures: Error[] = [] + ctx.on('agent/start-failed', (_id, error) => { failures.push(error) }) + const loopFiber = await ctx.plugin(AgentLoop, { + agents: [{ id: AgentId('main'), model: 'mock', resumeSessionId: SessionId('rejected') }], + }) + await ctx.plugin(RejectingSessionPersistence) + + await vi.waitFor(() => { expect(failures).toHaveLength(1) }) + expect(failures[0]?.message).toBe('plain failure') + expect(failures[0]?.cause).toBe('plain failure') + await loopFiber.dispose() + await ctx.fiber.dispose() + }) + it('config-driven create uses a fresh ${id}-session- per run (restart-safe)', async () => { const root = await mkdtemp(join(tmpdir(), 'dsh-cfg-session-')) dirs.push(root) @@ -137,7 +204,9 @@ describe('config-driven session id', () => { await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRegistry) await ctx.plugin(AgentRegistry) - await ctx.plugin(AgentLoop, { agents: [{ id: AgentId('main'), model: 'mock', resumeSessionId: SessionId('does-not-exist') }] }) + const failures: Array<{ id: string; error: Error }> = [] + ctx.on('agent/start-failed', (id, error) => { failures.push({ id, error }) }) + const loopFiber = await ctx.plugin(AgentLoop, { agents: [{ id: AgentId('main'), model: 'mock', resumeSessionId: SessionId('does-not-exist') }] }) const warn = vi.spyOn((ctx.agentLoop as unknown as { ctx: { logger: { warn: (...a: unknown[]) => void } } }).ctx.logger, 'warn') .mockImplementation(() => undefined) await ctx.plugin(SessionPersistenceJsonl, { root }) @@ -148,6 +217,13 @@ describe('config-driven session id', () => { await new Promise(r => setTimeout(r, 200)) expect(ctx.agents.get(AgentId('main'))).toBeUndefined() expect(warn).toHaveBeenCalledWith(expect.stringContaining('config-driven resume of "does-not-exist" failed')) + expect(failures).toHaveLength(1) + expect(failures[0]?.id).toBe('main') + expect(failures[0]?.error.message).toBe('session "does-not-exist" not found') + expect(failures[0]?.error.cause).toBeInstanceOf(Error) + expect(ctx.agents.getStartFailure(AgentId('main'))).toBe(failures[0]?.error) + await loopFiber.dispose() + expect(ctx.agents.getStartFailure(AgentId('main'))).toBeUndefined() warn.mockRestore() await ctx.fiber.dispose() }) diff --git a/packages/core/agent/README.md b/packages/core/agent/README.md index 5639f30daf..ed91da24ce 100644 --- a/packages/core/agent/README.md +++ b/packages/core/agent/README.md @@ -14,6 +14,9 @@ Tracks live agents so UI, hook, and orchestrator plugins can find them without i - Advanced factory lifecycle: `enter(agent)` publishes without announcing and returns an entry-bound detach; `announce(agent)` emits creation once. Detach during creation dispatch is deferred. Ordinary plugins use `register()`. - `ctx.agents.get(id: AgentId): Agent | undefined` - `ctx.agents.list(): Agent[]` +- `ctx.agents.reportStartFailure(id, error): () => void` announces and retains a declarative startup failure; the disposer clears that exact record without deleting a newer replacement. +- `ctx.agents.getStartFailure(id: AgentId): Error | undefined` returns a retained declarative startup failure for a configured id that never became live. Successful publication clears it. +- `observeAgentStart(ctx, id, handlers)` observes publication or declarative startup failure, including the retained-failure race for late-mounted front doors; its listeners belong to `ctx` and the returned disposer cancels the observation. #### Factory seam (creation) @@ -29,7 +32,7 @@ The loop plugin registers `AgentFactory`, keeping consumers independent of its c `dsh-agent` declares the live `agent/*` coordination vocabulary so plugins do not depend on the concrete loop. Exact signatures, dispatch modes, scope-filtering rules, and payload contracts live in the generated [Cordis event catalog](../../../docs/cordis-catalog/events.md); the [architecture turn flow](../../../docs/architecture.md#turn-flow) shows their order relative to durable session events. -`agent/created` runs after setup and both registry entries; the following `agent/session-start` is the first supported startup injection point. `agent/disposed` means the exact entry left the registry. The loop quiesces its driver first; directly registered custom agents own any stronger ordering. +`agent/created` runs after setup and both registry entries; the following `agent/session-start` is the first supported startup injection point. `agent/disposed` means the exact entry left the registry. `agent/start-failed` reports a contained declarative startup failure before publication; programmatic factory calls report failures through rejection. The loop quiesces its driver first; directly registered custom agents own any stronger ordering. Most interception points are cooperative waterfalls returning seam-specific decisions. `agent/pre-step` is a serial surface-mutation checkpoint, while `agent/turn-stop` is the terminal serial fold: it runs after ordinary continuation and steering folding, and a returned stop remains in force through turn close and flush so later steering cannot create an extra step or turn. Ordinary queued prompts remain intact. The full rationale is in the [agent-scope runtime-design RFC](../../../docs/rfc/implemented/architecture/2026-07-12-agent-scope-runtime-design.md#three-execution-boundaries-are-deliberately-one-way). @@ -69,7 +72,7 @@ The handle every plugin programs against: ## Known Limitations and Deferred Work - **Inter-agent channels beyond delegation** — shared state, streaming child output, and background/poll semantics remain outside the current synchronous `ctx.subagents` seam. -- **`agent/session-start` cannot gate startup** — it remains a synchronous, veto-less notification; async composition that must finish before publication belongs in the factory's `setup(agentCtx)` transaction instead. +- **`agent/session-start` cannot gate startup** — it remains a synchronous, veto-less notification; async composition that must finish before publication belongs in the factory's `setup(agentCtx)` transaction instead. Declarative startup failures are reported separately through `agent/start-failed`. - **No public step-only abort** — `cancel()` clears ALL pending work (queued + steering + in-flight); an abort that preserves queued prompts returns only with a named consumer ([stop-surface RFC](../../../docs/rfc/implemented/simplification/2026-06-20-public-agent-stop-surface.md)). - **`HookContext` carries exactly one `MessageSource`** — contributions from several plugins merged onto one tool call collapse under one source; mixed provenance is unrepresentable. - **`SessionStartSource` reserves `'clear'`/`'compact'` with no emitter yet** — only `'startup'`/`'resume'` occur until the driving subsystems land (`TODO(compaction)`). diff --git a/packages/core/agent/src/index.ts b/packages/core/agent/src/index.ts index fc5ad371b4..fc3a59b43f 100644 --- a/packages/core/agent/src/index.ts +++ b/packages/core/agent/src/index.ts @@ -80,6 +80,60 @@ export interface AgentHandle { dispose(): Promise } +/** Handlers for observing one configured agent's publication or startup failure. */ +export interface AgentStartHandlers { + /** + * Handle publication of the requested agent. + * @param agent - the live agent that was published. + */ + onStarted: (agent: Agent) => void + /** + * Handle a retained or live declarative startup failure. + * @param error - the contained startup failure. + */ + onFailed: (error: Error) => void +} + +/** + * Observe one configured agent until it is published or its declarative startup fails. + * Listeners are owned by the supplied context fiber and the returned disposer is + * idempotent through Cordis's event-disposer semantics. + * @param ctx - the context whose fiber owns the observation listeners. + * @param id - the configured agent id to observe. + * @param handlers - publication and failure callbacks. + * @returns a disposer for the observation listeners. + */ +export function observeAgentStart(ctx: Context, id: AgentId, handlers: AgentStartHandlers): () => void { + const stop = (): void => { + disposeCreated() + disposeFailure() + } + const handleStarted = (agent: Agent): void => { + if (agent.id !== id) return + stop() + handlers.onStarted(agent) + } + const handleFailed = (failedId: AgentId, error: Error): void => { + if (failedId !== id) return + stop() + handlers.onFailed(error) + } + const disposeCreated = ctx.on('agent/created', handleStarted) + const disposeFailure = ctx.on('agent/start-failed', handleFailed) + const agent = ctx.agents.get(id) + if (agent !== undefined) { + stop() + handlers.onStarted(agent) + } else { + const failure = ctx.agents.getStartFailure(id) + if (failure !== undefined) { + stop() + handlers.onFailed(failure) + } + } + return stop +} + /** * The agent-creation factory the loop implementation provides to the registry * via {@link AgentRegistry.setFactory}. Kept on the `dsh-agent` interface so @@ -136,6 +190,7 @@ export class AgentRegistry extends Service { // plus entry.agent identity; this WeakMap mirrors the authoritative id map. private entries = new WeakMap() private factory: FactorySlot | undefined + private startFailures = new Map() constructor(ctx: Context) { super(ctx, 'agents') @@ -224,6 +279,7 @@ export class AgentRegistry extends Service { const carrier = scopeTarget(agent, agent) // Prepared transactions arbitrate identity at this publication boundary. if (this.entries.has(agent) || this.store.has(id)) throw new Error(`agent "${id}" is already registered`) + this.startFailures.delete(id) const entry: AgentEntry = { id, agent, @@ -316,6 +372,45 @@ export class AgentRegistry extends Service { } } + /** + * Report a contained declarative startup failure and retain it for late UI observers. + * @param id - the configured agent id that failed before publication. + * @param error - the normalized startup error. + * @returns a disposer that clears this exact failure record. + */ + reportStartFailure(id: AgentId, error: Error): () => void { + const token = {} + this.startFailures.set(id, { error, token }) + this.emitStartFailed(id, error) + return () => { + if (this.startFailures.get(id)?.token === token) this.startFailures.delete(id) + } + } + + /** + * Return a retained declarative startup failure for an id that never became live. + * @param id - the configured agent id. + * @returns the startup error, or undefined when the id has no retained failure. + */ + getStartFailure(id: AgentId): Error | undefined { + return this.startFailures.get(id)?.error + } + + /** Emit an unscoped startup failure with the same listener containment as agent lifecycle events. */ + private emitStartFailed(id: AgentId, error: Error): void { + const args: unknown[] = ['agent/start-failed', id, error] + for (const callback of this.ctx.events.dispatch('emit', args)) { + try { + const returned: unknown = callback(id, error) + void Promise.resolve(returned).catch((listenerError: unknown) => { + this.ctx.logger.warn(`agent "${id}": agent/start-failed listener rejected: ${String(listenerError)}`) + }) + } catch (listenerError: unknown) { + this.ctx.logger.warn(`agent "${id}": agent/start-failed listener threw: ${String(listenerError)}`) + } + } + } + /** * Look up a live agent. * @param id - the agent id to look up. diff --git a/packages/core/agent/src/types.ts b/packages/core/agent/src/types.ts index 3aad65a70e..a9c16dec49 100644 --- a/packages/core/agent/src/types.ts +++ b/packages/core/agent/src/types.ts @@ -146,6 +146,15 @@ declare module 'cordis' { * @mode emit */ 'agent/disposed'(this: Scoped, agent: Agent): void + /** + * Declarative startup failed before an agent could be published. Programmatic + * `ctx.agents.create()` / `resume()` calls report failure through rejection; + * this event covers the fire-and-forget config path. + * @param agentId - the configured id that could not be started. + * @param error - the contained startup failure. + * @mode emit + */ + 'agent/start-failed'(agentId: AgentId, error: Error): void /** * Agent status changed (`idle` ⇄ `running`, or → `disposed`). `send()` does * not enter `running` synchronously; drive lifecycle from this event. diff --git a/packages/core/agent/tests/agent.spec.ts b/packages/core/agent/tests/agent.spec.ts index 5d541d56a8..b9acf9ef4e 100644 --- a/packages/core/agent/tests/agent.spec.ts +++ b/packages/core/agent/tests/agent.spec.ts @@ -2,7 +2,7 @@ import { describe, expect, expectTypeOf, it } from 'vitest' import { Context, Service, symbols } from 'cordis' import type { Events } from 'cordis' import { Session, SessionId } from '@deepseek-ai/dsh-session' -import AgentRegistry, { AgentId, agentEvents } from '@deepseek-ai/dsh-agent' +import AgentRegistry, { AgentId, agentEvents, observeAgentStart } from '@deepseek-ai/dsh-agent' import type { Agent, AgentFactory, ContinuationStop, CreateAgentOptions, ResumeAgentOptions } from '@deepseek-ai/dsh-agent' function stubAgent(rawId: string): Agent { @@ -85,6 +85,110 @@ describe('AgentRegistry', () => { ]) }) + it('retains declarative startup failures, contains listeners, and clears stale records', async () => { + const ctx = new Context() + await ctx.plugin(AgentRegistry) + const warnings: string[] = [] + const heard: string[] = [] + ctx.logger.warn = ((message: unknown) => { warnings.push(String(message)) }) as typeof ctx.logger.warn + ctx.on('agent/start-failed', () => { throw new Error('start sync') }) + ctx.on('agent/start-failed', () => Promise.reject(new Error('start async')) as never) + ctx.on('agent/start-failed', id => void heard.push(id)) + + const firstError = new Error('first failure') + const disposeFirst = ctx.agents.reportStartFailure(AgentId('main'), firstError) + expect(ctx.agents.getStartFailure(AgentId('main'))).toBe(firstError) + await Promise.resolve() + expect(heard).toEqual(['main']) + expect(warnings).toEqual([ + 'agent "main": agent/start-failed listener threw: Error: start sync', + 'agent "main": agent/start-failed listener rejected: Error: start async', + ]) + disposeFirst() + expect(ctx.agents.getStartFailure(AgentId('main'))).toBeUndefined() + + const disposeSecond = ctx.agents.reportStartFailure(AgentId('main'), new Error('second failure')) + const thirdError = new Error('third failure') + const disposeThird = ctx.agents.reportStartFailure(AgentId('main'), thirdError) + expect(ctx.agents.getStartFailure(AgentId('main'))).toBe(thirdError) + disposeSecond() + expect(ctx.agents.getStartFailure(AgentId('main'))).toBe(thirdError) + disposeThird() + + const disposeOccupiedAgent = ctx.agents.register(stubAgent('occupied')) + const occupiedError = new Error('occupied failure') + const disposeOccupiedFailure = ctx.agents.reportStartFailure(AgentId('occupied'), occupiedError) + expect(() => { ctx.agents.enter(stubAgent('occupied')) }).toThrow(/already registered/) + expect(ctx.agents.getStartFailure(AgentId('occupied'))).toBe(occupiedError) + disposeOccupiedFailure() + disposeOccupiedAgent() + + const disposeCleared = ctx.agents.reportStartFailure(AgentId('main'), new Error('cleared failure')) + ctx.agents.register(stubAgent('main'))() + expect(ctx.agents.getStartFailure(AgentId('main'))).toBeUndefined() + disposeCleared() + await ctx.fiber.dispose() + }) + + it('observes immediate, retained, live, unrelated, and cancelled startup outcomes', async () => { + const ctx = new Context() + await ctx.plugin(AgentRegistry) + const outcomes: string[] = [] + + const existing = stubAgent('existing') + const disposeExisting = ctx.agents.register(existing) + observeAgentStart(ctx, existing.id, { + onStarted: agent => void outcomes.push(`started:${agent.id}`), + onFailed: error => void outcomes.push(`failed:${error.message}`), + })() + + const retainedError = new Error('retained') + const disposeRetained = ctx.agents.reportStartFailure(AgentId('retained'), retainedError) + observeAgentStart(ctx, AgentId('retained'), { + onStarted: agent => void outcomes.push(`started:${agent.id}`), + onFailed: error => void outcomes.push(`failed:${error.message}`), + })() + + const stopLiveStart = observeAgentStart(ctx, AgentId('live-start'), { + onStarted: agent => void outcomes.push(`started:${agent.id}`), + onFailed: error => void outcomes.push(`failed:${error.message}`), + }) + const disposeUnrelatedAgent = ctx.agents.register(stubAgent('unrelated')) + const disposeUnrelatedFailure = ctx.agents.reportStartFailure(AgentId('unrelated'), new Error('unrelated')) + const disposeLiveAgent = ctx.agents.register(stubAgent('live-start')) + stopLiveStart() + + const stopLiveFailure = observeAgentStart(ctx, AgentId('live-failure'), { + onStarted: agent => void outcomes.push(`started:${agent.id}`), + onFailed: error => void outcomes.push(`failed:${error.message}`), + }) + const disposeLiveFailure = ctx.agents.reportStartFailure(AgentId('live-failure'), new Error('live')) + stopLiveFailure() + + const stopCancelled = observeAgentStart(ctx, AgentId('cancelled'), { + onStarted: agent => void outcomes.push(`started:${agent.id}`), + onFailed: error => void outcomes.push(`failed:${error.message}`), + }) + stopCancelled() + const disposeCancelledFailure = ctx.agents.reportStartFailure(AgentId('cancelled'), new Error('cancelled')) + + expect(outcomes).toEqual([ + 'started:existing', + 'failed:retained', + 'started:live-start', + 'failed:live', + ]) + + disposeCancelledFailure() + disposeLiveFailure() + disposeLiveAgent() + disposeUnrelatedFailure() + disposeUnrelatedAgent() + disposeRetained() + disposeExisting() + await ctx.fiber.dispose() + }) + it('separates entry from announcement and stale/idempotent detach cannot remove a replacement', async () => { const ctx = new Context() await ctx.plugin(AgentRegistry) diff --git a/packages/examples/stdio-demo/README.md b/packages/examples/stdio-demo/README.md index 96704998c4..05dd4d34c2 100644 --- a/packages/examples/stdio-demo/README.md +++ b/packages/examples/stdio-demo/README.md @@ -15,8 +15,8 @@ A terminal chat always wants the same cluster, so the package owns it rather tha | `@deepseek-ai/dsh-user-interaction` | the human question/answer seam used by confirmation tools | | `@deepseek-ai/dsh-tool-ask-user` | the model-facing `ask_user_question` tool | | `@cordisjs/plugin-logger-console` | readline diagnostics for non-TTY operation; omitted from the fullscreen TUI path | -| `@deepseek-ai/dsh-stdio` | the line-oriented terminal channel, bound to `main` for pipes and automation | -| `@deepseek-ai/dsh-tui` | the interactive pi-tui channel, bound to `main` for TTY pairs | +| `@deepseek-ai/dsh-stdio` | the line-oriented terminal channel, bound to `main` for pipes and automation; matching `agent/start-failed` errors print and exit nonzero | +| `@deepseek-ai/dsh-tui` | the interactive pi-tui channel, bound to `main` for TTY pairs; matching `agent/start-failed` startup errors are printed before fullscreen mode and exit nonzero | `@cordisjs/plugin-hmr` (the dev/demo edit-reload loop) is deliberately a **leaf** entry, not baked in here: it is a Loader-only, subprocess-only dev plugin whose constructor needs `node --expose-internals` plus a live `loader`. The `demo:echo` / `demo:repl` leaves load it and pass `--expose-internals`. diff --git a/packages/ui/stdio/README.md b/packages/ui/stdio/README.md index b7d320880d..0ee46b3a3d 100644 --- a/packages/ui/stdio/README.md +++ b/packages/ui/stdio/README.md @@ -11,7 +11,7 @@ This package owns the terminal channel only. It injects `agents` and `userIntera | `welcome` | `ready.` | Banner printed before the first prompt | | `agent` | `main` | Agent id driven by stdin and observed for EOF shutdown | -The plugin seeds display labels from the live agent registry, then tracks `agent/created` and `agent/disposed` so HMR and externally managed agents render consistently. Disposal closes readline and unregisters every listener/provider through Cordis effects. +The plugin seeds display labels from the live agent registry, then tracks `agent/created` and `agent/disposed` so HMR and externally managed agents render consistently. While waiting for its configured agent, it also observes retained and live `agent/start-failed` notifications; a matching failure is printed and exits with status 1 instead of waiting forever. Disposal closes readline and unregisters every listener/provider through Cordis effects. ```yaml - id: stdio diff --git a/packages/ui/stdio/src/index.ts b/packages/ui/stdio/src/index.ts index 1e665381ce..3773a55e92 100644 --- a/packages/ui/stdio/src/index.ts +++ b/packages/ui/stdio/src/index.ts @@ -13,7 +13,7 @@ import { createInterface } from 'node:readline' import type { Readable, Writable } from 'node:stream' import type { Context } from 'cordis' import z from 'schemastery' -import { AgentId } from '@deepseek-ai/dsh-agent' +import { AgentId, observeAgentStart } from '@deepseek-ai/dsh-agent' import { UserInteractionError, type AskUserQuestionAnswer, @@ -363,14 +363,12 @@ export function createStdioChat(ctx: Context, config: Config, runtime: StdioRunt */ export function mountStdio(ctx: Context, config: Config, runtime: StdioRuntime): void { const agentId = AgentId(config.agent ?? 'main') - if (ctx.agents.get(agentId) !== undefined) { - createStdioChat(ctx, config, runtime) - return - } - const dispose = ctx.on('agent/created', (agent) => { - if (agent.id !== agentId) return - dispose() - createStdioChat(ctx, config, runtime) + observeAgentStart(ctx, agentId, { + onStarted: () => { createStdioChat(ctx, config, runtime) }, + onFailed: (error) => { + runtime.output.write(`ui-stdio: agent "${agentId}" failed to start: ${error.message}\n`) + runtime.exit(1) + }, }) } diff --git a/packages/ui/stdio/tests/stdio.spec.ts b/packages/ui/stdio/tests/stdio.spec.ts index 7bb6a6f245..60407df416 100644 --- a/packages/ui/stdio/tests/stdio.spec.ts +++ b/packages/ui/stdio/tests/stdio.spec.ts @@ -2,7 +2,7 @@ import { Readable, Writable } from 'node:stream' import { describe, expect, it, vi } from 'vitest' import { Context } from 'cordis' import type { Agent, AgentStatus } from '@deepseek-ai/dsh-agent' -import AgentRegistry from '@deepseek-ai/dsh-agent' +import AgentRegistry, { AgentId } from '@deepseek-ai/dsh-agent' import type { ContentBlock, StreamChunk } from '@deepseek-ai/dsh-llm' import type { Session, SessionEvent } from '@deepseek-ai/dsh-session' import UserInteractionService from '@deepseek-ai/dsh-user-interaction' @@ -111,6 +111,42 @@ describe('mountStdio readiness', () => { await fiber.dispose() }) + it('prints a matching live startup failure and exits instead of waiting forever', async () => { + const ctx = new Context() + await ctx.plugin(AgentRegistry) + await ctx.plugin(UserInteractionService) + const { runtime, out, exit } = makeRuntime() + const fiber = await ctx.plugin(Object.assign((inner: Context) => { + mountStdio(inner, CONFIG, runtime) + }, { inject: ['agents', 'userInteraction'] })) + + ctx.agents.reportStartFailure(AgentId('other'), new Error('other failed')) + expect(out.text()).toBe('') + expect(exit).not.toHaveBeenCalled() + ctx.agents.reportStartFailure(AgentId('main'), new Error('resume failed')) + expect(out.text()).toBe('ui-stdio: agent "main" failed to start: resume failed\n') + expect(exit).toHaveBeenCalledWith(1) + + ctx.agents.register(makeAgent('main')) + expect(out.text()).toBe('ui-stdio: agent "main" failed to start: resume failed\n') + await fiber.dispose() + }) + + it('prints a retained startup failure for a late-mounted stdio channel', async () => { + const ctx = new Context() + await ctx.plugin(AgentRegistry) + await ctx.plugin(UserInteractionService) + ctx.agents.reportStartFailure(AgentId('main'), new Error('already failed')) + const { runtime, out, exit } = makeRuntime() + const fiber = await ctx.plugin(Object.assign((inner: Context) => { + mountStdio(inner, CONFIG, runtime) + }, { inject: ['agents', 'userInteraction'] })) + + expect(out.text()).toBe('ui-stdio: agent "main" failed to start: already failed\n') + expect(exit).toHaveBeenCalledWith(1) + await fiber.dispose() + }) + it('opens immediately when the configured agent already exists', async () => { const ctx = new Context() await ctx.plugin(AgentRegistry) diff --git a/packages/ui/tui/README.md b/packages/ui/tui/README.md index 1e678beac8..7cc84cee3d 100644 --- a/packages/ui/tui/README.md +++ b/packages/ui/tui/README.md @@ -33,7 +33,7 @@ While the agent is running, editor submissions call `agent.steer()`; otherwise t maxToolOutputLines: 12 ``` -Startup fails before mounting when either process stream is not a TTY. Disposal stops loaders, rejects pending questions, drains terminal input, restores terminal state, unregisters event listeners and the user-interaction provider, and never exits a replacement process during HMR. +Startup fails before mounting when either process stream is not a TTY. While waiting for its configured agent, the front door also observes retained and live `agent/start-failed` notifications; a matching failure is written before fullscreen mode starts and exits with status 1 instead of leaving a blank terminal. Disposal stops loaders, rejects pending questions, drains terminal input, restores terminal state, unregisters event listeners and the user-interaction provider, and never exits a replacement process during HMR. ## Model Experience diff --git a/packages/ui/tui/src/index.ts b/packages/ui/tui/src/index.ts index 14f45242d7..13b5b3866c 100644 --- a/packages/ui/tui/src/index.ts +++ b/packages/ui/tui/src/index.ts @@ -34,7 +34,7 @@ import { } from '@earendil-works/pi-tui' import type { Context } from 'cordis' import z from 'schemastery' -import { AgentId, type Agent, type AgentStatus } from '@deepseek-ai/dsh-agent' +import { AgentId, observeAgentStart, type Agent, type AgentStatus } from '@deepseek-ai/dsh-agent' import type { ContentBlock, StreamChunk } from '@deepseek-ai/dsh-llm' import type { Session, SessionEvent, TodoItem } from '@deepseek-ai/dsh-session' import type { @@ -133,7 +133,7 @@ export interface ResolvedTuiConfig { export interface TuiRuntime { /** Terminal implementation; production uses pi-tui's `ProcessTerminal`. */ terminal: Terminal - /** Exit hook used by `/exit`, Ctrl+D, or Ctrl+C while idle. */ + /** Exit hook used by terminal shutdown or a target-agent startup failure. */ exit(code: number): void } @@ -1252,20 +1252,17 @@ export function createTuiChat( */ export function mountTui(ctx: Context, config: Config, runtime: TuiRuntime): void { const agentId = AgentId(config.agent ?? 'main') - const start = (): void => { - ctx.effect(() => { - const controller = createTuiChat(ctx, config, runtime) - return () => controller.dispose() - }, 'ui-tui') - } - if (ctx.agents.get(agentId) !== undefined) { - start() - return - } - const dispose = ctx.on('agent/created', (agent) => { - if (agent.id !== agentId) return - dispose() - start() + observeAgentStart(ctx, agentId, { + onStarted: () => { + ctx.effect(() => { + const controller = createTuiChat(ctx, config, runtime) + return () => controller.dispose() + }, 'ui-tui') + }, + onFailed: (error) => { + runtime.terminal.write(`ui-tui: agent "${agentId}" failed to start: ${error.message}\n`) + runtime.exit(1) + }, }) } diff --git a/packages/ui/tui/tests/tui.spec.ts b/packages/ui/tui/tests/tui.spec.ts index e4ffe2efd0..c0d1d419b8 100644 --- a/packages/ui/tui/tests/tui.spec.ts +++ b/packages/ui/tui/tests/tui.spec.ts @@ -910,6 +910,51 @@ describe('terminal mounting', () => { await ctx.fiber.dispose() }) + it('prints a matching live startup failure and exits instead of waiting forever', async () => { + const ctx = new Context() + await ctx.plugin(SessionStore) + await ctx.plugin(AgentRegistry) + await ctx.plugin(UserInteractionService) + ctx.provide('tools', { get: () => undefined } as never) + const terminal = new FakeTerminal() + const exit = vi.fn() + mountTui(ctx, { agent: 'main', color: false }, { terminal, exit }) + + ctx.agents.reportStartFailure(AgentId('other'), new Error('other failed')) + expect(terminal.output).toBe('') + expect(exit).not.toHaveBeenCalled() + ctx.agents.reportStartFailure(AgentId('main'), new Error('resume failed')) + expect(terminal.output).toBe('ui-tui: agent "main" failed to start: resume failed\n') + expect(exit).toHaveBeenCalledWith(1) + + const session = ctx.sessions.create(SessionId('must-not-start')) + ctx.agents.register({ + id: AgentId('main'), options: {}, session, status: 'idle', ctx, + send() {}, steer() {}, inject() {}, cancel() {}, whenIdle: () => Promise.resolve(), + }) + await tick() + expect(terminal.started).toBe(0) + await ctx.fiber.dispose() + }) + + it('prints a retained startup failure for a late-mounted TUI', async () => { + const ctx = new Context() + await ctx.plugin(SessionStore) + await ctx.plugin(AgentRegistry) + await ctx.plugin(UserInteractionService) + ctx.provide('tools', { get: () => undefined } as never) + ctx.agents.reportStartFailure(AgentId('main'), new Error('already failed')) + const terminal = new FakeTerminal() + const exit = vi.fn() + + mountTui(ctx, { agent: 'main', color: false }, { terminal, exit }) + + expect(terminal.started).toBe(0) + expect(terminal.output).toBe('ui-tui: agent "main" failed to start: already failed\n') + expect(exit).toHaveBeenCalledWith(1) + await ctx.fiber.dispose() + }) + it('rolls back providers, listeners, and terminal state when startup fails', async () => { const ctx = new Context() await ctx.plugin(SessionStore) From 87e25ef1fefe6b5614bf33cf5f2580f71ad2c5f0 Mon Sep 17 00:00:00 2001 From: Turtle Date: Fri, 17 Jul 2026 16:50:53 +0800 Subject: [PATCH 03/13] fix(tui): make the color palette theme-agnostic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The palette used absolute 256-color indices (`38;5;252`, `48;5;236`, …) that terminals never remap to the active scheme: near-white body text vanished on light backgrounds and fixed dark bubble fills put default foreground text on dark blocks. Rebuild it from the standard 16-color ANSI foregrounds plus SGR attributes, keep body text on the terminal default foreground, replace filled background bubbles with foreground-only left-gutter bars, and use reverse video for the active selection so every grouping reads on any color scheme. --- docs/config-catalog.md | 2 +- packages/ui/tui/README.md | 6 ++- packages/ui/tui/src/index.ts | 89 ++++++++++++++++++++++++------------ 3 files changed, 66 insertions(+), 31 deletions(-) diff --git a/docs/config-catalog.md b/docs/config-catalog.md index 79aa6ce17a..8004a8fa43 100644 --- a/docs/config-catalog.md +++ b/docs/config-catalog.md @@ -1122,7 +1122,7 @@ export interface TuiConfig { } ``` -Source: [`packages/ui/tui/src/index.ts:100`](../packages/ui/tui/src/index.ts) +Source: [`packages/ui/tui/src/index.ts:99`](../packages/ui/tui/src/index.ts) ## `@deepseek-ai/dsh-user-approval` diff --git a/packages/ui/tui/README.md b/packages/ui/tui/README.md index 7cc84cee3d..865a20a469 100644 --- a/packages/ui/tui/README.md +++ b/packages/ui/tui/README.md @@ -20,7 +20,7 @@ While the agent is running, editor submissions call `agent.steer()`; otherwise t | `questionDialogWidth` | `72` | Question-overlay width in columns | | `questionDialogMaxHeight` | `20` | Question-overlay maximum rows | | `showHardwareCursor` | `false` | Show the hardware cursor at pi-tui's IME marker | -| `color` | `true` | Apply the built-in ANSI palette | +| `color` | `true` | Apply the built-in ANSI palette (see [Color](#color)) | | `title` | `DeepSeek Harness` | Terminal window title | ```yaml @@ -35,6 +35,10 @@ While the agent is running, editor submissions call `agent.steer()`; otherwise t Startup fails before mounting when either process stream is not a TTY. While waiting for its configured agent, the front door also observes retained and live `agent/start-failed` notifications; a matching failure is written before fullscreen mode starts and exits with status 1 instead of leaving a blank terminal. Disposal stops loaders, rejects pending questions, drains terminal input, restores terminal state, unregisters event listeners and the user-interaction provider, and never exits a replacement process during HMR. +## Color + +The palette uses the standard 16-color ANSI foregrounds and SGR attributes, which every terminal remaps to its active color scheme, so it stays readable on light and dark backgrounds alike. Body text keeps the terminal's default foreground rather than a fixed shade. Grouped regions (user prompts, tool cards) use a colored left-gutter bar instead of a filled background block, and the question overlay's active row uses reverse video; both are foreground-only, so they never collide with the terminal background. Set `color: false` to strip all styling. + ## Model Experience ### Interactive prompt input diff --git a/packages/ui/tui/src/index.ts b/packages/ui/tui/src/index.ts index 13b5b3866c..aa8a510a32 100644 --- a/packages/ui/tui/src/index.ts +++ b/packages/ui/tui/src/index.ts @@ -8,7 +8,6 @@ import { homedir } from 'node:os' import { relative, resolve, sep } from 'node:path' import { - Box, CombinedAutocompleteProvider, Container, Editor, @@ -172,39 +171,39 @@ interface Palette { italic: (text: string) => string underline: (text: string) => string strike: (text: string) => string - userBg: (text: string) => string - pendingBg: (text: string) => string - successBg: (text: string) => string - errorBg: (text: string) => string - selectedBg: (text: string) => string + /** Reverse video for the active selection; swaps the theme's own fg/bg so it reads on any scheme. */ + selected: (text: string) => string } function ansi(open: string, close: string, enabled: boolean): (text: string) => string { return enabled ? text => `\x1b[${open}m${text}\x1b[${close}m` : text => text } +/** + * Theme-agnostic palette built from the standard 16-color ANSI set plus SGR + * attributes, which every terminal remaps to its active color scheme. Body + * `text` stays the terminal's default foreground so it reads on light and dark + * backgrounds alike; grouping uses foreground-only gutter bars and reverse + * video rather than fixed background fills. + */ function createPalette(enabled: boolean): Palette { return { - accent: ansi('38;5;39', '39', enabled), - accent2: ansi('38;5;141', '39', enabled), - text: ansi('38;5;252', '39', enabled), - muted: ansi('38;5;245', '39', enabled), - dim: ansi('38;5;240', '39', enabled), - success: ansi('38;5;78', '39', enabled), - warning: ansi('38;5;221', '39', enabled), - error: ansi('38;5;203', '39', enabled), - code: ansi('38;5;215', '39', enabled), - added: ansi('38;5;78', '39', enabled), - removed: ansi('38;5;203', '39', enabled), + accent: ansi('94', '39', enabled), + accent2: ansi('95', '39', enabled), + text: text => text, + muted: ansi('90', '39', enabled), + dim: ansi('2', '22', enabled), + success: ansi('32', '39', enabled), + warning: ansi('33', '39', enabled), + error: ansi('31', '39', enabled), + code: ansi('36', '39', enabled), + added: ansi('32', '39', enabled), + removed: ansi('31', '39', enabled), bold: ansi('1', '22', enabled), italic: ansi('3', '23', enabled), underline: ansi('4', '24', enabled), strike: ansi('9', '29', enabled), - userBg: ansi('48;5;236', '49', enabled), - pendingBg: ansi('48;5;235', '49', enabled), - successBg: ansi('48;5;22', '49', enabled), - errorBg: ansi('48;5;52', '49', enabled), - selectedBg: ansi('48;5;24', '49', enabled), + selected: ansi('7', '27', enabled), } } @@ -296,9 +295,41 @@ class HeaderComponent implements Component { } } -class UserMessageComponent extends Box { +/** + * Groups children behind a colored left-gutter bar (`▌`). Foreground-only, so + * it renders legibly on any terminal background — unlike a filled block whose + * body text would collide with the theme's default foreground. + */ +class GutterBox implements Component { + protected readonly children: Component[] = [] + + constructor(private readonly barFn: (text: string) => string, private readonly paddingY = 1) {} + + addChild(child: Component): void { + this.children.push(child) + } + + invalidate(): void { + for (const child of this.children) child.invalidate() + } + + render(width: number): string[] { + const inner = Math.max(1, width - 2) + const body: string[] = [] + for (const child of this.children) for (const line of child.render(inner)) body.push(line) + // Every caller adds a non-empty title/label child, so an all-empty box is unreachable; + // the guard preserves Box semantics (render nothing) rather than emitting stray gutter bars. + /* v8 ignore next */ + if (body.length === 0) return [] + const bar = this.barFn('▌') + const pad = Array.from({ length: this.paddingY }, () => '') + return [...pad, ...body, ...pad].map(line => `${bar} ${line}`) + } +} + +class UserMessageComponent extends GutterBox { constructor(text: string, palette: Palette, mdTheme: MarkdownTheme, label = 'You') { - super(1, 1, value => palette.userBg(value)) + super(value => palette.accent(value)) this.addChild(new Text(palette.bold(palette.accent(label)), 0, 0)) this.addChild(new Markdown(text, 0, 0, mdTheme, { color: value => palette.text(value) }, { preserveOrderedListMarkers: true, @@ -465,10 +496,10 @@ class ToolCardComponent implements Component { const visibleBody = this.expanded || body.length <= this.maxOutputLines ? body : [...body.slice(0, this.maxOutputLines), this.palette.dim(`… ${body.length - this.maxOutputLines} more lines (Ctrl+O to expand)`)] - const box = new Box(1, visibleBody.length > 0 ? 1 : 0, (value) => { - if (this.result === undefined) return this.palette.pendingBg(value) - return isError ? this.palette.errorBg(value) : this.palette.successBg(value) - }) + const barFn = this.result === undefined + ? this.palette.warning + : isError ? this.palette.error : this.palette.success + const box = new GutterBox(barFn, visibleBody.length > 0 ? 1 : 0) box.addChild(new Text(this.palette.bold(title), 0, 0)) if (visibleBody.length > 0) box.addChild(new Text(visibleBody.join('\n'), 0, 0)) return box.render(width) @@ -700,7 +731,7 @@ class QuestionDialog implements Component, Focusable { : index === this.selectedIndex ? this.palette.accent('●') : this.palette.dim('○') const description = option.description ? this.palette.muted(` — ${option.description}`) : '' const line = `${cursor} ${mark} ${option.label}${description}` - push(index === this.selectedIndex ? this.palette.selectedBg(line) : line) + push(index === this.selectedIndex ? this.palette.selected(line) : line) } if (options.length > this.maxVisible) push(this.palette.dim(`${this.selectedIndex + 1}/${options.length}`)) push(this.palette.dim(this.question.multiSelect From 7f91a05ed772cee464cb450b0a69858c7fdd4cfc Mon Sep 17 00:00:00 2001 From: Turtle Date: Fri, 17 Jul 2026 17:13:41 +0800 Subject: [PATCH 04/13] docs: add TUI screenshots --- docs/assets/tui/jokes-dark.png | Bin 0 -> 66598 bytes docs/assets/tui/jokes-dynamic.gif | Bin 0 -> 78162 bytes docs/assets/tui/jokes-light.png | Bin 0 -> 70844 bytes 3 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 docs/assets/tui/jokes-dark.png create mode 100644 docs/assets/tui/jokes-dynamic.gif create mode 100644 docs/assets/tui/jokes-light.png diff --git a/docs/assets/tui/jokes-dark.png b/docs/assets/tui/jokes-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..876ea9c24c9422e76aa839bdead1edd6cb6df22f GIT binary patch literal 66598 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 a`$p*P1=7cj$ra$88&Gk1u|iQjum1%LHms5W literal 0 HcmV?d00001 diff --git a/docs/assets/tui/jokes-dynamic.gif b/docs/assets/tui/jokes-dynamic.gif new file mode 100644 index 0000000000000000000000000000000000000000..f0036572e8a2eeb0721d6929cafe404e0f6099ad GIT binary patch literal 78162 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�%^@#dq}3j{IDaY(s{Y&2k(r-F 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 Date: Sat, 18 Jul 2026 22:31:04 +0800 Subject: [PATCH 05/13] test(tui): snapshot semantic terminal state --- docs/rfc/INDEX.md | 1 + ...-18-tui-terminal-state-snapshots.i18n.yaml | 6 + ...2026-07-18-tui-terminal-state-snapshots.md | 57 +++ ...6-07-18-tui-terminal-state-snapshots.zh.md | 57 +++ docs/testing.md | 4 +- .../tests/fixtures/tui-scripted-llm.ts | 60 +++ .../tests/fixtures/tui-scripted.cordis.yml | 27 + .../tests/tui-keyless-smoke.e2e.ts | 87 +++- knip.json | 5 + packages/ui/tui/package.json | 5 + packages/ui/tui/tests/harness.ts | 130 +++++ packages/ui/tui/tests/headless-terminal.ts | 318 ++++++++++++ .../advanced-cards-collapsed.golden.txt | 107 ++++ .../advanced-cards-expanded.golden.txt | 127 +++++ .../snapshots/code-mode-complete.golden.txt | 50 ++ .../snapshots/code-mode-pending.golden.txt | 52 ++ .../conversation-complete.golden.txt | 75 +++ .../snapshots/conversation-replay.golden.txt | 73 +++ .../conversation-streaming.golden.txt | 75 +++ .../cordis-tools-complete.golden.txt | 73 +++ .../snapshots/cordis-tools-pending.golden.txt | 59 +++ .../snapshots/disposed-terminal.golden.txt | 52 ++ .../dynamic-workflow-complete.golden.txt | 53 ++ .../dynamic-workflow-pending.golden.txt | 55 ++ .../snapshots/errors-and-help.golden.txt | 52 ++ .../question-dialog-validation.golden.txt | 69 +++ .../snapshots/question-dialog.golden.txt | 67 +++ ...surface-after-compaction-narrow.golden.txt | 45 ++ .../surface-after-compaction-wide.golden.txt | 37 ++ .../surface-before-compaction.golden.txt | 67 +++ packages/ui/tui/tests/tui.snapshot.ts | 472 ++++++++++++++++++ packages/ui/tui/tests/tui.spec.ts | 101 +--- pnpm-lock.yaml | 20 + vitest.snapshot.config.ts | 6 +- 34 files changed, 2437 insertions(+), 107 deletions(-) create mode 100644 docs/rfc/implemented/testing/2026-07-18-tui-terminal-state-snapshots.i18n.yaml create mode 100644 docs/rfc/implemented/testing/2026-07-18-tui-terminal-state-snapshots.md create mode 100644 docs/rfc/implemented/testing/2026-07-18-tui-terminal-state-snapshots.zh.md create mode 100644 examples/coding-agent/tests/fixtures/tui-scripted-llm.ts create mode 100644 examples/coding-agent/tests/fixtures/tui-scripted.cordis.yml create mode 100644 packages/ui/tui/tests/harness.ts create mode 100644 packages/ui/tui/tests/headless-terminal.ts create mode 100644 packages/ui/tui/tests/snapshots/advanced-cards-collapsed.golden.txt create mode 100644 packages/ui/tui/tests/snapshots/advanced-cards-expanded.golden.txt create mode 100644 packages/ui/tui/tests/snapshots/code-mode-complete.golden.txt create mode 100644 packages/ui/tui/tests/snapshots/code-mode-pending.golden.txt create mode 100644 packages/ui/tui/tests/snapshots/conversation-complete.golden.txt create mode 100644 packages/ui/tui/tests/snapshots/conversation-replay.golden.txt create mode 100644 packages/ui/tui/tests/snapshots/conversation-streaming.golden.txt create mode 100644 packages/ui/tui/tests/snapshots/cordis-tools-complete.golden.txt create mode 100644 packages/ui/tui/tests/snapshots/cordis-tools-pending.golden.txt create mode 100644 packages/ui/tui/tests/snapshots/disposed-terminal.golden.txt create mode 100644 packages/ui/tui/tests/snapshots/dynamic-workflow-complete.golden.txt create mode 100644 packages/ui/tui/tests/snapshots/dynamic-workflow-pending.golden.txt create mode 100644 packages/ui/tui/tests/snapshots/errors-and-help.golden.txt create mode 100644 packages/ui/tui/tests/snapshots/question-dialog-validation.golden.txt create mode 100644 packages/ui/tui/tests/snapshots/question-dialog.golden.txt create mode 100644 packages/ui/tui/tests/snapshots/surface-after-compaction-narrow.golden.txt create mode 100644 packages/ui/tui/tests/snapshots/surface-after-compaction-wide.golden.txt create mode 100644 packages/ui/tui/tests/snapshots/surface-before-compaction.golden.txt create mode 100644 packages/ui/tui/tests/tui.snapshot.ts diff --git a/docs/rfc/INDEX.md b/docs/rfc/INDEX.md index ecb66326b3..434df9f305 100644 --- a/docs/rfc/INDEX.md +++ b/docs/rfc/INDEX.md @@ -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 diff --git a/docs/rfc/implemented/testing/2026-07-18-tui-terminal-state-snapshots.i18n.yaml b/docs/rfc/implemented/testing/2026-07-18-tui-terminal-state-snapshots.i18n.yaml new file mode 100644 index 0000000000..5b682b642b --- /dev/null +++ b/docs/rfc/implemented/testing/2026-07-18-tui-terminal-state-snapshots.i18n.yaml @@ -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 diff --git a/docs/rfc/implemented/testing/2026-07-18-tui-terminal-state-snapshots.md b/docs/rfc/implemented/testing/2026-07-18-tui-terminal-state-snapshots.md new file mode 100644 index 0000000000..225efe8de9 --- /dev/null +++ b/docs/rfc/implemented/testing/2026-07-18-tui-terminal-state-snapshots.md @@ -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 0–15, 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. diff --git a/docs/rfc/implemented/testing/2026-07-18-tui-terminal-state-snapshots.zh.md b/docs/rfc/implemented/testing/2026-07-18-tui-terminal-state-snapshots.zh.md new file mode 100644 index 0000000000..b7399a31b0 --- /dev/null +++ b/docs/rfc/implemented/testing/2026-07-18-tui-terminal-state-snapshots.zh.md @@ -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 会话覆盖进程选择、键盘输入、用户交互和清理,无需复制完整矩阵。 +- 金标有意固定指定尺寸下的换行与视口行为。布局的预期变更通过无密钥刷新命令更新,并接受常规快照评审。 diff --git a/docs/testing.md b/docs/testing.md index 8d4f0f554f..5f10e1741c 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -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//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//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. diff --git a/examples/coding-agent/tests/fixtures/tui-scripted-llm.ts b/examples/coding-agent/tests/fixtures/tui-scripted-llm.ts new file mode 100644 index 0000000000..2147dfb222 --- /dev/null +++ b/examples/coding-agent/tests/fixtures/tui-scripted-llm.ts @@ -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 { + 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()) +} diff --git a/examples/coding-agent/tests/fixtures/tui-scripted.cordis.yml b/examples/coding-agent/tests/fixtures/tui-scripted.cordis.yml new file mode 100644 index 0000000000..e40405524e --- /dev/null +++ b/examples/coding-agent/tests/fixtures/tui-scripted.cordis.yml @@ -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 diff --git a/examples/coding-agent/tests/tui-keyless-smoke.e2e.ts b/examples/coding-agent/tests/tui-keyless-smoke.e2e.ts index f5f9c5db36..cc2397c069 100644 --- a/examples/coding-agent/tests/tui-keyless-smoke.e2e.ts +++ b/examples/coding-agent/tests/tui-keyless-smoke.e2e.ts @@ -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 { +interface TuiLoaderSmokeOptions { + config?: string + resumeSessionId?: string + scenario?: 'boot' | 'conversation' | 'resume-failure' +} + +async function runTuiLoaderSmoke(options: TuiLoaderSmokeOptions = {}): Promise { 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) diff --git a/knip.json b/knip.json index 16f83b918b..03fae14896 100644 --- a/knip.json +++ b/knip.json @@ -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"] }, diff --git a/packages/ui/tui/package.json b/packages/ui/tui/package.json index e960f33e65..a7ed458ca5 100644 --- a/packages/ui/tui/package.json +++ b/packages/ui/tui/package.json @@ -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" } } diff --git a/packages/ui/tui/tests/harness.ts b/packages/ui/tui/tests/harness.ts new file mode 100644 index 0000000000..52f4159b72 --- /dev/null +++ b/packages/ui/tui/tests/harness.ts @@ -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 + configureContext?: (ctx: Context) => Promise + beforeMount?: (session: Session) => void + cwd?: string | null +} + +export interface TuiHarness void> { + ctx: Context + session: Session + agent: FakeAgent + terminal: TerminalType + exit: Exit + controller: ReturnType +} + +/** + * 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 void>( + terminal: TerminalType, + exit: Exit, + options: TuiHarnessOptions = {}, +): Promise> { + 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 void>, 'controller' | 'ctx'>, +): Promise { + 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' }) +} diff --git a/packages/ui/tui/tests/headless-terminal.ts b/packages/ui/tui/tests/headless-terminal.ts new file mode 100644 index 0000000000..1155016ab7 --- /dev/null +++ b/packages/ui/tui/tests/headless-terminal.ts @@ -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 +} + +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}| ` : `${blankStart}-${end}| `) + 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 => 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 = Promise.resolve() + private readonly frameWaiters = new Set() + + 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 { + if (this.frames <= after) { + await new Promise((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 { + let pending: Promise + 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 { + 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 { + 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() + } +} diff --git a/packages/ui/tui/tests/snapshots/advanced-cards-collapsed.golden.txt b/packages/ui/tui/tests/snapshots/advanced-cards-collapsed.golden.txt new file mode 100644 index 0000000000..10e6e250b4 --- /dev/null +++ b/packages/ui/tui/tests/snapshots/advanced-cards-collapsed.golden.txt @@ -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| +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| +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| +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| +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| +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 diff --git a/packages/ui/tui/tests/snapshots/advanced-cards-expanded.golden.txt b/packages/ui/tui/tests/snapshots/advanced-cards-expanded.golden.txt new file mode 100644 index 0000000000..42822d3720 --- /dev/null +++ b/packages/ui/tui/tests/snapshots/advanced-cards-expanded.golden.txt @@ -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| +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| +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| +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| +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| +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| +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 diff --git a/packages/ui/tui/tests/snapshots/code-mode-complete.golden.txt b/packages/ui/tui/tests/snapshots/code-mode-complete.golden.txt new file mode 100644 index 0000000000..72f7fce352 --- /dev/null +++ b/packages/ui/tui/tests/snapshots/code-mode-complete.golden.txt @@ -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| +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| diff --git a/packages/ui/tui/tests/snapshots/code-mode-pending.golden.txt b/packages/ui/tui/tests/snapshots/code-mode-pending.golden.txt new file mode 100644 index 0000000000..6908b69890 --- /dev/null +++ b/packages/ui/tui/tests/snapshots/code-mode-pending.golden.txt @@ -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| +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| diff --git a/packages/ui/tui/tests/snapshots/conversation-complete.golden.txt b/packages/ui/tui/tests/snapshots/conversation-complete.golden.txt new file mode 100644 index 0000000000..8642d2ffa1 --- /dev/null +++ b/packages/ui/tui/tests/snapshots/conversation-complete.golden.txt @@ -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| +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| +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| +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| +27| " Reasoning " + style 1-9 fg=bright-black italic +28| " Inspecting width and styles. " + style 1-28 fg=bright-black italic +29| +30| " Assistant " + style 1-9 fg=bright-magenta bold +31| " Streaming visible state is complete. " + style 11-23 bold +32| +33| " The model reached its output-token limit. " + style 1-41 fg=yellow +34| +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 diff --git a/packages/ui/tui/tests/snapshots/conversation-replay.golden.txt b/packages/ui/tui/tests/snapshots/conversation-replay.golden.txt new file mode 100644 index 0000000000..2395db555b --- /dev/null +++ b/packages/ui/tui/tests/snapshots/conversation-replay.golden.txt @@ -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| +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| +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| +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| +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| diff --git a/packages/ui/tui/tests/snapshots/conversation-streaming.golden.txt b/packages/ui/tui/tests/snapshots/conversation-streaming.golden.txt new file mode 100644 index 0000000000..55044a44f9 --- /dev/null +++ b/packages/ui/tui/tests/snapshots/conversation-streaming.golden.txt @@ -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| +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| +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| +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| +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| +27| " Reasoning " + style 1-9 fg=bright-black italic +28| " Inspecting width and styles. " + style 1-28 fg=bright-black italic +29| +30| " Assistant " + style 1-9 fg=bright-magenta bold +31| " Streaming visible state… " + style 11-23 bold +32| +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 diff --git a/packages/ui/tui/tests/snapshots/cordis-tools-complete.golden.txt b/packages/ui/tui/tests/snapshots/cordis-tools-complete.golden.txt new file mode 100644 index 0000000000..d33b56d7ce --- /dev/null +++ b/packages/ui/tui/tests/snapshots/cordis-tools-complete.golden.txt @@ -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| +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| +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| +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| diff --git a/packages/ui/tui/tests/snapshots/cordis-tools-pending.golden.txt b/packages/ui/tui/tests/snapshots/cordis-tools-pending.golden.txt new file mode 100644 index 0000000000..eb44cb6b94 --- /dev/null +++ b/packages/ui/tui/tests/snapshots/cordis-tools-pending.golden.txt @@ -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| +6| "▌ ◌ Inspect cordis runtime: tools " + style 0-0 fg=yellow + style 2-2 fg=yellow bold + style 3-32 bold +7| +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| +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| diff --git a/packages/ui/tui/tests/snapshots/disposed-terminal.golden.txt b/packages/ui/tui/tests/snapshots/disposed-terminal.golden.txt new file mode 100644 index 0000000000..6a008f4032 --- /dev/null +++ b/packages/ui/tui/tests/snapshots/disposed-terminal.golden.txt @@ -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| +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| +12| " Unknown command: /unknown-advanced-command " + style 1-42 fg=yellow +13| +14| " provider stream failed after partial output " + style 1-43 fg=red +15| +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| diff --git a/packages/ui/tui/tests/snapshots/dynamic-workflow-complete.golden.txt b/packages/ui/tui/tests/snapshots/dynamic-workflow-complete.golden.txt new file mode 100644 index 0000000000..00da73a956 --- /dev/null +++ b/packages/ui/tui/tests/snapshots/dynamic-workflow-complete.golden.txt @@ -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| +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| diff --git a/packages/ui/tui/tests/snapshots/dynamic-workflow-pending.golden.txt b/packages/ui/tui/tests/snapshots/dynamic-workflow-pending.golden.txt new file mode 100644 index 0000000000..0e9849bc76 --- /dev/null +++ b/packages/ui/tui/tests/snapshots/dynamic-workflow-pending.golden.txt @@ -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| +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| diff --git a/packages/ui/tui/tests/snapshots/errors-and-help.golden.txt b/packages/ui/tui/tests/snapshots/errors-and-help.golden.txt new file mode 100644 index 0000000000..fb7a119735 --- /dev/null +++ b/packages/ui/tui/tests/snapshots/errors-and-help.golden.txt @@ -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| +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| +12| " Unknown command: /unknown-advanced-command " + style 1-42 fg=yellow +13| +14| " provider stream failed after partial output " + style 1-43 fg=red +15| +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| diff --git a/packages/ui/tui/tests/snapshots/question-dialog-validation.golden.txt b/packages/ui/tui/tests/snapshots/question-dialog-validation.golden.txt new file mode 100644 index 0000000000..3704b8e510 --- /dev/null +++ b/packages/ui/tui/tests/snapshots/question-dialog-validation.golden.txt @@ -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| diff --git a/packages/ui/tui/tests/snapshots/question-dialog.golden.txt b/packages/ui/tui/tests/snapshots/question-dialog.golden.txt new file mode 100644 index 0000000000..8c1565fdda --- /dev/null +++ b/packages/ui/tui/tests/snapshots/question-dialog.golden.txt @@ -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| diff --git a/packages/ui/tui/tests/snapshots/surface-after-compaction-narrow.golden.txt b/packages/ui/tui/tests/snapshots/surface-after-compaction-narrow.golden.txt new file mode 100644 index 0000000000..429cebfb84 --- /dev/null +++ b/packages/ui/tui/tests/snapshots/surface-after-compaction-narrow.golden.txt @@ -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| +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| diff --git a/packages/ui/tui/tests/snapshots/surface-after-compaction-wide.golden.txt b/packages/ui/tui/tests/snapshots/surface-after-compaction-wide.golden.txt new file mode 100644 index 0000000000..06380259af --- /dev/null +++ b/packages/ui/tui/tests/snapshots/surface-after-compaction-wide.golden.txt @@ -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| +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| diff --git a/packages/ui/tui/tests/snapshots/surface-before-compaction.golden.txt b/packages/ui/tui/tests/snapshots/surface-before-compaction.golden.txt new file mode 100644 index 0000000000..eaad66daf8 --- /dev/null +++ b/packages/ui/tui/tests/snapshots/surface-before-compaction.golden.txt @@ -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| +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| +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 diff --git a/packages/ui/tui/tests/tui.snapshot.ts b/packages/ui/tui/tests/tui.snapshot.ts new file mode 100644 index 0000000000..eb6d5e9965 --- /dev/null +++ b/packages/ui/tui/tests/tui.snapshot.ts @@ -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 void> + +const observedCheckpoints = new Set() + +async function checkpoint( + name: Checkpoint, + terminal: HeadlessTerminal, + options: TerminalSnapshotOptions = {}, +): Promise { + 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 { + 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 { + const before = harness.terminal.frames + action() + await harness.terminal.waitForFrame(before) +} + +async function disposeSnapshot(harness: SnapshotHarness): Promise { + await disposeTuiTestHarness(harness) + await harness.terminal.dispose() +} + +async function configureAdvancedTools(ctx: Context): Promise { + 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, + result?: NonNullable, +): ToolDefinition { + return { + name, + description: `${name} snapshot fixture`, + parameters: {}, + execute: () => Promise.resolve([]), + presentCall: call, + ...result === undefined ? {} : { presentResult: result }, + } +} + +const ADVANCED_CARD_TOOLS: Record = { + 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()) +}) diff --git a/packages/ui/tui/tests/tui.spec.ts b/packages/ui/tui/tests/tui.spec.ts index 6cc8e60243..5fc11cbe40 100644 --- a/packages/ui/tui/tests/tui.spec.ts +++ b/packages/ui/tui/tests/tui.spec.ts @@ -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 { await new Promise(resolve => setTimeout(resolve, 25)) } -async function setup(options: { - status?: AgentStatus - config?: Config - tools?: Record - 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>): Promise { - 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', () => { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2dccd293c9..164fbed86f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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 diff --git a/vitest.snapshot.config.ts b/vitest.snapshot.config.ts index 9753176f9d..18127b55ee 100644 --- a/vitest.snapshot.config.ts +++ b/vitest.snapshot.config.ts @@ -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. From bea6a74ea0f44d1d9e0f0b5dde331a23f4c552ff Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Sun, 19 Jul 2026 01:09:20 +0800 Subject: [PATCH 06/13] refactor(examples): give TUI its own agent leaf --- docs/graph-atlas.md | 1 + examples/README.md | 8 ++++- examples/coding-agent/README.md | 8 ++--- examples/coding-agent/code-mode.cordis.yml | 2 ++ examples/coding-agent/composition.md | 2 +- examples/coding-agent/cordis.yml | 12 +++----- examples/tui-agent/README.md | 19 ++++++++++++ examples/tui-agent/code-mode.cordis.yml | 30 +++++++++++++++++++ examples/tui-agent/composition.md | 28 +++++++++++++++++ examples/tui-agent/cordis.yml | 28 +++++++++++++++++ examples/tui-agent/package.json | 7 +++++ .../tests/fixtures/tui-scripted-llm.ts | 0 .../tests/fixtures/tui-scripted.cordis.yml | 0 .../tests/tui-keyless-smoke.e2e.ts | 8 ++--- knip.json | 2 +- package.json | 1 + scripts/demo-code-mode.mjs | 7 +++-- scripts/gen-doc-graphs.ts | 21 +++++++++++-- 18 files changed, 159 insertions(+), 25 deletions(-) create mode 100644 examples/tui-agent/README.md create mode 100644 examples/tui-agent/code-mode.cordis.yml create mode 100644 examples/tui-agent/composition.md create mode 100644 examples/tui-agent/cordis.yml create mode 100644 examples/tui-agent/package.json rename examples/{coding-agent => tui-agent}/tests/fixtures/tui-scripted-llm.ts (100%) rename examples/{coding-agent => tui-agent}/tests/fixtures/tui-scripted.cordis.yml (100%) rename examples/{coding-agent => tui-agent}/tests/tui-keyless-smoke.e2e.ts (95%) diff --git a/docs/graph-atlas.md b/docs/graph-atlas.md index 60de01ef81..5220cfa81f 100644 --- a/docs/graph-atlas.md +++ b/docs/graph-atlas.md @@ -14,6 +14,7 @@ The process decision behind this index is recorded in [the documentation graph R | [capability seams and core services](capability-seams.md) | `hybrid generated` | | [echo-agent app composition](../examples/echo-agent/composition.md) | `hybrid generated` | | [coding-agent app composition](../examples/coding-agent/composition.md) | `hybrid generated` | +| [tui-agent app composition](../examples/tui-agent/composition.md) | `hybrid generated` | | [cordis-agent app composition](../examples/cordis-agent/composition.md) | `hybrid generated` | | [acp-agent app composition](../examples/acp-agent/composition.md) | `hybrid generated` | | [event producer/consumer matrix](event-producer-consumer.md) | `hybrid generated` | diff --git a/examples/README.md b/examples/README.md index 25c017824f..03b798e164 100644 --- a/examples/README.md +++ b/examples/README.md @@ -15,12 +15,18 @@ Run with: `pnpm run demo:echo`. When prompted, type "echo " to trigge ## coding-agent -A coding-agent demo: DeepSeek V4 + the `read`/`write`/`edit` filesystem tools + the bash tool suite, `subagent` delegation, and the `todo_write` task tracker on the same `@deepseek-ai/dsh-stdio-demo` app. Interactive runs use the pi-tui coding interface; pipes use readline. +A coding-agent REPL: DeepSeek V4 + the `read`/`write`/`edit` filesystem tools + the bash tool suite, `subagent` delegation, and the `todo_write` task tracker on the `@deepseek-ai/dsh-stdio-demo` app's readline front door. Run with: `pnpm run demo:repl` (needs `DEEPSEEK_API_KEY` in the environment or a gitignored repo-root `.env`). See [coding-agent/README.md](coding-agent/README.md) for details. Run the Code Mode overlay with `pnpm run demo:code-mode`, or pass `acp` for the ACP example. See the [Code Mode example](coding-agent/README.md#code-mode) for its composition and a sample task. +## tui-agent + +The full-screen terminal sibling of `coding-agent`: it reuses the same coding backends and tools while forcing the shared terminal app to `dsh-tui`. It is the home of TUI PTY and snapshot scenarios. + +Run with: `pnpm run demo:tui` (needs `DEEPSEEK_API_KEY`). See [tui-agent/README.md](tui-agent/README.md) for controls and composition. + ## cordis-agent The **self-referential** demo: the coding spine plus [`@deepseek-ai/dsh-tool-cordis`](../packages/cordis/tool-cordis), whose three tools (`cordis_inspect` / `cordis_mount` / `cordis_unmount`) let the agent inspect the live cordis runtime it runs inside, mount model-written plugins into it (an event listener, a brand-new tool for itself, or a service another mount injects), and dispose them again — all dynamic mounts grouped under one `cordis-dynamic` fiber subtree. The `ctx.fs`/`ctx.web` services ride along provider-only, as the capabilities those plugins build on. diff --git a/examples/coding-agent/README.md b/examples/coding-agent/README.md index 9fdadf809d..f94cd31adb 100644 --- a/examples/coding-agent/README.md +++ b/examples/coding-agent/README.md @@ -1,6 +1,6 @@ # coding-agent -The coding-agent demo wiring: DeepSeek V4 + the `read`/`write`/`edit` filesystem tools + the bash tool suite + subagent delegation + `todo_write` + terminal chat + JSONL persistence, loaded from `cordis.yml`. Interactive runs use the pi-tui coding interface; piped runs use readline. +The coding-agent REPL wiring: DeepSeek V4 + the `read`/`write`/`edit` filesystem tools + the bash tool suite + subagent delegation + `todo_write` + readline chat + JSONL persistence, loaded from `cordis.yml`. The sibling [`tui-agent`](../tui-agent/README.md) fixes the same agent composition to the full-screen terminal front door. ## Run it @@ -13,7 +13,7 @@ pnpm run demo:repl Type a coding task. The agent works through the `read`/`write`/`edit` filesystem tools for ordinary file operations and `bash` (+ the generic `task_output` / `task_list` / `task_kill` for background tasks) for shell commands, searches, and test runs, each in a fresh `bash -c` (the system prompt tells the model to pass `workdir` instead of `cd`). Both the fs tools and bash resolve relative paths against the session workspace. It can also delegate with `subagent`/`subagent_fork` and track multi-step work with `todo_write`. -The TUI renders resumed Markdown history, reasoning, tool-owned terminal/diff/generic cards, token totals, and the latest todo list. Enter submits or steers while the agent is running; Ctrl+O expands cards, Ctrl+R toggles reasoning, Escape cancels, and `/help` lists commands. `ask_user_question` opens a keyboard-driven overlay rather than taking over the editor. +The REPL renders reasoning, tool calls/results, and the latest todo list as line-oriented output suitable for terminals and pipes. Use `pnpm run demo:tui` for the interactive Markdown/card interface. ### Resuming a prior session @@ -42,14 +42,14 @@ and watch the transcript: one `run_code` call, a program looping over tools, and ## What each leaf entry demonstrates -This example is a thin leaf `cordis.yml`: it picks the swappable backends, loads one app package, and adds product tools that are intentionally outside the shared spine. The spine (sessions, system-prompt, tools, agents, invariants, `agent-loop`) and the front-door cluster (JSONL persistence, TTY-selected `dsh-tui`/`dsh-stdio` channels, the pre-created `main` agent) live inside the [`@deepseek-ai/dsh-stdio-demo`](../../packages/examples/stdio-demo) app and the [`@deepseek-ai/dsh-agent-spine-demo`](../../packages/examples/agent-spine-demo) bundle it loads; the leaf wires the backends and model-facing optional tools: +This example is a thin leaf `cordis.yml`: it picks the swappable backends, loads one app package, and adds product tools that are intentionally outside the shared spine. The spine (sessions, system-prompt, tools, agents, invariants, `agent-loop`) and the front-door cluster (JSONL persistence, the selected terminal channel, the pre-created `main` agent) live inside the [`@deepseek-ai/dsh-stdio-demo`](../../packages/examples/stdio-demo) app and the [`@deepseek-ai/dsh-agent-spine-demo`](../../packages/examples/agent-spine-demo) bundle it loads; the leaf wires the backends and model-facing optional tools: | Entry | Demonstrates | |---|---| | `hmr` (`@cordisjs/plugin-hmr`) | the dev/demo edit-reload loop — a **leaf** entry (not baked into the app) because it is Loader-only and needs `node --expose-internals`, which `demo:repl` passes | | `llm-deepseek` | real `LlmAdapter` via config (`!!js process.env.…` secrets); swap one line to `@deepseek-ai/dsh-llm-pi-ai` for the library-backed twin | | `bash` (`dsh-bash-local`) | the executor implementation — the swappable half of the bash seam. The model-facing `bash` schema (`tool-bash`) and generic `task_*` controls (`tool-tasks`) come from `dsh-agent-spine-demo`, so only the executor is a leaf choice | -| `stdio-agent` (`@deepseek-ai/dsh-stdio-demo`) | the app bundle: the agent-spine demo + JSONL persistence + TTY-selected `dsh-tui`/`dsh-stdio` channels + a pre-created `main` agent. Its config carries the model, system prompt, `persistenceRoot` (`./.sessions`), `resumeSessionId`, and optional `ui` presentation settings | +| `stdio-agent` (`@deepseek-ai/dsh-stdio-demo`) | the app bundle: the agent-spine demo + JSONL persistence + the configured terminal channel + a pre-created `main` agent. This leaf fixes `ui.mode` to `readline`; `tui-agent` owns the corresponding TUI leaf | | `subagent`, `subagent-spawn`, `subagent-fork` | the subagent provider registry plus the two in-process backends: a fresh child and a child seeded with the parent's completed-turn prefix | | `tool-subagent`, `tool-subagent-fork` | two model-facing `dsh-tool-subagent` loads, each bound to a different provider and exposed under a distinct tool name (`subagent`, `subagent_fork`) | | `tool-todo` | the model-facing `todo_write` tool; writes the whole task list to the session log and renders as a persistent TUI plan or readline checklist | diff --git a/examples/coding-agent/code-mode.cordis.yml b/examples/coding-agent/code-mode.cordis.yml index 3fec40e0f3..b1f62829bc 100644 --- a/examples/coding-agent/code-mode.cordis.yml +++ b/examples/coding-agent/code-mode.cordis.yml @@ -20,6 +20,8 @@ tools: mode: code welcome: 'code-mode agent ready. Give it a multi-tool task.' + ui: + mode: readline persona: | You are coding-agent, a coding assistant powered by the {{model}} model. diff --git a/examples/coding-agent/composition.md b/examples/coding-agent/composition.md index d0411e43d9..2956291dc8 100644 --- a/examples/coding-agent/composition.md +++ b/examples/coding-agent/composition.md @@ -18,7 +18,7 @@ flowchart LR cfg --> plugin_coding_stdio_agent plugin_coding_stdio_agent --> bundle_agent_core["@deepseek-ai/dsh-agent-spine-demo"] plugin_coding_stdio_agent --> bundle_jsonl["@deepseek-ai/dsh-session-persistence-jsonl"] - plugin_coding_stdio_agent --> frontdoor_stdio["dsh-tui (TTY) / dsh-stdio (pipes)
pre-created main agent"] + plugin_coding_stdio_agent --> frontdoor_stdio["@deepseek-ai/dsh-stdio
pre-created main agent"] bundle_agent_core --> spine_llm["ctx.llm"] bundle_agent_core --> spine_sessions["ctx.sessions"] bundle_agent_core --> spine_tools["ctx.tools + tool-bash"] diff --git a/examples/coding-agent/cordis.yml b/examples/coding-agent/cordis.yml index d3da67ce29..497e2a80c8 100644 --- a/examples/coding-agent/cordis.yml +++ b/examples/coding-agent/cordis.yml @@ -1,7 +1,6 @@ -# Coding agent with swappable DeepSeek and local-bash backends. `dsh-stdio-demo` -# supplies the agent spine, workspace instructions, generic task controls, -# JSONL persistence, TTY-selected `dsh-tui`/`dsh-stdio` terminal front doors, -# readline logging, and `main` agent. +# Readline coding REPL with swappable DeepSeek and local-bash backends. +# `dsh-stdio-demo` supplies the agent spine, workspace instructions, generic +# task controls, JSONL persistence, the line-oriented front door, and `main`. # HMR remains a leaf because it requires Loader internals; `demo:repl` passes # `--expose-internals`. The app bin loads the gitignored root `.env`; this file # reads `DEEPSEEK_API_KEY` and optional `DEEPSEEK_BASE_URL` through `!!js`. @@ -39,10 +38,7 @@ maxBytes: 65536 welcome: 'agent REPL ready. Give it a coding task.' ui: - mode: auto - tui: - showReasoning: true - maxToolOutputLines: 12 + mode: readline # Keep the persona to identity and behavior; tool plugins own tool guidance. # The loop resolves {{model}} from this agent's configuration. persona: | diff --git a/examples/tui-agent/README.md b/examples/tui-agent/README.md new file mode 100644 index 0000000000..909ccf700d --- /dev/null +++ b/examples/tui-agent/README.md @@ -0,0 +1,19 @@ +# tui-agent + +The full-screen terminal counterpart to the [`coding-agent`](../coding-agent/README.md) readline REPL and [`acp-agent`](../acp-agent/README.md) server. It reuses the coding agent's backends and tool composition, then fixes the shared terminal app to the `dsh-tui` front door. + +## Run it + +```sh +pnpm run demo:tui +``` + +The command needs `DEEPSEEK_API_KEY` in the environment or the gitignored repository-root `.env`. Set `RESUME_SESSION_ID` to reopen a persisted conversation under `./.sessions`. + +The TUI renders Markdown history, reasoning, tool-owned terminal/diff/generic cards, token totals, and the latest todo list. Enter submits or steers while the agent is running; Ctrl+O expands cards, Ctrl+R toggles reasoning, Escape cancels, and `/help` lists commands. `ask_user_question` opens a keyboard-driven overlay. + +Run `pnpm run demo:code-mode tui` for the sibling Code Mode overlay. + +## Composition + +[`cordis.yml`](cordis.yml) includes the readline coding-agent leaf so the LLM, bash, filesystem, compaction, subagent, workflow, todo, timeout, and spill choices have one owner. Its asserted patch replaces only the terminal app config and forces `ui.mode: tui`; [`code-mode.cordis.yml`](code-mode.cordis.yml) applies the same front-door patch to the coding agent's Code Mode overlay. diff --git a/examples/tui-agent/code-mode.cordis.yml b/examples/tui-agent/code-mode.cordis.yml new file mode 100644 index 0000000000..7bb7932ba0 --- /dev/null +++ b/examples/tui-agent/code-mode.cordis.yml @@ -0,0 +1,30 @@ +# Code Mode keeps the TUI front door while reusing the coding-agent overlay's +# worker runtime and one-tool registry composition. +- id: base + name: '@cordisjs/plugin-include' + config: + path: ../coding-agent/code-mode.cordis.yml + patches: + - id: stdio-agent + name: '@deepseek-ai/dsh-stdio-demo' + config: + provider: deepseek + model: deepseek-v4-flash + resumeSessionId: !!js process.env.RESUME_SESSION_ID + persistenceRoot: './.sessions' + workspaceContext: + maxBytes: 65536 + tools: + mode: code + welcome: 'TUI Code Mode ready. Give it a multi-tool task.' + ui: + mode: tui + tui: + showReasoning: true + maxToolOutputLines: 12 + persona: | + You are coding-agent, a coding assistant powered by the {{model}} model. + + You work by writing TypeScript programs for run_code: batch related + tool work into one program, loop and branch where it helps, and print + or return ONLY the findings that matter. diff --git a/examples/tui-agent/composition.md b/examples/tui-agent/composition.md new file mode 100644 index 0000000000..511f76d41e --- /dev/null +++ b/examples/tui-agent/composition.md @@ -0,0 +1,28 @@ + + +# TUI Agent App Composition + +The TUI agent reuses the coding-agent backend and tool composition while fixing the shared terminal app to the full-screen dsh-tui front door. + +```mermaid +flowchart LR + cfg["examples/tui-agent
cordis.yml"] + plugin_tui_base["base
@deepseek-ai/dsh-stdio-demo"] + cfg --> plugin_tui_base + plugin_tui_base --> bundle_agent_core["@deepseek-ai/dsh-agent-spine-demo"] + plugin_tui_base --> bundle_jsonl["@deepseek-ai/dsh-session-persistence-jsonl"] + plugin_tui_base --> frontdoor_stdio["@deepseek-ai/dsh-tui
pre-created main agent"] + bundle_agent_core --> spine_llm["ctx.llm"] + bundle_agent_core --> spine_sessions["ctx.sessions"] + bundle_agent_core --> spine_tools["ctx.tools + tool-bash"] + bundle_agent_core --> spine_loop["ctx.agents + ctx.agentLoop"] +``` + +| Plugin id | Package / module | +| --- | --- | +| `base` | `@deepseek-ai/dsh-stdio-demo` | + +Source config: [`examples/tui-agent/cordis.yml`](cordis.yml). + +Maintenance mode: hybrid: the leaf plugin list is parsed from its `cordis.yml`; app package expansion is curated from package source. diff --git a/examples/tui-agent/cordis.yml b/examples/tui-agent/cordis.yml new file mode 100644 index 0000000000..0f329c38e2 --- /dev/null +++ b/examples/tui-agent/cordis.yml @@ -0,0 +1,28 @@ +# Full-screen TUI front door over the same coding-agent composition used by the +# readline REPL. The include keeps backends and optional tools aligned; the +# patch owns only the terminal-specific app config. +- id: base + name: '@cordisjs/plugin-include' + config: + path: ../coding-agent/cordis.yml + patches: + - id: stdio-agent + name: '@deepseek-ai/dsh-stdio-demo' + config: + provider: deepseek + model: deepseek-v4-flash + resumeSessionId: !!js process.env.RESUME_SESSION_ID + persistenceRoot: './.sessions' + workspaceContext: + maxBytes: 65536 + welcome: 'TUI agent ready. Give it a coding task.' + ui: + mode: tui + tui: + showReasoning: true + maxToolOutputLines: 12 + persona: | + You are coding-agent, a coding assistant powered by the {{model}} model. + + Verify your work by running the code or tests. Keep answers brief and + factual. diff --git a/examples/tui-agent/package.json b/examples/tui-agent/package.json new file mode 100644 index 0000000000..f45e6746a3 --- /dev/null +++ b/examples/tui-agent/package.json @@ -0,0 +1,7 @@ +{ + "name": "tui-agent-example", + "private": true, + "version": "0.0.1", + "type": "module", + "description": "Runnable demo: the coding agent through the full-screen terminal UI" +} diff --git a/examples/coding-agent/tests/fixtures/tui-scripted-llm.ts b/examples/tui-agent/tests/fixtures/tui-scripted-llm.ts similarity index 100% rename from examples/coding-agent/tests/fixtures/tui-scripted-llm.ts rename to examples/tui-agent/tests/fixtures/tui-scripted-llm.ts diff --git a/examples/coding-agent/tests/fixtures/tui-scripted.cordis.yml b/examples/tui-agent/tests/fixtures/tui-scripted.cordis.yml similarity index 100% rename from examples/coding-agent/tests/fixtures/tui-scripted.cordis.yml rename to examples/tui-agent/tests/fixtures/tui-scripted.cordis.yml diff --git a/examples/coding-agent/tests/tui-keyless-smoke.e2e.ts b/examples/tui-agent/tests/tui-keyless-smoke.e2e.ts similarity index 95% rename from examples/coding-agent/tests/tui-keyless-smoke.e2e.ts rename to examples/tui-agent/tests/tui-keyless-smoke.e2e.ts index cc2397c069..26cee8aea2 100644 --- a/examples/coding-agent/tests/tui-keyless-smoke.e2e.ts +++ b/examples/tui-agent/tests/tui-keyless-smoke.e2e.ts @@ -53,7 +53,7 @@ while time.monotonic() < deadline: 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: + if scenario == "boot" and not sent_exit and b"TUI agent ready." in output: os.write(fd, b"/exit\r") sent_exit = True waited, candidate = os.waitpid(pid, os.WNOHANG) @@ -101,7 +101,7 @@ interface TuiLoaderSmokeOptions { } async function runTuiLoaderSmoke(options: TuiLoaderSmokeOptions = {}): Promise { - const cwd = await mkdtemp(join(tmpdir(), 'coding-tui-smoke-')) + const cwd = await mkdtemp(join(tmpdir(), 'tui-agent-smoke-')) try { const launch = resolveExampleLaunch({ srcBin: binScript, @@ -142,11 +142,11 @@ async function runTuiLoaderSmoke(options: TuiLoaderSmokeOptions = {}): Promise { +describe('tui-agent keyless smoke (real Loader tree in a PTY)', () => { it('boots pi-tui, renders the configured banner, accepts /exit, and restores the terminal', async () => { const output = await runTuiLoaderSmoke() expect(output).toContain('DEEPSEEK') - expect(output).toContain('agent REPL ready.') + expect(output).toContain('TUI agent ready.') expect(output).toContain('\u001B[?2004l') }, LOADER_SMOKE_TEST_TIMEOUT_MS) diff --git a/knip.json b/knip.json index 03fae14896..db77587b48 100644 --- a/knip.json +++ b/knip.json @@ -10,7 +10,7 @@ "examples": { "entry": [ "echo-agent/src/*.ts", - "coding-agent/tests/fixtures/tui-scripted-llm.ts", + "tui-agent/tests/fixtures/tui-scripted-llm.ts", "*/tests/**/*.e2e.ts", "*/tests/**/*.snapshot.ts" ], diff --git a/package.json b/package.json index 39d6a53161..ca85fd32d2 100644 --- a/package.json +++ b/package.json @@ -80,6 +80,7 @@ "hygiene": "pnpm run knip && pnpm run publint && pnpm run constraints && pnpm run verify-cordis-config && pnpm run verify-node-next-types && pnpm run verify-runtime-closure", "demo:echo": "node --expose-internals --import tsx packages/examples/stdio-demo/src/bin.ts examples/echo-agent/cordis.yml", "demo:repl": "node --expose-internals --import tsx packages/examples/stdio-demo/src/bin.ts examples/coding-agent/cordis.yml", + "demo:tui": "node --expose-internals --import tsx packages/examples/stdio-demo/src/bin.ts examples/tui-agent/cordis.yml", "demo:code-mode": "node scripts/demo-code-mode.mjs", "demo:cordis": "node --expose-internals --import tsx packages/examples/stdio-demo/src/bin.ts examples/cordis-agent/cordis.yml", "demo:acp": "node --import tsx packages/examples/acp-demo/src/bin.ts --config examples/acp-agent/cordis.yml", diff --git a/scripts/demo-code-mode.mjs b/scripts/demo-code-mode.mjs index edfc26b4d5..7a5e054cc0 100644 --- a/scripts/demo-code-mode.mjs +++ b/scripts/demo-code-mode.mjs @@ -1,7 +1,7 @@ /** - * Boot the REPL or ACP Code Mode overlay, defaulting to REPL. Each overlay + * Boot the REPL, TUI, or ACP Code Mode overlay, defaulting to REPL. Each overlay * includes its base example, selects Code Mode, and adds the worker runtime. - * Both require a DeepSeek API key; unsupported arguments fail with usage. + * All require a DeepSeek API key; unsupported arguments fail with usage. */ import { spawn } from 'node:child_process' @@ -10,13 +10,14 @@ import { spawn } from 'node:child_process' // Loader's HMR path). const UIS = new Map([ ['repl', ['--expose-internals', '--import', 'tsx', 'packages/examples/stdio-demo/src/bin.ts', 'examples/coding-agent/code-mode.cordis.yml']], + ['tui', ['--expose-internals', '--import', 'tsx', 'packages/examples/stdio-demo/src/bin.ts', 'examples/tui-agent/code-mode.cordis.yml']], ['acp', ['--import', 'tsx', 'packages/examples/acp-demo/src/bin.ts', '--config', 'examples/acp-agent/code-mode.cordis.yml']], ]) const ui = process.argv[2] ?? 'repl' const args = UIS.get(ui) if (!args || process.argv.length > 3) { - console.error('usage: pnpm run demo:code-mode [repl|acp]') + console.error('usage: pnpm run demo:code-mode [repl|tui|acp]') process.exit(2) } diff --git a/scripts/gen-doc-graphs.ts b/scripts/gen-doc-graphs.ts index 5bf0a9456c..29cea3f9a0 100644 --- a/scripts/gen-doc-graphs.ts +++ b/scripts/gen-doc-graphs.ts @@ -434,6 +434,14 @@ const APP_EXAMPLES = [ config: 'examples/coding-agent/cordis.yml', summary: 'The coding-agent demo adds the real DeepSeek adapter, filesystem tools, todo_write, compaction, and both subagent transports on top of the stdio app package.', }, + { + id: 'tui', + rel: 'examples/tui-agent/composition.md', + title: 'TUI Agent App Composition', + label: 'examples/tui-agent', + config: 'examples/tui-agent/cordis.yml', + summary: 'The TUI agent reuses the coding-agent backend and tool composition while fixing the shared terminal app to the full-screen dsh-tui front door.', + }, { id: 'cordis', rel: 'examples/cordis-agent/composition.md', @@ -454,13 +462,18 @@ const APP_EXAMPLES = [ type AppExample = typeof APP_EXAMPLES[number] -function renderAppExpansion(lines: string[], appNode: string, pluginName: string): void { +function renderAppExpansion(lines: string[], appNode: string, pluginName: string, exampleId: string): void { const agentCore = nodeId('bundle', 'agent_core') const jsonl = nodeId('bundle', 'jsonl') lines.push(` ${appNode} --> ${agentCore}["@deepseek-ai/dsh-agent-spine-demo"]`) lines.push(` ${appNode} --> ${jsonl}["@deepseek-ai/dsh-session-persistence-jsonl"]`) if (pluginName === '@deepseek-ai/dsh-stdio-demo') { - lines.push(` ${appNode} --> ${nodeId('frontdoor', 'stdio')}["dsh-tui (TTY) / dsh-stdio (pipes)
pre-created main agent"]`) + const frontDoor = exampleId === 'tui' + ? '@deepseek-ai/dsh-tui
pre-created main agent' + : exampleId === 'coding' + ? '@deepseek-ai/dsh-stdio
pre-created main agent' + : 'dsh-tui (TTY) / dsh-stdio (pipes)
pre-created main agent' + lines.push(` ${appNode} --> ${nodeId('frontdoor', 'stdio')}["${frontDoor}"]`) } else if (pluginName === '@deepseek-ai/dsh-acp-demo') { lines.push(` ${appNode} --> ${nodeId('frontdoor', 'acp')}["@deepseek-ai/dsh-acp
JSON-RPC stdio bridge
sessions created by client"]`) } @@ -488,7 +501,7 @@ function renderAppComposition(example: AppExample): string { lines.push(` ${pluginNode}["${escLabel(plugin.id)}
${escLabel(plugin.name)}"]`) lines.push(` cfg --> ${pluginNode}`) if (plugin.name === '@deepseek-ai/dsh-stdio-demo' || plugin.name === '@deepseek-ai/dsh-acp-demo') { - renderAppExpansion(lines, pluginNode, plugin.name) + renderAppExpansion(lines, pluginNode, plugin.name, example.id) } } lines.push( @@ -978,6 +991,7 @@ function renderIndex(docs: GraphDoc[]): string { 'docs/capability-seams.md': 'capability seams and core services', 'examples/echo-agent/composition.md': 'echo-agent app composition', 'examples/coding-agent/composition.md': 'coding-agent app composition', + 'examples/tui-agent/composition.md': 'tui-agent app composition', 'examples/cordis-agent/composition.md': 'cordis-agent app composition', 'examples/acp-agent/composition.md': 'acp-agent app composition', 'docs/event-producer-consumer.md': 'event producer/consumer matrix', @@ -989,6 +1003,7 @@ function renderIndex(docs: GraphDoc[]): string { 'docs/capability-seams.md': 'hybrid generated', 'examples/echo-agent/composition.md': 'hybrid generated', 'examples/coding-agent/composition.md': 'hybrid generated', + 'examples/tui-agent/composition.md': 'hybrid generated', 'examples/cordis-agent/composition.md': 'hybrid generated', 'examples/acp-agent/composition.md': 'hybrid generated', 'docs/event-producer-consumer.md': 'hybrid generated', From e68cef1be1dff8cde8a04556969af400ecb964fa Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Sun, 19 Jul 2026 01:31:55 +0800 Subject: [PATCH 07/13] test(tui): replay recorded sessions through real tools --- ...-18-tui-terminal-state-snapshots.i18n.yaml | 4 +- ...2026-07-18-tui-terminal-state-snapshots.md | 67 ++-- ...6-07-18-tui-terminal-state-snapshots.zh.md | 65 ++-- docs/testing.md | 4 +- examples/tui-agent/README.md | 4 + .../bash-terminal-card/session.jsonl | 98 ++++++ .../bash-terminal-card/terminal.golden.txt | 72 ++++ .../tests/snapshots/code-mode/session.jsonl | 150 ++++++++ .../snapshots/code-mode/terminal.golden.txt | 79 +++++ .../cordis-dynamic-toolchain/session.1.jsonl | 13 + .../cordis-dynamic-toolchain/session.2.jsonl | 13 + .../cordis-dynamic-toolchain/session.jsonl | 64 ++++ .../terminal.golden.txt | 115 ++++++ .../dynamic-workflow/session.1.jsonl | 36 ++ .../snapshots/dynamic-workflow/session.jsonl | 209 +++++++++++ .../dynamic-workflow/terminal.golden.txt | 105 ++++++ .../multi-turn-conversation/session.jsonl | 65 ++++ .../terminal.golden.txt | 69 ++++ .../parallel-file-reads/session.jsonl | 28 ++ .../parallel-file-reads/terminal.golden.txt | 94 +++++ .../parallel-file-reads/workspace/a.txt | 1 + .../parallel-file-reads/workspace/b.txt | 1 + .../tests/snapshots/todo-plan/session.jsonl | 134 +++++++ .../snapshots/todo-plan/terminal.golden.txt | 81 +++++ examples/tui-agent/tests/tui.snapshot.ts | 329 ++++++++++++++++++ .../snapshots/code-mode-complete.golden.txt | 50 --- .../conversation-complete.golden.txt | 75 ---- .../snapshots/conversation-replay.golden.txt | 73 ---- .../conversation-streaming.golden.txt | 81 ++--- .../cordis-tools-complete.golden.txt | 73 ---- .../dynamic-workflow-complete.golden.txt | 53 --- packages/ui/tui/tests/tui.snapshot.ts | 57 +-- 32 files changed, 1874 insertions(+), 488 deletions(-) create mode 100644 examples/tui-agent/tests/snapshots/bash-terminal-card/session.jsonl create mode 100644 examples/tui-agent/tests/snapshots/bash-terminal-card/terminal.golden.txt create mode 100644 examples/tui-agent/tests/snapshots/code-mode/session.jsonl create mode 100644 examples/tui-agent/tests/snapshots/code-mode/terminal.golden.txt create mode 100644 examples/tui-agent/tests/snapshots/cordis-dynamic-toolchain/session.1.jsonl create mode 100644 examples/tui-agent/tests/snapshots/cordis-dynamic-toolchain/session.2.jsonl create mode 100644 examples/tui-agent/tests/snapshots/cordis-dynamic-toolchain/session.jsonl create mode 100644 examples/tui-agent/tests/snapshots/cordis-dynamic-toolchain/terminal.golden.txt create mode 100644 examples/tui-agent/tests/snapshots/dynamic-workflow/session.1.jsonl create mode 100644 examples/tui-agent/tests/snapshots/dynamic-workflow/session.jsonl create mode 100644 examples/tui-agent/tests/snapshots/dynamic-workflow/terminal.golden.txt create mode 100644 examples/tui-agent/tests/snapshots/multi-turn-conversation/session.jsonl create mode 100644 examples/tui-agent/tests/snapshots/multi-turn-conversation/terminal.golden.txt create mode 100644 examples/tui-agent/tests/snapshots/parallel-file-reads/session.jsonl create mode 100644 examples/tui-agent/tests/snapshots/parallel-file-reads/terminal.golden.txt create mode 100644 examples/tui-agent/tests/snapshots/parallel-file-reads/workspace/a.txt create mode 100644 examples/tui-agent/tests/snapshots/parallel-file-reads/workspace/b.txt create mode 100644 examples/tui-agent/tests/snapshots/todo-plan/session.jsonl create mode 100644 examples/tui-agent/tests/snapshots/todo-plan/terminal.golden.txt create mode 100644 examples/tui-agent/tests/tui.snapshot.ts delete mode 100644 packages/ui/tui/tests/snapshots/code-mode-complete.golden.txt delete mode 100644 packages/ui/tui/tests/snapshots/conversation-complete.golden.txt delete mode 100644 packages/ui/tui/tests/snapshots/conversation-replay.golden.txt delete mode 100644 packages/ui/tui/tests/snapshots/cordis-tools-complete.golden.txt delete mode 100644 packages/ui/tui/tests/snapshots/dynamic-workflow-complete.golden.txt diff --git a/docs/rfc/implemented/testing/2026-07-18-tui-terminal-state-snapshots.i18n.yaml b/docs/rfc/implemented/testing/2026-07-18-tui-terminal-state-snapshots.i18n.yaml index 5b682b642b..b68f7e7e78 100644 --- a/docs/rfc/implemented/testing/2026-07-18-tui-terminal-state-snapshots.i18n.yaml +++ b/docs/rfc/implemented/testing/2026-07-18-tui-terminal-state-snapshots.i18n.yaml @@ -2,5 +2,5 @@ # 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 +2026-07-18-tui-terminal-state-snapshots.md: 280c2b4faec3bd5e14a24a5df7bc901ad31718cd +2026-07-18-tui-terminal-state-snapshots.zh.md: d1c609c6ce511bba1f498b72444af662b4444578 diff --git a/docs/rfc/implemented/testing/2026-07-18-tui-terminal-state-snapshots.md b/docs/rfc/implemented/testing/2026-07-18-tui-terminal-state-snapshots.md index 225efe8de9..280c2b4fae 100644 --- a/docs/rfc/implemented/testing/2026-07-18-tui-terminal-state-snapshots.md +++ b/docs/rfc/implemented/testing/2026-07-18-tui-terminal-state-snapshots.md @@ -8,50 +8,63 @@ English | [中文](2026-07-18-tui-terminal-state-snapshots.zh.md) 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. +Component-line snapshots stop before ANSI reaches a terminal and miss cursor movement, clearing, styling, overlay composition, and reflow. Raster screenshots include font and platform rendering noise that is unrelated to the TUI contract. A completed flow built by directly appending plausible session events has another blind spot: it proves the renderer accepts those shapes, not that the production agent loop and tool implementations produce them. + +The TUI therefore needs a deterministic, reviewable representation of terminal state, recorded model journeys that execute the real downstream stack, and a smaller test at the real process and PTY boundary. ## Decision -TUI coverage has three complementary layers: +TUI coverage has four 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. +1. `packages/ui/tui/tests/tui.spec.ts` tests event mapping, input routing, disposal, and error behavior directly. +2. `packages/ui/tui/tests/tui.snapshot.ts` mounts the production TUI against a headless terminal emulator for transient states that a completed session log cannot retain: in-flight streaming, pending tool calls, overlays, expansion, compaction reflow, errors, and shutdown. +3. `examples/tui-agent/tests/tui.snapshot.ts` replays committed JSONL session logs through the production agent loop and real tools, then compares the resulting semantic terminal state. +4. `examples/tui-agent/tests/tui-keyless-smoke.e2e.ts` boots the real Loader composition in a PTY, drives a scripted conversation through streaming and `ask_user_question`, and verifies startup, input, exit, failure reporting, and terminal restoration. -The 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. +The runnable TUI has its own `examples/tui-agent` leaf beside the readline `coding-agent` and `acp-agent` leaves. It reuses the coding agent's backend and tool composition through an asserted include patch while fixing the shared terminal app to `ui.mode: tui`; TUI snapshots and PTY tests live with that leaf. -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. +### Recorded-session replay -Every checkpoint also enforces theme independence across the complete terminal state: no RGB colors, no palette entries beyond ANSI 0–15, and no explicit background colors. Reverse video remains valid for selection because it uses terminal defaults. The suite owns a closed checkpoint list: its type rejects undeclared names, and its inventory checks reject missing checkpoints and orphaned `.golden.txt` files. +Each example-level scenario directory owns `session.jsonl`, optional child logs `session..jsonl`, and `terminal.golden.txt`. The primary log supplies user-authored `user/message` prompts and the recorded `assistant/chunk` sequence. `dsh-llm-replay` derives one model-call script per session, binds child logs to fresh child sessions, and is the only mocked boundary. The agent loop, bash and filesystem implementations, Code Mode worker, subagent provider, workflow worker, Cordis tools, presenters, and TUI are production implementations. + +The suite rejects a journey when its tool-call sequence differs, an expected event count is missing, a tool result is an error, a turn ends in error, a workflow lifecycle is incomplete, or the live child-session count differs from the fixture set. These assertions prevent an attractive terminal golden from hiding a failed or bypassed production path. + +The live-model fixtures use `DSH_SNAPSHOT=record`; record mode rewrites their primary and child JSONL logs and terminal goldens. The deterministic Cordis toolchain keeps an authored complete JSONL script because reliably coercing a live model through five exact tool boundaries and two children is not a stable recording contract. `DSH_SNAPSHOT=refresh` replays every committed script keylessly and rewrites only derived terminal goldens. Plain replay compares without writing, and unknown mode values fail loud. + +### Semantic terminal projection + +The package-local `HeadlessTerminal` implements the same pi-tui `Terminal` interface as the process terminal and feeds every ANSI write into the pinned `@xterm/headless` parser. Snapshot code waits for synchronized frames to quiesce before reading state, so a checkpoint represents a completed screen rather than a timer-dependent write prefix. + +Each golden projects dimensions, active-buffer and viewport coordinates, lifecycle and cursor state, rows, wrap markers, and non-default style ranges into text. Scroll-heavy cards capture the used buffer; overlays capture the visible viewport. Text and style remain separate so a reviewer can distinguish content changes from presentation changes without decoding ANSI bytes. + +Every checkpoint enforces theme independence across the complete terminal state: no RGB colors, no palette entries beyond ANSI 0–15, and no explicit background colors. Reverse video remains valid for selection because it uses terminal defaults. Both suites own closed inventories that reject missing scenarios, missing checkpoints, and orphaned golden files. ### Required scenario matrix -| Area | Representative checkpoints | Contract pinned | +| Layer | Scenario | 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. +| Recorded journey | Multi-turn conversation | Recorded reasoning/text chunks, two input turns, retained history, token totals, and idle editor state | +| Recorded journey | Todo plan | Real `todo_write` execution, result card, and persistent plan rendering | +| Recorded journey | Bash terminal card | Real local executor output, description, exit status, and completed terminal card | +| Recorded journey | Parallel filesystem reads | Two calls from one assistant message, real file contents, ordering, and separate completed cards | +| Recorded journey | Code Mode | Real `run_code` worker execution, two `tool/code-dispatch` events, captured program output, and completed card | +| Recorded journey | Dynamic workflow | Real workflow worker, phase lifecycle, replayed child session, structured return value, and completed card | +| Recorded journey | Cordis dynamic toolchain | Real mount, Code Mode inspect, direct subagent, workflow child, unmount, and all production presenters | +| Transient state | Streaming and pending advanced calls | In-flight reasoning/text plus pending Code Mode, workflow, and Cordis cards that disappear from completed logs | +| Transient state | Cards, interaction, layout, failure, and shutdown | Collapsed/expanded card families, question validation, compaction replacement, resize reflow, help/errors, cursor restoration, and terminal stop | ## Alternatives considered - **Snapshot raw terminal writes** — rejected because differential rendering may change write boundaries without changing the screen, while cursor and clear sequences are unreadable in review. -- **Snapshot component render lines before terminal output** — rejected because it does not test ANSI parsing, cursor movement, overlays, viewport behavior, or the interaction between independent components in one frame. +- **Snapshot component render lines before terminal output** — rejected because it does not test ANSI parsing, cursor movement, overlays, viewport behavior, or independent components in one frame. +- **Build every completed flow by appending session events** — rejected because a hand-authored event sequence can drift from the agent loop, tool execution, child-session binding, or worker behavior while its presentation test stays green. Direct event construction remains limited to transient renderer states. +- **Reuse ACP stdout goldens as the TUI oracle** — rejected because a recorded model journey is transport-neutral but its presentation is not. TUI scenarios own terminal goldens while using the same JSONL replay vocabulary. - **Commit raster screenshots** — rejected because fonts, glyph metrics, antialiasing, and host terminal themes make them platform-sensitive and make semantic style changes difficult to review. - **Use only PTY end-to-end tests** — rejected because raw PTY output is a stream of historical drawing operations, not queryable final state. PTY tests retain the real Loader/input/teardown boundary, while the emulator owns broad state coverage. -- **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. +- Completed advanced snapshots now fail when the real Code Mode, workflow, subagent, filesystem, bash, or Cordis path breaks, rather than accepting a fabricated result event. +- TUI visual regressions produce readable cell-and-style diffs, while JSONL fixtures retain the exact model chunks that made the production path execute. +- The emulator uses xterm's proposed buffer API. An xterm upgrade requires rerunning and reviewing the semantic projection; terminal-specific behavior still needs the PTY smoke. +- Goldens deliberately encode wrapping and viewport behavior at fixed sizes. Intentional layout changes use keyless refresh, while model-journey changes use record mode and review both JSONL and terminal diffs. diff --git a/docs/rfc/implemented/testing/2026-07-18-tui-terminal-state-snapshots.zh.md b/docs/rfc/implemented/testing/2026-07-18-tui-terminal-state-snapshots.zh.md index b7399a31b0..d1c609c6ce 100644 --- a/docs/rfc/implemented/testing/2026-07-18-tui-terminal-state-snapshots.zh.md +++ b/docs/rfc/implemented/testing/2026-07-18-tui-terminal-state-snapshots.zh.md @@ -8,50 +8,63 @@ Status: implemented TUI 是有状态的渲染器。用户最终看到的结果取决于 ANSI 解析、差分帧、换行、回滚缓冲、视口位置、终端宽度、焦点、光标状态,以及各工具的呈现意图。收集 `Terminal.write()` 片段的单元测试可以验证事件处理,却无法验证终端最终显示的画面。同一画面也可能由不同的写入片段产生,因此固定这些片段会制造误报。 -组件行快照止于 ANSI 进入终端之前,无法覆盖光标移动、清屏、样式、浮层组合和重排。栅格截图会带入与 TUI 契约无关的字体和平台渲染噪声。因此,TUI 既需要一种确定、便于评审的终端状态表示,也需要一项范围更小、覆盖真实进程与 PTY 边界的测试。 +组件行快照止于 ANSI 进入终端之前,无法覆盖光标移动、清屏、样式、浮层组合和重排。栅格截图会带入与 TUI 契约无关的字体和平台渲染噪声。直接追加看似合理的会话事件来构造完整流程还存在另一处盲区:这种测试只能证明渲染器接受这些数据形态,无法证明生产环境的 agent loop(智能体循环)和工具实现会生成这些事件。 + +因此,TUI 既需要确定、便于评审的终端状态表示,也需要通过已录制模型流程执行真实下游组件,并保留一项范围更小、覆盖真实进程与 PTY 边界的测试。 ## 决策 -TUI 覆盖分为三个互补层次: +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 场景。 +1. `packages/ui/tui/tests/tui.spec.ts` 直接测试事件映射、输入路由、资源释放和错误行为。 +2. `packages/ui/tui/tests/tui.snapshot.ts` 将生产 TUI 挂载到无界面终端模拟器,覆盖完整会话日志无法保留的瞬态:进行中的流式输出、待完成工具调用、浮层、展开状态、压缩重排、错误和关闭过程。 +3. `examples/tui-agent/tests/tui.snapshot.ts` 通过生产 agent loop 和真实工具回放已提交的 JSONL 会话日志,再比较生成的语义终端状态。 +4. `examples/tui-agent/tests/tui-keyless-smoke.e2e.ts` 在 PTY 中启动真实 Loader 组合,驱动一段经过流式输出和 `ask_user_question` 的脚本化会话,并验证启动、输入、退出、失败报告和终端恢复。 -包内的 `HeadlessTerminal` 实现与进程终端相同的 pi-tui `Terminal` 接口,并把每次 ANSI 写入交给固定版本的 `@xterm/headless` 解析器。快照会等待 pi-tui 的同步输出结束标记,再读取状态。因此,每个检查点表示已经完成的帧,而不是依赖计时的写入前缀。 +可运行 TUI 在 `examples/tui-agent` 中拥有独立叶节点,与 readline `coding-agent` 和 `acp-agent` 叶节点并列。它通过带断言的 include patch 复用 coding agent 的后端与工具组合,只把共享终端应用固定为 `ui.mode: tui`;TUI 快照和 PTY 测试也归属这个叶节点。 -每份金标把终端状态投影为文本:尺寸、活动缓冲区和视口坐标、生命周期与光标状态、各行、换行标记,以及非默认样式区间。滚动内容较多的卡片捕获已使用缓冲区;浮层捕获可见视口。文本和样式相互分离,评审人无需解码 ANSI 字节即可区分内容变化与呈现变化。 +### 已录制会话回放 -每个检查点还会对完整终端状态强制执行主题无关性:禁止 RGB 颜色、禁止 ANSI 0–15 以外的调色板项,也禁止显式背景色。选择行使用终端默认色进行反显,因此仍然有效。测试套件拥有封闭的检查点清单:类型会拒绝未声明的名称,清单检查会拒绝缺失的检查点和遗留的 `.golden.txt` 文件。 +每个示例级场景目录都包含 `session.jsonl`、可选的子会话日志 `session..jsonl`,以及 `terminal.golden.txt`。主日志提供用户来源的 `user/message` 提示词和已录制的 `assistant/chunk` 序列。`dsh-llm-replay` 为每个会话派生一份模型调用脚本,并将子日志绑定到新建的子会话;这是测试中唯一的 mock 边界。agent loop、bash 与文件系统实现、Code Mode worker、subagent 提供方、工作流 worker、Cordis 工具、呈现器和 TUI 都使用生产实现。 + +如果工具调用顺序不符、预期事件数量不足、工具结果报错、轮次以错误结束、工作流生命周期不完整,或者实时子会话数量与 fixture(测试前置数据)集合不一致,测试都会失败。即使终端金标表面正确,这些断言也能阻止失败或被绕过的生产路径混入结果。 + +真实模型 fixture 通过 `DSH_SNAPSHOT=record` 更新;录制模式会重写其主会话与子会话 JSONL 日志以及终端金标。确定性的 Cordis 工具链保留一份人工编写的完整 JSONL 脚本,因为要求真实模型稳定经过五个指定工具边界和两个子会话并不是可靠的录制契约。`DSH_SNAPSHOT=refresh` 会无密钥回放所有已提交脚本,并且只重写派生的终端金标。普通回放只比较而不写入,未知模式值会快速失败。 + +### 语义终端投影 + +包内的 `HeadlessTerminal` 实现与进程终端相同的 pi-tui `Terminal` 接口,并把每次 ANSI 写入交给固定版本的 `@xterm/headless` 解析器。读取状态前,快照代码会等待同步帧稳定,因此每个检查点表示已经完成的画面,而不是依赖计时的写入前缀。 + +每份金标把终端尺寸、活动缓冲区和视口坐标、生命周期与光标状态、各行、换行标记以及非默认样式区间投影为文本。滚动内容较多的卡片捕获已使用缓冲区;浮层捕获可见视口。文本和样式相互分离,评审人无需解码 ANSI 字节即可区分内容变化与呈现变化。 + +每个检查点还会对完整终端状态强制执行主题无关性:禁止 RGB 颜色、禁止 ANSI 0–15 以外的调色板项,也禁止显式背景色。选择行使用终端默认色进行反显,因此仍然有效。两套测试都拥有封闭清单,会拒绝缺失的场景、缺失的检查点和遗留金标文件。 ### 必需场景矩阵 -| 范围 | 代表性检查点 | 固定的契约 | +| 层次 | 场景 | 固定的契约 | |---|---|---| -| 会话 | 回放、流式输出、完成 | 恢复后的 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` 仍有其原有含义。两条刷新路径都会在同一次运行中继续比较生成后的文件。 +| 已录制流程 | 多轮会话 | 已录制的推理与文本分片、两轮输入、保留历史、token 总量和空闲编辑器状态 | +| 已录制流程 | Todo 计划 | 真实 `todo_write` 执行、结果卡片和持久计划渲染 | +| 已录制流程 | Bash 终端卡片 | 真实本地执行器输出、说明、退出状态和已完成终端卡片 | +| 已录制流程 | 并行文件读取 | 同一条 assistant 消息中的两次调用、真实文件内容、顺序和两个独立完成卡片 | +| 已录制流程 | Code Mode | 真实 `run_code` worker 执行、两条 `tool/code-dispatch` 事件、捕获的程序输出和已完成卡片 | +| 已录制流程 | 动态工作流 | 真实工作流 worker、阶段生命周期、回放的子会话、结构化返回值和已完成卡片 | +| 已录制流程 | Cordis 动态工具链 | 真实挂载、Code Mode 检查、直接 subagent、工作流子会话、卸载和全部生产呈现器 | +| 瞬态 | 流式输出与待完成高级调用 | 进行中的推理和文本,以及完整日志中不会保留的待完成 Code Mode、工作流和 Cordis 卡片 | +| 瞬态 | 卡片、交互、布局、失败和关闭 | 折叠与展开的卡片族、问题校验、压缩替换、尺寸重排、帮助与错误、光标恢复和终端停止 | ## 曾考虑的替代方案 - **快照原始终端写入**:不予采纳,因为差分渲染可能在画面不变时改变写入边界,而且光标与清屏序列难以评审。 - **快照进入终端输出之前的组件渲染行**:不予采纳,因为它无法测试 ANSI 解析、光标移动、浮层、视口行为,也无法测试独立组件在同一帧中的相互作用。 +- **通过追加会话事件构造所有完整流程**:不予采纳,因为人工编写的事件序列可能与 agent loop、工具执行、子会话绑定或 worker 行为发生偏差,但呈现测试仍然保持绿色。直接构造事件只用于渲染器瞬态。 +- **复用 ACP stdout 金标作为 TUI 判定依据**:不予采纳,因为已录制模型流程与传输方式无关,其呈现方式却并非如此。TUI 场景使用同一套 JSONL 回放词汇,但拥有独立的终端金标。 - **提交栅格截图**:不予采纳,因为字体、字形度量、抗锯齿和宿主终端主题会使结果依赖平台,也会增加语义样式变更的评审难度。 - **只使用 PTY 端到端测试**:不予采纳,因为原始 PTY 输出是一系列历史绘制操作,而不是可查询的最终状态。PTY 测试保留真实 Loader、输入与清理边界,模拟器负责广泛的状态覆盖。 -- **复制 pi-tui 未发布的虚拟终端测试 helper**:不予采纳,因为已安装的包并未导出该 helper。围绕公开 `@xterm/headless` API 编写小型适配器,可以显式声明依赖,并让本包拥有状态投影。 ## 后果 -- TUI 视觉回归会产生便于阅读的单元格和样式 diff;必需场景矩阵也让高级功能成为一等测试对象,而不是偶然覆盖。 -- 测试依赖固定到 pi-tui 使用的 xterm 版本。适配器使用 xterm 的拟议缓冲区 API,因此升级 xterm 时必须重新运行并评审语义投影。 -- 模拟器可以建模 ANSI 终端状态,但无法证明每种终端实现独有的行为。真实 PTY 会话覆盖进程选择、键盘输入、用户交互和清理,无需复制完整矩阵。 -- 金标有意固定指定尺寸下的换行与视口行为。布局的预期变更通过无密钥刷新命令更新,并接受常规快照评审。 +- 当真实 Code Mode、工作流、subagent、文件系统、bash 或 Cordis 路径损坏时,已完成高级快照会失败,不会继续接受伪造的结果事件。 +- TUI 视觉回归会产生便于阅读的单元格和样式 diff,而 JSONL fixture 会保留触发生产路径的确切模型分片。 +- 模拟器使用 xterm 的拟议缓冲区 API。升级 xterm 时必须重新运行并评审语义投影;终端特有行为仍需由 PTY 冒烟测试覆盖。 +- 金标有意固定指定尺寸下的换行与视口行为。预期布局变更使用无密钥刷新;模型流程变更使用录制模式,并同时评审 JSONL 与终端 diff。 diff --git a/docs/testing.md b/docs/testing.md index 5f10e1741c..e8b32fac80 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -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`): 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)). +- **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)). TUI completed journeys replay recorded primary/child JSONL through the real agent loop and tools before projecting ANSI into semantic terminal-state goldens; package-local snapshots retain transient renderer states, and a real PTY conversation covers 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 committed replay input remains correct; review every JSONL and 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 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//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. +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//tests/snapshots/`, a scenario table over the [`dsh-acp-snapshot`](../packages/support/acp-snapshot/README.md) suite factory (`examples/acp-agent` is primary). Completed interactive-terminal journeys use JSONL-driven scenarios under `examples/tui-agent/tests/snapshots/`; transient presentation uses the package-local semantic 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. diff --git a/examples/tui-agent/README.md b/examples/tui-agent/README.md index 909ccf700d..ee5b610d53 100644 --- a/examples/tui-agent/README.md +++ b/examples/tui-agent/README.md @@ -17,3 +17,7 @@ Run `pnpm run demo:code-mode tui` for the sibling Code Mode overlay. ## Composition [`cordis.yml`](cordis.yml) includes the readline coding-agent leaf so the LLM, bash, filesystem, compaction, subagent, workflow, todo, timeout, and spill choices have one owner. Its asserted patch replaces only the terminal app config and forces `ui.mode: tui`; [`code-mode.cordis.yml`](code-mode.cordis.yml) applies the same front-door patch to the coding agent's Code Mode overlay. + +## Snapshot tests + +`tests/snapshots//session.jsonl` supplies recorded user prompts and model chunks; sibling child logs drive subagents and workflows. The keyless suite executes those scripts through the real loop and tool implementations, then compares readable terminal cell/style goldens. Use `pnpm run test:snapshot:refresh` for presentation-only changes and `pnpm run test:snapshot:record` with a DeepSeek key when a recorded model journey changes. The implemented [TUI snapshot RFC](../../docs/rfc/implemented/testing/2026-07-18-tui-terminal-state-snapshots.md) owns the scenario matrix and the split between recorded journeys, transient package snapshots, and PTY coverage. diff --git a/examples/tui-agent/tests/snapshots/bash-terminal-card/session.jsonl b/examples/tui-agent/tests/snapshots/bash-terminal-card/session.jsonl new file mode 100644 index 0000000000..e53f4b3da3 --- /dev/null +++ b/examples/tui-agent/tests/snapshots/bash-terminal-card/session.jsonl @@ -0,0 +1,98 @@ +{"type":"session","version":0,"id":"e128dda9-ed11-4868-8266-0ef90d03c3d6","createdAt":1783352050748,"cwd":"/tmp/acp-snap-cwd-mrFUuk"} +{"type":"turn/start","seq":0,"time":1783352050753,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}} +{"type":"user/message","seq":1,"time":1783352050753,"data":{"content":[{"type":"text","text":"Use the bash tool to run exactly: echo TERMINAL_OK. Then reply with the single word DONE and stop."}],"source":{"kind":"user"}},"surfaceOp":"append"} +{"type":"step/start","seq":2,"time":1783352050755,"data":{"turn":1,"step":1}} +{"type":"request/header","seq":3,"time":1783352050756,"data":{"header":{"config":{"provider":"deepseek","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} +{"type":"assistant/chunk","seq":4,"time":1783352051421,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}} +{"type":"assistant/chunk","seq":5,"time":1783352051422,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"The"}}} +{"type":"assistant/chunk","seq":6,"time":1783352051590,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" user"}}} +{"type":"assistant/chunk","seq":7,"time":1783352051618,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" wants"}}} +{"type":"assistant/chunk","seq":8,"time":1783352051618,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" me"}}} +{"type":"assistant/chunk","seq":9,"time":1783352051619,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" to"}}} +{"type":"assistant/chunk","seq":10,"time":1783352051619,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" run"}}} +{"type":"assistant/chunk","seq":11,"time":1783352051619,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" a"}}} +{"type":"assistant/chunk","seq":12,"time":1783352051645,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" simple"}}} +{"type":"assistant/chunk","seq":13,"time":1783352051675,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" bash"}}} +{"type":"assistant/chunk","seq":14,"time":1783352051675,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" command"}}} +{"type":"assistant/chunk","seq":15,"time":1783352051675,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" and"}}} +{"type":"assistant/chunk","seq":16,"time":1783352051676,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" then"}}} +{"type":"assistant/chunk","seq":17,"time":1783352051676,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" reply"}}} +{"type":"assistant/chunk","seq":18,"time":1783352051703,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" with"}}} +{"type":"assistant/chunk","seq":19,"time":1783352051704,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" \""}}} +{"type":"assistant/chunk","seq":20,"time":1783352051704,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"D"}}} +{"type":"assistant/chunk","seq":21,"time":1783352051704,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"ONE"}}} +{"type":"assistant/chunk","seq":22,"time":1783352051704,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"\"."}}} +{"type":"assistant/chunk","seq":23,"time":1783352051790,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}}} +{"type":"assistant/chunk","seq":24,"time":1783352051791,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_fkbBRJsUrGKd1pWVc4Gn8233","name":"bash","argumentsDelta":""}}} +{"type":"assistant/chunk","seq":25,"time":1783352051820,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_fkbBRJsUrGKd1pWVc4Gn8233","name":"bash","argumentsDelta":"{"}}} +{"type":"assistant/chunk","seq":26,"time":1783352051820,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_fkbBRJsUrGKd1pWVc4Gn8233","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":27,"time":1783352051820,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_fkbBRJsUrGKd1pWVc4Gn8233","name":"bash","argumentsDelta":"command"}}} +{"type":"assistant/chunk","seq":28,"time":1783352051820,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_fkbBRJsUrGKd1pWVc4Gn8233","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":29,"time":1783352051820,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_fkbBRJsUrGKd1pWVc4Gn8233","name":"bash","argumentsDelta":": "}}} +{"type":"assistant/chunk","seq":30,"time":1783352051848,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_fkbBRJsUrGKd1pWVc4Gn8233","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":31,"time":1783352051848,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_fkbBRJsUrGKd1pWVc4Gn8233","name":"bash","argumentsDelta":"echo"}}} +{"type":"assistant/chunk","seq":32,"time":1783352051848,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_fkbBRJsUrGKd1pWVc4Gn8233","name":"bash","argumentsDelta":" TER"}}} +{"type":"assistant/chunk","seq":33,"time":1783352051848,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_fkbBRJsUrGKd1pWVc4Gn8233","name":"bash","argumentsDelta":"MIN"}}} +{"type":"assistant/chunk","seq":34,"time":1783352051877,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_fkbBRJsUrGKd1pWVc4Gn8233","name":"bash","argumentsDelta":"AL"}}} +{"type":"assistant/chunk","seq":35,"time":1783352051877,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_fkbBRJsUrGKd1pWVc4Gn8233","name":"bash","argumentsDelta":"_OK"}}} +{"type":"assistant/chunk","seq":36,"time":1783352051877,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_fkbBRJsUrGKd1pWVc4Gn8233","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":37,"time":1783352051905,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_fkbBRJsUrGKd1pWVc4Gn8233","name":"bash","argumentsDelta":", "}}} +{"type":"assistant/chunk","seq":38,"time":1783352051906,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_fkbBRJsUrGKd1pWVc4Gn8233","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":39,"time":1783352051906,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_fkbBRJsUrGKd1pWVc4Gn8233","name":"bash","argumentsDelta":"description"}}} +{"type":"assistant/chunk","seq":40,"time":1783352051935,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_fkbBRJsUrGKd1pWVc4Gn8233","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":41,"time":1783352051935,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_fkbBRJsUrGKd1pWVc4Gn8233","name":"bash","argumentsDelta":": "}}} +{"type":"assistant/chunk","seq":42,"time":1783352051935,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_fkbBRJsUrGKd1pWVc4Gn8233","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":43,"time":1783352051935,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_fkbBRJsUrGKd1pWVc4Gn8233","name":"bash","argumentsDelta":"E"}}} +{"type":"assistant/chunk","seq":44,"time":1783352051967,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_fkbBRJsUrGKd1pWVc4Gn8233","name":"bash","argumentsDelta":"cho"}}} +{"type":"assistant/chunk","seq":45,"time":1783352051967,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_fkbBRJsUrGKd1pWVc4Gn8233","name":"bash","argumentsDelta":" TER"}}} +{"type":"assistant/chunk","seq":46,"time":1783352051967,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_fkbBRJsUrGKd1pWVc4Gn8233","name":"bash","argumentsDelta":"MIN"}}} +{"type":"assistant/chunk","seq":47,"time":1783352051967,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_fkbBRJsUrGKd1pWVc4Gn8233","name":"bash","argumentsDelta":"AL"}}} +{"type":"assistant/chunk","seq":48,"time":1783352051967,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_fkbBRJsUrGKd1pWVc4Gn8233","name":"bash","argumentsDelta":"_OK"}}} +{"type":"assistant/chunk","seq":49,"time":1783352051967,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_fkbBRJsUrGKd1pWVc4Gn8233","name":"bash","argumentsDelta":" to"}}} +{"type":"assistant/chunk","seq":50,"time":1783352052041,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_fkbBRJsUrGKd1pWVc4Gn8233","name":"bash","argumentsDelta":" verify"}}} +{"type":"assistant/chunk","seq":51,"time":1783352052041,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_fkbBRJsUrGKd1pWVc4Gn8233","name":"bash","argumentsDelta":" terminal"}}} +{"type":"assistant/chunk","seq":52,"time":1783352052041,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_fkbBRJsUrGKd1pWVc4Gn8233","name":"bash","argumentsDelta":" access"}}} +{"type":"assistant/chunk","seq":53,"time":1783352052054,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_fkbBRJsUrGKd1pWVc4Gn8233","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":54,"time":1783352052054,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_fkbBRJsUrGKd1pWVc4Gn8233","name":"bash","argumentsDelta":"}"}}} +{"type":"assistant/chunk","seq":55,"time":1783352052117,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to run a simple bash command and then reply with \"DONE\"."}}}} +{"type":"assistant/chunk","seq":56,"time":1783352052118,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_00_fkbBRJsUrGKd1pWVc4Gn8233","name":"bash","arguments":"{\"command\": \"echo TERMINAL_OK\", \"description\": \"Echo TERMINAL_OK to verify terminal access\"}"}}}} +{"type":"assistant/chunk","seq":57,"time":1783352052118,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":2877,"outputTokens":90,"cacheReadTokens":0,"reasoningTokens":18}}}} +{"type":"assistant/chunk","seq":58,"time":1783352052118,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} +{"type":"assistant/message","seq":59,"time":1783352052121,"data":{"turn":1,"step":1,"content":[{"type":"reasoning","text":"The user wants me to run a simple bash command and then reply with \"DONE\"."},{"type":"tool-call","id":"call_00_fkbBRJsUrGKd1pWVc4Gn8233","name":"bash","arguments":"{\"command\": \"echo TERMINAL_OK\", \"description\": \"Echo TERMINAL_OK to verify terminal access\"}"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":2877,"outputTokens":90,"cacheReadTokens":0,"reasoningTokens":18}},"sourceEventSeqs":[4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58],"surfaceOp":"append"} +{"type":"tool/call","seq":60,"time":1783352052121,"data":{"turn":1,"step":1,"callId":"call_00_fkbBRJsUrGKd1pWVc4Gn8233","name":"bash","arguments":"{\"command\": \"echo TERMINAL_OK\", \"description\": \"Echo TERMINAL_OK to verify terminal access\"}"}} +{"type":"tool/result","seq":61,"time":1783352052136,"data":{"turn":1,"step":1,"callId":"call_00_fkbBRJsUrGKd1pWVc4Gn8233","content":[{"type":"text","text":"TERMINAL_OK\n"}],"isError":false},"sourceEventSeqs":[60],"surfaceOp":"append"} +{"type":"step/end","seq":62,"time":1783352052137,"data":{"turn":1,"step":1}} +{"type":"step/start","seq":63,"time":1783352052137,"data":{"turn":1,"step":2}} +{"type":"assistant/chunk","seq":64,"time":1783352052701,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}} +{"type":"assistant/chunk","seq":65,"time":1783352052702,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"The"}}} +{"type":"assistant/chunk","seq":66,"time":1783352052780,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" command"}}} +{"type":"assistant/chunk","seq":67,"time":1783352052809,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" ran"}}} +{"type":"assistant/chunk","seq":68,"time":1783352052838,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" successfully"}}} +{"type":"assistant/chunk","seq":69,"time":1783352052838,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" and"}}} +{"type":"assistant/chunk","seq":70,"time":1783352052838,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" output"}}} +{"type":"assistant/chunk","seq":71,"time":1783352052867,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" \""}}} +{"type":"assistant/chunk","seq":72,"time":1783352052867,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"TER"}}} +{"type":"assistant/chunk","seq":73,"time":1783352052867,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"MIN"}}} +{"type":"assistant/chunk","seq":74,"time":1783352052867,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"AL"}}} +{"type":"assistant/chunk","seq":75,"time":1783352052867,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"_OK"}}} +{"type":"assistant/chunk","seq":76,"time":1783352052867,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"\"."}}} +{"type":"assistant/chunk","seq":77,"time":1783352052895,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" I"}}} +{"type":"assistant/chunk","seq":78,"time":1783352052896,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" should"}}} +{"type":"assistant/chunk","seq":79,"time":1783352052924,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" now"}}} +{"type":"assistant/chunk","seq":80,"time":1783352052925,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" reply"}}} +{"type":"assistant/chunk","seq":81,"time":1783352052925,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" with"}}} +{"type":"assistant/chunk","seq":82,"time":1783352052925,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" just"}}} +{"type":"assistant/chunk","seq":83,"time":1783352052957,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" \""}}} +{"type":"assistant/chunk","seq":84,"time":1783352052957,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"D"}}} +{"type":"assistant/chunk","seq":85,"time":1783352052957,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"ONE"}}} +{"type":"assistant/chunk","seq":86,"time":1783352052957,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"\"."}}} +{"type":"assistant/chunk","seq":87,"time":1783352052957,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":1,"blockType":"text"}}} +{"type":"assistant/chunk","seq":88,"time":1783352052957,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":"D"}}} +{"type":"assistant/chunk","seq":89,"time":1783352052986,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":"ONE"}}} +{"type":"assistant/chunk","seq":90,"time":1783352052986,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The command ran successfully and output \"TERMINAL_OK\". I should now reply with just \"DONE\"."}}}} +{"type":"assistant/chunk","seq":91,"time":1783352052986,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"DONE"}}}} +{"type":"assistant/chunk","seq":92,"time":1783352052986,"data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":168,"outputTokens":25,"cacheReadTokens":2816,"reasoningTokens":22}}}} +{"type":"assistant/chunk","seq":93,"time":1783352052986,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"stop"}}}} +{"type":"assistant/message","seq":94,"time":1783352052987,"data":{"turn":1,"step":2,"content":[{"type":"reasoning","text":"The command ran successfully and output \"TERMINAL_OK\". I should now reply with just \"DONE\"."},{"type":"text","text":"DONE"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":168,"outputTokens":25,"cacheReadTokens":2816,"reasoningTokens":22}},"sourceEventSeqs":[64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93],"surfaceOp":"append"} +{"type":"step/end","seq":95,"time":1783352052987,"data":{"turn":1,"step":2}} +{"type":"turn/end","seq":96,"time":1783352052987,"data":{"turn":1,"reason":{"kind":"completed"}}} diff --git a/examples/tui-agent/tests/snapshots/bash-terminal-card/terminal.golden.txt b/examples/tui-agent/tests/snapshots/bash-terminal-card/terminal.golden.txt new file mode 100644 index 0000000000..30770dcccb --- /dev/null +++ b/examples/tui-agent/tests/snapshots/bash-terminal-card/terminal.golden.txt @@ -0,0 +1,72 @@ +terminal 100x36 buffer=normal length=36 base=0 viewport=0 +lifecycle started=1 stopped=0 progress=inactive +title "DSH TUI snapshot" +cursor hidden column=1 viewportRow=27 bufferRow=27 +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| "│ Recorded replay: bash-terminal-card │" + style 0-0 fg=bright-blue + style 2-36 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| +6| "▌ " + style 0-0 fg=bright-blue +7| "▌ You " + style 0-0 fg=bright-blue + style 2-4 fg=bright-blue bold +8| "▌ Use the bash tool to run exactly: echo TERMINAL_OK. Then reply with the single word DONE and stop." + style 0-0 fg=bright-blue +9| "▌ " + style 0-0 fg=bright-blue +10| +11| " Reasoning " + style 1-9 fg=bright-black italic +12| " The user wants me to run a simple bash command and then reply with \"DONE\". " + style 1-74 fg=bright-black italic +13| +14| "▌ " + style 0-0 fg=green +15| "▌ ✓ echo TERMINAL_OK " + style 0-0 fg=green + style 2-2 fg=green bold + style 3-19 bold +16| "▌ Echo TERMINAL_OK to verify terminal access " + style 0-0 fg=green + style 2-43 fg=bright-black +17| "▌ TERMINAL_OK " + style 0-0 fg=green +18| "▌ [exit 0] " + style 0-0 fg=green + style 2-9 dim +19| "▌ " + style 0-0 fg=green +20| +21| " Reasoning " + style 1-9 fg=bright-black italic +22| " The command ran successfully and output \"TERMINAL_OK\". I should now reply with just \"DONE\". " + style 1-91 fg=bright-black italic +23| +24| " Assistant " + style 1-9 fg=bright-magenta bold +25| " DONE " +26| "────────────────────────────────────────────────────────────────────────────────────────────────────" + style 0-99 dim +27| " " + style 1-1 inverse +28| "────────────────────────────────────────────────────────────────────────────────────────────────────" + style 0-99 dim +29| "/workspace/project ↑3.0k ↓" + style 0-99 dim +30-35| diff --git a/examples/tui-agent/tests/snapshots/code-mode/session.jsonl b/examples/tui-agent/tests/snapshots/code-mode/session.jsonl new file mode 100644 index 0000000000..0fe57055e8 --- /dev/null +++ b/examples/tui-agent/tests/snapshots/code-mode/session.jsonl @@ -0,0 +1,150 @@ +{"type":"session","version":0,"id":"94cd1ae4-e1d1-4ec8-9d27-50a1f849b6b3","createdAt":1783611771392,"cwd":"/var/folders/2c/psb0_fmx7hbgz558xjt_f0l00000gn/T/acp-snap-cwd-BteTVR"} +{"type":"turn/start","seq":0,"time":1783611771394,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}} +{"type":"user/message","seq":1,"time":1783611771394,"data":{"content":[{"type":"text","text":"Using ONE run_code program: call the bash tool twice — exactly `echo CODE_ONE` then exactly `echo CODE_TWO` — and return the two outputs joined with a plus sign. Then reply with that joined string only and stop."}],"source":{"kind":"user"}},"surfaceOp":"append"} +{"type":"step/start","seq":2,"time":1783611771396,"data":{"turn":1,"step":1}} +{"type":"request/header","seq":3,"time":1783611771396,"data":{"header":{"config":{"provider":"deepseek","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} +{"type":"assistant/chunk","seq":4,"time":1783611771869,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}} +{"type":"assistant/chunk","seq":5,"time":1783611771869,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"The"}}} +{"type":"assistant/chunk","seq":6,"time":1783611771978,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" user"}}} +{"type":"assistant/chunk","seq":7,"time":1783611772007,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" wants"}}} +{"type":"assistant/chunk","seq":8,"time":1783611772008,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" a"}}} +{"type":"assistant/chunk","seq":9,"time":1783611772036,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" single"}}} +{"type":"assistant/chunk","seq":10,"time":1783611772036,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" run"}}} +{"type":"assistant/chunk","seq":11,"time":1783611772036,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"_code"}}} +{"type":"assistant/chunk","seq":12,"time":1783611772036,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" program"}}} +{"type":"assistant/chunk","seq":13,"time":1783611772036,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" that"}}} +{"type":"assistant/chunk","seq":14,"time":1783611772089,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" calls"}}} +{"type":"assistant/chunk","seq":15,"time":1783611772089,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" bash"}}} +{"type":"assistant/chunk","seq":16,"time":1783611772089,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" twice"}}} +{"type":"assistant/chunk","seq":17,"time":1783611772096,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":","}}} +{"type":"assistant/chunk","seq":18,"time":1783611772124,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" then"}}} +{"type":"assistant/chunk","seq":19,"time":1783611772153,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" returns"}}} +{"type":"assistant/chunk","seq":20,"time":1783611772183,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" the"}}} +{"type":"assistant/chunk","seq":21,"time":1783611772183,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" two"}}} +{"type":"assistant/chunk","seq":22,"time":1783611772211,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" outputs"}}} +{"type":"assistant/chunk","seq":23,"time":1783611772212,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" joined"}}} +{"type":"assistant/chunk","seq":24,"time":1783611772212,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" with"}}} +{"type":"assistant/chunk","seq":25,"time":1783611772212,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" a"}}} +{"type":"assistant/chunk","seq":26,"time":1783611772212,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" plus"}}} +{"type":"assistant/chunk","seq":27,"time":1783611772212,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" sign"}}} +{"type":"assistant/chunk","seq":28,"time":1783611772241,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"."}}} +{"type":"assistant/chunk","seq":29,"time":1783611772241,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" Let"}}} +{"type":"assistant/chunk","seq":30,"time":1783611772241,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" me"}}} +{"type":"assistant/chunk","seq":31,"time":1783611772241,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" write"}}} +{"type":"assistant/chunk","seq":32,"time":1783611772270,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" this"}}} +{"type":"assistant/chunk","seq":33,"time":1783611772270,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"."}}} +{"type":"assistant/chunk","seq":34,"time":1783611772361,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}}} +{"type":"assistant/chunk","seq":35,"time":1783611772361,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_DRxnM6R1TThfDwcudW0f2050","name":"run_code","argumentsDelta":""}}} +{"type":"assistant/chunk","seq":36,"time":1783611772361,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_DRxnM6R1TThfDwcudW0f2050","name":"run_code","argumentsDelta":"{"}}} +{"type":"assistant/chunk","seq":37,"time":1783611772362,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_DRxnM6R1TThfDwcudW0f2050","name":"run_code","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":38,"time":1783611772390,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_DRxnM6R1TThfDwcudW0f2050","name":"run_code","argumentsDelta":"code"}}} +{"type":"assistant/chunk","seq":39,"time":1783611772390,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_DRxnM6R1TThfDwcudW0f2050","name":"run_code","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":40,"time":1783611772390,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_DRxnM6R1TThfDwcudW0f2050","name":"run_code","argumentsDelta":": "}}} +{"type":"assistant/chunk","seq":41,"time":1783611772390,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_DRxnM6R1TThfDwcudW0f2050","name":"run_code","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":42,"time":1783611772420,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_DRxnM6R1TThfDwcudW0f2050","name":"run_code","argumentsDelta":"const"}}} +{"type":"assistant/chunk","seq":43,"time":1783611772420,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_DRxnM6R1TThfDwcudW0f2050","name":"run_code","argumentsDelta":" out"}}} +{"type":"assistant/chunk","seq":44,"time":1783611772420,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_DRxnM6R1TThfDwcudW0f2050","name":"run_code","argumentsDelta":"1"}}} +{"type":"assistant/chunk","seq":45,"time":1783611772420,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_DRxnM6R1TThfDwcudW0f2050","name":"run_code","argumentsDelta":" ="}}} +{"type":"assistant/chunk","seq":46,"time":1783611772421,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_DRxnM6R1TThfDwcudW0f2050","name":"run_code","argumentsDelta":" await"}}} +{"type":"assistant/chunk","seq":47,"time":1783611772421,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_DRxnM6R1TThfDwcudW0f2050","name":"run_code","argumentsDelta":" tools"}}} +{"type":"assistant/chunk","seq":48,"time":1783611772449,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_DRxnM6R1TThfDwcudW0f2050","name":"run_code","argumentsDelta":".b"}}} +{"type":"assistant/chunk","seq":49,"time":1783611772449,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_DRxnM6R1TThfDwcudW0f2050","name":"run_code","argumentsDelta":"ash"}}} +{"type":"assistant/chunk","seq":50,"time":1783611772449,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_DRxnM6R1TThfDwcudW0f2050","name":"run_code","argumentsDelta":"({"}}} +{"type":"assistant/chunk","seq":51,"time":1783611772449,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_DRxnM6R1TThfDwcudW0f2050","name":"run_code","argumentsDelta":" command"}}} +{"type":"assistant/chunk","seq":52,"time":1783611772478,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_DRxnM6R1TThfDwcudW0f2050","name":"run_code","argumentsDelta":":"}}} +{"type":"assistant/chunk","seq":53,"time":1783611772479,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_DRxnM6R1TThfDwcudW0f2050","name":"run_code","argumentsDelta":" \\\""}}} +{"type":"assistant/chunk","seq":54,"time":1783611772479,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_DRxnM6R1TThfDwcudW0f2050","name":"run_code","argumentsDelta":"echo"}}} +{"type":"assistant/chunk","seq":55,"time":1783611772479,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_DRxnM6R1TThfDwcudW0f2050","name":"run_code","argumentsDelta":" CODE"}}} +{"type":"assistant/chunk","seq":56,"time":1783611772479,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_DRxnM6R1TThfDwcudW0f2050","name":"run_code","argumentsDelta":"_"}}} +{"type":"assistant/chunk","seq":57,"time":1783611772508,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_DRxnM6R1TThfDwcudW0f2050","name":"run_code","argumentsDelta":"ONE"}}} +{"type":"assistant/chunk","seq":58,"time":1783611772510,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_DRxnM6R1TThfDwcudW0f2050","name":"run_code","argumentsDelta":"\\\","}}} +{"type":"assistant/chunk","seq":59,"time":1783611772510,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_DRxnM6R1TThfDwcudW0f2050","name":"run_code","argumentsDelta":" description"}}} +{"type":"assistant/chunk","seq":60,"time":1783611772510,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_DRxnM6R1TThfDwcudW0f2050","name":"run_code","argumentsDelta":":"}}} +{"type":"assistant/chunk","seq":61,"time":1783611772510,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_DRxnM6R1TThfDwcudW0f2050","name":"run_code","argumentsDelta":" \\\""}}} +{"type":"assistant/chunk","seq":62,"time":1783611772510,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_DRxnM6R1TThfDwcudW0f2050","name":"run_code","argumentsDelta":"First"}}} +{"type":"assistant/chunk","seq":63,"time":1783611772538,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_DRxnM6R1TThfDwcudW0f2050","name":"run_code","argumentsDelta":" echo"}}} +{"type":"assistant/chunk","seq":64,"time":1783611772538,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_DRxnM6R1TThfDwcudW0f2050","name":"run_code","argumentsDelta":"\\\""}}} +{"type":"assistant/chunk","seq":65,"time":1783611772566,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_DRxnM6R1TThfDwcudW0f2050","name":"run_code","argumentsDelta":" });\\n"}}} +{"type":"assistant/chunk","seq":66,"time":1783611772567,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_DRxnM6R1TThfDwcudW0f2050","name":"run_code","argumentsDelta":"const"}}} +{"type":"assistant/chunk","seq":67,"time":1783611772567,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_DRxnM6R1TThfDwcudW0f2050","name":"run_code","argumentsDelta":" out"}}} +{"type":"assistant/chunk","seq":68,"time":1783611772567,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_DRxnM6R1TThfDwcudW0f2050","name":"run_code","argumentsDelta":"2"}}} +{"type":"assistant/chunk","seq":69,"time":1783611772567,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_DRxnM6R1TThfDwcudW0f2050","name":"run_code","argumentsDelta":" ="}}} +{"type":"assistant/chunk","seq":70,"time":1783611772567,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_DRxnM6R1TThfDwcudW0f2050","name":"run_code","argumentsDelta":" await"}}} +{"type":"assistant/chunk","seq":71,"time":1783611772597,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_DRxnM6R1TThfDwcudW0f2050","name":"run_code","argumentsDelta":" tools"}}} +{"type":"assistant/chunk","seq":72,"time":1783611772597,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_DRxnM6R1TThfDwcudW0f2050","name":"run_code","argumentsDelta":".b"}}} +{"type":"assistant/chunk","seq":73,"time":1783611772597,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_DRxnM6R1TThfDwcudW0f2050","name":"run_code","argumentsDelta":"ash"}}} +{"type":"assistant/chunk","seq":74,"time":1783611772598,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_DRxnM6R1TThfDwcudW0f2050","name":"run_code","argumentsDelta":"({"}}} +{"type":"assistant/chunk","seq":75,"time":1783611772598,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_DRxnM6R1TThfDwcudW0f2050","name":"run_code","argumentsDelta":" command"}}} +{"type":"assistant/chunk","seq":76,"time":1783611772598,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_DRxnM6R1TThfDwcudW0f2050","name":"run_code","argumentsDelta":":"}}} +{"type":"assistant/chunk","seq":77,"time":1783611772625,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_DRxnM6R1TThfDwcudW0f2050","name":"run_code","argumentsDelta":" \\\""}}} +{"type":"assistant/chunk","seq":78,"time":1783611772625,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_DRxnM6R1TThfDwcudW0f2050","name":"run_code","argumentsDelta":"echo"}}} +{"type":"assistant/chunk","seq":79,"time":1783611772625,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_DRxnM6R1TThfDwcudW0f2050","name":"run_code","argumentsDelta":" CODE"}}} +{"type":"assistant/chunk","seq":80,"time":1783611772626,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_DRxnM6R1TThfDwcudW0f2050","name":"run_code","argumentsDelta":"_T"}}} +{"type":"assistant/chunk","seq":81,"time":1783611772626,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_DRxnM6R1TThfDwcudW0f2050","name":"run_code","argumentsDelta":"WO"}}} +{"type":"assistant/chunk","seq":82,"time":1783611772626,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_DRxnM6R1TThfDwcudW0f2050","name":"run_code","argumentsDelta":"\\\","}}} +{"type":"assistant/chunk","seq":83,"time":1783611772654,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_DRxnM6R1TThfDwcudW0f2050","name":"run_code","argumentsDelta":" description"}}} +{"type":"assistant/chunk","seq":84,"time":1783611772655,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_DRxnM6R1TThfDwcudW0f2050","name":"run_code","argumentsDelta":":"}}} +{"type":"assistant/chunk","seq":85,"time":1783611772655,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_DRxnM6R1TThfDwcudW0f2050","name":"run_code","argumentsDelta":" \\\""}}} +{"type":"assistant/chunk","seq":86,"time":1783611772655,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_DRxnM6R1TThfDwcudW0f2050","name":"run_code","argumentsDelta":"Second"}}} +{"type":"assistant/chunk","seq":87,"time":1783611772655,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_DRxnM6R1TThfDwcudW0f2050","name":"run_code","argumentsDelta":" echo"}}} +{"type":"assistant/chunk","seq":88,"time":1783611772655,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_DRxnM6R1TThfDwcudW0f2050","name":"run_code","argumentsDelta":"\\\""}}} +{"type":"assistant/chunk","seq":89,"time":1783611772684,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_DRxnM6R1TThfDwcudW0f2050","name":"run_code","argumentsDelta":" });\\n"}}} +{"type":"assistant/chunk","seq":90,"time":1783611772685,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_DRxnM6R1TThfDwcudW0f2050","name":"run_code","argumentsDelta":"return"}}} +{"type":"assistant/chunk","seq":91,"time":1783611772685,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_DRxnM6R1TThfDwcudW0f2050","name":"run_code","argumentsDelta":" out"}}} +{"type":"assistant/chunk","seq":92,"time":1783611772713,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_DRxnM6R1TThfDwcudW0f2050","name":"run_code","argumentsDelta":"1"}}} +{"type":"assistant/chunk","seq":93,"time":1783611772714,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_DRxnM6R1TThfDwcudW0f2050","name":"run_code","argumentsDelta":".trim"}}} +{"type":"assistant/chunk","seq":94,"time":1783611772714,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_DRxnM6R1TThfDwcudW0f2050","name":"run_code","argumentsDelta":"()"}}} +{"type":"assistant/chunk","seq":95,"time":1783611772714,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_DRxnM6R1TThfDwcudW0f2050","name":"run_code","argumentsDelta":" +"}}} +{"type":"assistant/chunk","seq":96,"time":1783611772714,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_DRxnM6R1TThfDwcudW0f2050","name":"run_code","argumentsDelta":" \\\"+"}}} +{"type":"assistant/chunk","seq":97,"time":1783611772744,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_DRxnM6R1TThfDwcudW0f2050","name":"run_code","argumentsDelta":"\\\""}}} +{"type":"assistant/chunk","seq":98,"time":1783611772744,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_DRxnM6R1TThfDwcudW0f2050","name":"run_code","argumentsDelta":" +"}}} +{"type":"assistant/chunk","seq":99,"time":1783611772745,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_DRxnM6R1TThfDwcudW0f2050","name":"run_code","argumentsDelta":" out"}}} +{"type":"assistant/chunk","seq":100,"time":1783611772745,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_DRxnM6R1TThfDwcudW0f2050","name":"run_code","argumentsDelta":"2"}}} +{"type":"assistant/chunk","seq":101,"time":1783611772745,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_DRxnM6R1TThfDwcudW0f2050","name":"run_code","argumentsDelta":".trim"}}} +{"type":"assistant/chunk","seq":102,"time":1783611772745,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_DRxnM6R1TThfDwcudW0f2050","name":"run_code","argumentsDelta":"();"}}} +{"type":"assistant/chunk","seq":103,"time":1783611772772,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_DRxnM6R1TThfDwcudW0f2050","name":"run_code","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":104,"time":1783611772773,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_DRxnM6R1TThfDwcudW0f2050","name":"run_code","argumentsDelta":"}"}}} +{"type":"assistant/chunk","seq":105,"time":1783611772836,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants a single run_code program that calls bash twice, then returns the two outputs joined with a plus sign. Let me write this."}}}} +{"type":"assistant/chunk","seq":106,"time":1783611772836,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_00_DRxnM6R1TThfDwcudW0f2050","name":"run_code","arguments":"{\"code\": \"const out1 = await tools.bash({ command: \\\"echo CODE_ONE\\\", description: \\\"First echo\\\" });\\nconst out2 = await tools.bash({ command: \\\"echo CODE_TWO\\\", description: \\\"Second echo\\\" });\\nreturn out1.trim() + \\\"+\\\" + out2.trim();\"}"}}}} +{"type":"assistant/chunk","seq":107,"time":1783611772836,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":3009,"outputTokens":133,"cacheReadTokens":0,"reasoningTokens":29}}}} +{"type":"assistant/chunk","seq":108,"time":1783611772836,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} +{"type":"assistant/message","seq":109,"time":1783611772840,"data":{"turn":1,"step":1,"content":[{"type":"reasoning","text":"The user wants a single run_code program that calls bash twice, then returns the two outputs joined with a plus sign. Let me write this."},{"type":"tool-call","id":"call_00_DRxnM6R1TThfDwcudW0f2050","name":"run_code","arguments":"{\"code\": \"const out1 = await tools.bash({ command: \\\"echo CODE_ONE\\\", description: \\\"First echo\\\" });\\nconst out2 = await tools.bash({ command: \\\"echo CODE_TWO\\\", description: \\\"Second echo\\\" });\\nreturn out1.trim() + \\\"+\\\" + out2.trim();\"}"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":3009,"outputTokens":133,"cacheReadTokens":0,"reasoningTokens":29}},"sourceEventSeqs":[4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108],"surfaceOp":"append"} +{"type":"tool/call","seq":110,"time":1783611772840,"data":{"turn":1,"step":1,"callId":"call_00_DRxnM6R1TThfDwcudW0f2050","name":"run_code","arguments":"{\"code\": \"const out1 = await tools.bash({ command: \\\"echo CODE_ONE\\\", description: \\\"First echo\\\" });\\nconst out2 = await tools.bash({ command: \\\"echo CODE_TWO\\\", description: \\\"Second echo\\\" });\\nreturn out1.trim() + \\\"+\\\" + out2.trim();\"}"}} +{"type":"tool/code-dispatch","seq":111,"time":1783611772933,"data":{"parentCallId":"call_00_DRxnM6R1TThfDwcudW0f2050","subCallId":"call_00_DRxnM6R1TThfDwcudW0f2050:code:1","name":"bash","arguments":{"command":"echo CODE_ONE","description":"First echo"},"isError":false,"resultSummary":"CODE_ONE\n"}} +{"type":"tool/code-dispatch","seq":112,"time":1783611772936,"data":{"parentCallId":"call_00_DRxnM6R1TThfDwcudW0f2050","subCallId":"call_00_DRxnM6R1TThfDwcudW0f2050:code:2","name":"bash","arguments":{"command":"echo CODE_TWO","description":"Second echo"},"isError":false,"resultSummary":"CODE_TWO\n"}} +{"type":"tool/result","seq":113,"time":1783611772937,"data":{"turn":1,"step":1,"callId":"call_00_DRxnM6R1TThfDwcudW0f2050","content":[{"type":"text","text":"CODE_ONE+CODE_TWO"}],"isError":false,"meta":{"logs":[]}},"sourceEventSeqs":[110],"surfaceOp":"append"} +{"type":"step/end","seq":114,"time":1783611772938,"data":{"turn":1,"step":1}} +{"type":"step/start","seq":115,"time":1783611772938,"data":{"turn":1,"step":2}} +{"type":"assistant/chunk","seq":116,"time":1783611773376,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}} +{"type":"assistant/chunk","seq":117,"time":1783611773376,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"The"}}} +{"type":"assistant/chunk","seq":118,"time":1783611773480,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" output"}}} +{"type":"assistant/chunk","seq":119,"time":1783611773511,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" is"}}} +{"type":"assistant/chunk","seq":120,"time":1783611773512,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" exactly"}}} +{"type":"assistant/chunk","seq":121,"time":1783611773540,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" what"}}} +{"type":"assistant/chunk","seq":122,"time":1783611773541,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" the"}}} +{"type":"assistant/chunk","seq":123,"time":1783611773541,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" user"}}} +{"type":"assistant/chunk","seq":124,"time":1783611773569,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" asked"}}} +{"type":"assistant/chunk","seq":125,"time":1783611773570,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" for"}}} +{"type":"assistant/chunk","seq":126,"time":1783611773570,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":":"}}} +{"type":"assistant/chunk","seq":127,"time":1783611773597,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" CODE"}}} +{"type":"assistant/chunk","seq":128,"time":1783611773597,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"_"}}} +{"type":"assistant/chunk","seq":129,"time":1783611773597,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"ONE"}}} +{"type":"assistant/chunk","seq":130,"time":1783611773626,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"+"}}} +{"type":"assistant/chunk","seq":131,"time":1783611773627,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"CODE"}}} +{"type":"assistant/chunk","seq":132,"time":1783611773627,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"_T"}}} +{"type":"assistant/chunk","seq":133,"time":1783611773627,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"WO"}}} +{"type":"assistant/chunk","seq":134,"time":1783611773654,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":1,"blockType":"text"}}} +{"type":"assistant/chunk","seq":135,"time":1783611773655,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":"CODE"}}} +{"type":"assistant/chunk","seq":136,"time":1783611773655,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":"_"}}} +{"type":"assistant/chunk","seq":137,"time":1783611773656,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":"ONE"}}} +{"type":"assistant/chunk","seq":138,"time":1783611773656,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":"+"}}} +{"type":"assistant/chunk","seq":139,"time":1783611773656,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":"CODE"}}} +{"type":"assistant/chunk","seq":140,"time":1783611773657,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":"_T"}}} +{"type":"assistant/chunk","seq":141,"time":1783611773685,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":"WO"}}} +{"type":"assistant/chunk","seq":142,"time":1783611773686,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The output is exactly what the user asked for: CODE_ONE+CODE_TWO"}}}} +{"type":"assistant/chunk","seq":143,"time":1783611773686,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"CODE_ONE+CODE_TWO"}}}} +{"type":"assistant/chunk","seq":144,"time":1783611773686,"data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":89,"outputTokens":25,"cacheReadTokens":3072,"reasoningTokens":17}}}} +{"type":"assistant/chunk","seq":145,"time":1783611773686,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"stop"}}}} +{"type":"assistant/message","seq":146,"time":1783611773687,"data":{"turn":1,"step":2,"content":[{"type":"reasoning","text":"The output is exactly what the user asked for: CODE_ONE+CODE_TWO"},{"type":"text","text":"CODE_ONE+CODE_TWO"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":89,"outputTokens":25,"cacheReadTokens":3072,"reasoningTokens":17}},"sourceEventSeqs":[116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145],"surfaceOp":"append"} +{"type":"step/end","seq":147,"time":1783611773687,"data":{"turn":1,"step":2}} +{"type":"turn/end","seq":148,"time":1783611773687,"data":{"turn":1,"reason":{"kind":"completed"}}} diff --git a/examples/tui-agent/tests/snapshots/code-mode/terminal.golden.txt b/examples/tui-agent/tests/snapshots/code-mode/terminal.golden.txt new file mode 100644 index 0000000000..a7f1457d7a --- /dev/null +++ b/examples/tui-agent/tests/snapshots/code-mode/terminal.golden.txt @@ -0,0 +1,79 @@ +terminal 100x36 buffer=normal length=36 base=0 viewport=0 +lifecycle started=1 stopped=0 progress=inactive +title "DSH TUI snapshot" +cursor hidden column=1 viewportRow=29 bufferRow=29 +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| "│ Recorded replay: code-mode │" + style 0-0 fg=bright-blue + style 2-27 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| +6| "▌ " + style 0-0 fg=bright-blue +7| "▌ You " + style 0-0 fg=bright-blue + style 2-4 fg=bright-blue bold +8| "▌ Using ONE run_code program: call the bash tool twice — exactly echo CODE_ONE then exactly echo " + style 0-0 fg=bright-blue + style 65-77 fg=cyan + style 92-99 fg=cyan +9| "▌ CODE_TWO — and return the two outputs joined with a plus sign. Then reply with that joined string " + style 0-0 fg=bright-blue + style 2-9 fg=cyan +10| "▌ only and stop. " + style 0-0 fg=bright-blue +11| "▌ " + style 0-0 fg=bright-blue +12| +13| " Reasoning " + style 1-9 fg=bright-black italic +14| " The user wants a single run_code program that calls bash twice, then returns the two outputs " + style 1-99 fg=bright-black italic +15| " joined with a plus sign. Let me write this. " + style 1-43 fg=bright-black italic +16| +17| "▌ " + style 0-0 fg=green +18| "▌ ✓ const out1 = await tools.bash({ command: \"echo CODE_ONE\", description: \"First echo\" }); " + style 0-0 fg=green + style 2-2 fg=green bold + style 3-99 bold +19| "▌ const o " + style 0-0 fg=green + style 2-8 bold +20| "▌ CODE_ONE+CODE_TWO " + style 0-0 fg=green +21| "▌ " + style 0-0 fg=green +22| +23| " Reasoning " + style 1-9 fg=bright-black italic +24| " The output is exactly what the user asked for: CODE_ONE+CODE_TWO " + style 1-64 fg=bright-black italic +25| +26| " Assistant " + style 1-9 fg=bright-magenta bold +27| " CODE_ONE+CODE_TWO " +28| "────────────────────────────────────────────────────────────────────────────────────────────────────" + style 0-99 dim +29| " " + style 1-1 inverse +30| "────────────────────────────────────────────────────────────────────────────────────────────────────" + style 0-99 dim +31| "/workspace/project ↑3.1k ↓158 idle" + style 0-93 dim + style 96-99 dim +32-35| diff --git a/examples/tui-agent/tests/snapshots/cordis-dynamic-toolchain/session.1.jsonl b/examples/tui-agent/tests/snapshots/cordis-dynamic-toolchain/session.1.jsonl new file mode 100644 index 0000000000..25a6f76411 --- /dev/null +++ b/examples/tui-agent/tests/snapshots/cordis-dynamic-toolchain/session.1.jsonl @@ -0,0 +1,13 @@ +{"type":"session","version":0,"id":"22222222-2222-4222-8222-222222222222","createdAt":1783950001000,"cwd":"/tmp/advanced-acp","parentSession":"11111111-1111-4111-8111-111111111111"} +{"type":"turn/start","seq":0,"time":1783957884563,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}} +{"type":"user/message","seq":1,"time":1783957884563,"data":{"content":[{"type":"text","text":"Reply with exactly DIRECT_CHILD_OK and nothing else."}],"source":{"kind":"user"}},"surfaceOp":"append"} +{"type":"step/start","seq":2,"time":1783957884564,"data":{"turn":1,"step":1}} +{"type":"request/header","seq":3,"time":1783957884564,"data":{"header":{"config":{"provider":"deepseek","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} +{"type":"assistant/chunk","seq":4,"time":1783950001005,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"text"}}} +{"type":"assistant/chunk","seq":5,"time":1783957884564,"data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":0,"text":"DIRECT_CHILD_OK"}}} +{"type":"assistant/chunk","seq":6,"time":1783957884564,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"DIRECT_CHILD_OK"}}}} +{"type":"assistant/chunk","seq":7,"time":1783957884564,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}} +{"type":"assistant/chunk","seq":8,"time":1783957884564,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}} +{"type":"assistant/message","seq":9,"time":1783957884564,"data":{"turn":1,"step":1,"content":[{"type":"text","text":"DIRECT_CHILD_OK"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[4,5,6,7,8],"surfaceOp":"append"} +{"type":"step/end","seq":10,"time":1783957884564,"data":{"turn":1,"step":1}} +{"type":"turn/end","seq":11,"time":1783957884564,"data":{"turn":1,"reason":{"kind":"completed"}}} diff --git a/examples/tui-agent/tests/snapshots/cordis-dynamic-toolchain/session.2.jsonl b/examples/tui-agent/tests/snapshots/cordis-dynamic-toolchain/session.2.jsonl new file mode 100644 index 0000000000..45d9043a4a --- /dev/null +++ b/examples/tui-agent/tests/snapshots/cordis-dynamic-toolchain/session.2.jsonl @@ -0,0 +1,13 @@ +{"type":"session","version":0,"id":"33333333-3333-4333-8333-333333333333","createdAt":1783950002000,"cwd":"/tmp/advanced-acp","parentSession":"11111111-1111-4111-8111-111111111111"} +{"type":"turn/start","seq":0,"time":1783957884700,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}} +{"type":"user/message","seq":1,"time":1783957884700,"data":{"content":[{"type":"text","text":"Reply with exactly WORKFLOW_CHILD_OK and nothing else."}],"source":{"kind":"user"}},"surfaceOp":"append"} +{"type":"step/start","seq":2,"time":1783957884700,"data":{"turn":1,"step":1}} +{"type":"request/header","seq":3,"time":1783957884701,"data":{"header":{"config":{"provider":"deepseek","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} +{"type":"assistant/chunk","seq":4,"time":1783950002005,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"text"}}} +{"type":"assistant/chunk","seq":5,"time":1783957884701,"data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":0,"text":"WORKFLOW_CHILD_OK"}}} +{"type":"assistant/chunk","seq":6,"time":1783957884701,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"WORKFLOW_CHILD_OK"}}}} +{"type":"assistant/chunk","seq":7,"time":1783957884701,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}} +{"type":"assistant/chunk","seq":8,"time":1783957884701,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}} +{"type":"assistant/message","seq":9,"time":1783957884701,"data":{"turn":1,"step":1,"content":[{"type":"text","text":"WORKFLOW_CHILD_OK"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[4,5,6,7,8],"surfaceOp":"append"} +{"type":"step/end","seq":10,"time":1783957884701,"data":{"turn":1,"step":1}} +{"type":"turn/end","seq":11,"time":1783957884701,"data":{"turn":1,"reason":{"kind":"completed"}}} diff --git a/examples/tui-agent/tests/snapshots/cordis-dynamic-toolchain/session.jsonl b/examples/tui-agent/tests/snapshots/cordis-dynamic-toolchain/session.jsonl new file mode 100644 index 0000000000..39f867984a --- /dev/null +++ b/examples/tui-agent/tests/snapshots/cordis-dynamic-toolchain/session.jsonl @@ -0,0 +1,64 @@ +{"type":"session","version":0,"id":"11111111-1111-4111-8111-111111111111","createdAt":1783950000000,"cwd":"/tmp/advanced-acp"} +{"type":"turn/start","seq":0,"time":1783957884479,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}} +{"type":"user/message","seq":1,"time":1783957884479,"data":{"content":[{"type":"text","text":"Run this advanced flow exactly once: mount a no-op Cordis plugin named snapshot-marker; use run_code to inspect the live dynamic mounts through tools.cordis_inspect; delegate once to a direct spawn child; run one workflow that delegates to another spawn child; unmount dyn-1; then reply with exactly ADVANCED_ACP_OK."}],"source":{"kind":"user"}},"surfaceOp":"append"} +{"type":"step/start","seq":2,"time":1783957884486,"data":{"turn":1,"step":1}} +{"type":"request/header","seq":3,"time":1783957884486,"data":{"header":{"config":{"provider":"deepseek","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} +{"type":"assistant/chunk","seq":4,"time":1783950000005,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}} +{"type":"assistant/chunk","seq":5,"time":1783950000006,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":0,"id":"advanced-mount","name":"cordis_mount","argumentsDelta":"{\"code\":\"return { name: 'snapshot-marker', apply() {} }\"}"}}} +{"type":"assistant/chunk","seq":6,"time":1783950000007,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-mount","name":"cordis_mount","arguments":"{\"code\":\"return { name: 'snapshot-marker', apply() {} }\"}"}}}} +{"type":"assistant/chunk","seq":7,"time":1783950000008,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}} +{"type":"assistant/chunk","seq":8,"time":1783950000009,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} +{"type":"assistant/message","seq":9,"time":1783957884487,"data":{"turn":1,"step":1,"content":[{"type":"tool-call","id":"advanced-mount","name":"cordis_mount","arguments":"{\"code\":\"return { name: 'snapshot-marker', apply() {} }\"}"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[4,5,6,7,8],"surfaceOp":"append"} +{"type":"tool/call","seq":10,"time":1783957884487,"data":{"turn":1,"step":1,"callId":"advanced-mount","name":"cordis_mount","arguments":"{\"code\":\"return { name: 'snapshot-marker', apply() {} }\"}"}} +{"type":"tool/result","seq":11,"time":1783957884488,"data":{"turn":1,"step":1,"callId":"advanced-mount","content":[{"type":"text","text":"mounted dyn-1 (plugin \"snapshot-marker\", state: active)"}],"isError":false},"sourceEventSeqs":[10],"surfaceOp":"append"} +{"type":"step/end","seq":12,"time":1783957884489,"data":{"turn":1,"step":1}} +{"type":"step/start","seq":13,"time":1783957884489,"data":{"turn":1,"step":2}} +{"type":"assistant/chunk","seq":14,"time":1783950000015,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}} +{"type":"assistant/chunk","seq":15,"time":1783950000016,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":0,"id":"advanced-code","name":"run_code","argumentsDelta":"{\"code\":\"return await tools.cordis_inspect({ what: 'dynamic' })\"}"}}} +{"type":"assistant/chunk","seq":16,"time":1783950000017,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-code","name":"run_code","arguments":"{\"code\":\"return await tools.cordis_inspect({ what: 'dynamic' })\"}"}}}} +{"type":"assistant/chunk","seq":17,"time":1783950000018,"data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}} +{"type":"assistant/chunk","seq":18,"time":1783950000019,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} +{"type":"assistant/message","seq":19,"time":1783957884490,"data":{"turn":1,"step":2,"content":[{"type":"tool-call","id":"advanced-code","name":"run_code","arguments":"{\"code\":\"return await tools.cordis_inspect({ what: 'dynamic' })\"}"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[14,15,16,17,18],"surfaceOp":"append"} +{"type":"tool/call","seq":20,"time":1783957884490,"data":{"turn":1,"step":2,"callId":"advanced-code","name":"run_code","arguments":"{\"code\":\"return await tools.cordis_inspect({ what: 'dynamic' })\"}"}} +{"type":"tool/code-dispatch","seq":21,"time":1783957884560,"data":{"parentCallId":"advanced-code","subCallId":"advanced-code:code:1","name":"cordis_inspect","arguments":{"what":"dynamic"},"isError":false,"resultSummary":"## dynamic\n- dyn-1: snapshot-marker [active]"}} +{"type":"tool/result","seq":22,"time":1783957884561,"data":{"turn":1,"step":2,"callId":"advanced-code","content":[{"type":"text","text":"## dynamic\n- dyn-1: snapshot-marker [active]"}],"isError":false,"meta":{"logs":[]}},"sourceEventSeqs":[20],"surfaceOp":"append"} +{"type":"step/end","seq":23,"time":1783957884561,"data":{"turn":1,"step":2}} +{"type":"step/start","seq":24,"time":1783957884562,"data":{"turn":1,"step":3}} +{"type":"assistant/chunk","seq":25,"time":1783950000026,"data":{"turn":1,"step":3,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}} +{"type":"assistant/chunk","seq":26,"time":1783950000027,"data":{"turn":1,"step":3,"chunk":{"type":"tool-call-delta","index":0,"id":"advanced-direct-child","name":"subagent","argumentsDelta":"{\"description\":\"Check direct child\",\"prompt\":\"Reply with exactly DIRECT_CHILD_OK and nothing else.\"}"}}} +{"type":"assistant/chunk","seq":27,"time":1783950000028,"data":{"turn":1,"step":3,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-direct-child","name":"subagent","arguments":"{\"description\":\"Check direct child\",\"prompt\":\"Reply with exactly DIRECT_CHILD_OK and nothing else.\"}"}}}} +{"type":"assistant/chunk","seq":28,"time":1783950000029,"data":{"turn":1,"step":3,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}} +{"type":"assistant/chunk","seq":29,"time":1783950000030,"data":{"turn":1,"step":3,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} +{"type":"assistant/message","seq":30,"time":1783957884562,"data":{"turn":1,"step":3,"content":[{"type":"tool-call","id":"advanced-direct-child","name":"subagent","arguments":"{\"description\":\"Check direct child\",\"prompt\":\"Reply with exactly DIRECT_CHILD_OK and nothing else.\"}"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[25,26,27,28,29],"surfaceOp":"append"} +{"type":"tool/call","seq":31,"time":1783957884562,"data":{"turn":1,"step":3,"callId":"advanced-direct-child","name":"subagent","arguments":"{\"description\":\"Check direct child\",\"prompt\":\"Reply with exactly DIRECT_CHILD_OK and nothing else.\"}"}} +{"type":"tool/result","seq":32,"time":1783957884593,"data":{"turn":1,"step":3,"callId":"advanced-direct-child","content":[{"type":"text","text":"DIRECT_CHILD_OK"}],"isError":false},"sourceEventSeqs":[31],"surfaceOp":"append"} +{"type":"step/end","seq":33,"time":1783957884593,"data":{"turn":1,"step":3}} +{"type":"step/start","seq":34,"time":1783957884594,"data":{"turn":1,"step":4}} +{"type":"assistant/chunk","seq":35,"time":1783957884594,"data":{"turn":1,"step":4,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}} +{"type":"assistant/chunk","seq":36,"time":1783957884594,"data":{"turn":1,"step":4,"chunk":{"type":"tool-call-delta","index":0,"id":"advanced-workflow","name":"workflow","argumentsDelta":"{\"script\":\"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\",\"meta\":{\"name\":\"advanced-acp-snapshot\",\"description\":\"exercise one workflow child through ACP\"}}"}}} +{"type":"assistant/chunk","seq":37,"time":1783957884594,"data":{"turn":1,"step":4,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-workflow","name":"workflow","arguments":"{\"script\":\"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\",\"meta\":{\"name\":\"advanced-acp-snapshot\",\"description\":\"exercise one workflow child through ACP\"}}"}}}} +{"type":"assistant/chunk","seq":38,"time":1783957884594,"data":{"turn":1,"step":4,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}} +{"type":"assistant/chunk","seq":39,"time":1783957884594,"data":{"turn":1,"step":4,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} +{"type":"assistant/message","seq":40,"time":1783957884594,"data":{"turn":1,"step":4,"content":[{"type":"tool-call","id":"advanced-workflow","name":"workflow","arguments":"{\"script\":\"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\",\"meta\":{\"name\":\"advanced-acp-snapshot\",\"description\":\"exercise one workflow child through ACP\"}}"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[35,36,37,38,39],"surfaceOp":"append"} +{"type":"tool/call","seq":41,"time":1783957884594,"data":{"turn":1,"step":4,"callId":"advanced-workflow","name":"workflow","arguments":"{\"script\":\"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\",\"meta\":{\"name\":\"advanced-acp-snapshot\",\"description\":\"exercise one workflow child through ACP\"}}"}} +{"type":"tool/result","seq":42,"time":1783957884717,"data":{"turn":1,"step":4,"callId":"advanced-workflow","content":[{"type":"text","text":"workflow \"advanced-acp-snapshot\" completed (1 agent).\nReturn value:\n{\n \"reply\": \"WORKFLOW_CHILD_OK\"\n}"}],"isError":false},"sourceEventSeqs":[41],"surfaceOp":"append"} +{"type":"step/end","seq":43,"time":1783957884718,"data":{"turn":1,"step":4}} +{"type":"step/start","seq":44,"time":1783957884718,"data":{"turn":1,"step":5}} +{"type":"assistant/chunk","seq":45,"time":1783957884719,"data":{"turn":1,"step":5,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}} +{"type":"assistant/chunk","seq":46,"time":1783957884719,"data":{"turn":1,"step":5,"chunk":{"type":"tool-call-delta","index":0,"id":"advanced-unmount","name":"cordis_unmount","argumentsDelta":"{\"id\":\"dyn-1\"}"}}} +{"type":"assistant/chunk","seq":47,"time":1783957884719,"data":{"turn":1,"step":5,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-unmount","name":"cordis_unmount","arguments":"{\"id\":\"dyn-1\"}"}}}} +{"type":"assistant/chunk","seq":48,"time":1783957884719,"data":{"turn":1,"step":5,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}} +{"type":"assistant/chunk","seq":49,"time":1783957884719,"data":{"turn":1,"step":5,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} +{"type":"assistant/message","seq":50,"time":1783957884719,"data":{"turn":1,"step":5,"content":[{"type":"tool-call","id":"advanced-unmount","name":"cordis_unmount","arguments":"{\"id\":\"dyn-1\"}"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[45,46,47,48,49],"surfaceOp":"append"} +{"type":"tool/call","seq":51,"time":1783957884719,"data":{"turn":1,"step":5,"callId":"advanced-unmount","name":"cordis_unmount","arguments":"{\"id\":\"dyn-1\"}"}} +{"type":"tool/result","seq":52,"time":1783957884719,"data":{"turn":1,"step":5,"callId":"advanced-unmount","content":[{"type":"text","text":"unmounted dyn-1 (plugin \"snapshot-marker\")"}],"isError":false},"sourceEventSeqs":[51],"surfaceOp":"append"} +{"type":"step/end","seq":53,"time":1783957884719,"data":{"turn":1,"step":5}} +{"type":"step/start","seq":54,"time":1783957884720,"data":{"turn":1,"step":6}} +{"type":"assistant/chunk","seq":55,"time":1783957884720,"data":{"turn":1,"step":6,"chunk":{"type":"block-start","index":0,"blockType":"text"}}} +{"type":"assistant/chunk","seq":56,"time":1783957884720,"data":{"turn":1,"step":6,"chunk":{"type":"text-delta","index":0,"text":"ADVANCED_ACP_OK"}}} +{"type":"assistant/chunk","seq":57,"time":1783957884720,"data":{"turn":1,"step":6,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"ADVANCED_ACP_OK"}}}} +{"type":"assistant/chunk","seq":58,"time":1783957884720,"data":{"turn":1,"step":6,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}} +{"type":"assistant/chunk","seq":59,"time":1783957884720,"data":{"turn":1,"step":6,"chunk":{"type":"finish","reason":{"kind":"stop"}}}} +{"type":"assistant/message","seq":60,"time":1783957884720,"data":{"turn":1,"step":6,"content":[{"type":"text","text":"ADVANCED_ACP_OK"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[55,56,57,58,59],"surfaceOp":"append"} +{"type":"step/end","seq":61,"time":1783957884721,"data":{"turn":1,"step":6}} +{"type":"turn/end","seq":62,"time":1783957884721,"data":{"turn":1,"reason":{"kind":"completed"}}} diff --git a/examples/tui-agent/tests/snapshots/cordis-dynamic-toolchain/terminal.golden.txt b/examples/tui-agent/tests/snapshots/cordis-dynamic-toolchain/terminal.golden.txt new file mode 100644 index 0000000000..5c5afc10ca --- /dev/null +++ b/examples/tui-agent/tests/snapshots/cordis-dynamic-toolchain/terminal.golden.txt @@ -0,0 +1,115 @@ +terminal 100x36 buffer=normal length=50 base=14 viewport=14 +lifecycle started=1 stopped=0 progress=inactive +title "DSH TUI snapshot" +cursor hidden column=1 viewportRow=33 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| "│ Recorded replay: cordis-dynamic-toolchain │" + style 0-0 fg=bright-blue + style 2-42 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| +6| "▌ " + style 0-0 fg=bright-blue +7| "▌ You " + style 0-0 fg=bright-blue + style 2-4 fg=bright-blue bold +8| "▌ Run this advanced flow exactly once: mount a no-op Cordis plugin named snapshot-marker; use " + style 0-0 fg=bright-blue +9| "▌ run_code to inspect the live dynamic mounts through tools.cordis_inspect; delegate once to a " + style 0-0 fg=bright-blue +10| "▌ direct spawn child; run one workflow that delegates to another spawn child; unmount dyn-1; then " + style 0-0 fg=bright-blue +11| "▌ reply with exactly ADVANCED_ACP_OK. " + style 0-0 fg=bright-blue +12| "▌ " + style 0-0 fg=bright-blue +13| +14| "▌ " + style 0-0 fg=green +15| "▌ ✓ Mount plugin into live cordis runtime " + style 0-0 fg=green + style 2-2 fg=green bold + style 3-40 bold +16| "▌ mounted dyn-1 (plugin \"snapshot-marker\", state: active) " + style 0-0 fg=green +17| "▌ " + style 0-0 fg=green +18| +19| "▌ " + style 0-0 fg=green +20| "▌ ✓ return await tools.cordis_inspect({ what: 'dynamic' }) " + style 0-0 fg=green + style 2-2 fg=green bold + style 3-57 bold +21| "▌ ## dynamic " + style 0-0 fg=green +22| "▌ - dyn-1: snapshot-marker [active] " + style 0-0 fg=green +23| "▌ " + style 0-0 fg=green +24| +25| "▌ " + style 0-0 fg=green +26| "▌ ✓ subagent " + style 0-0 fg=green + style 2-2 fg=green bold + style 3-11 bold +27| "▌ DIRECT_CHILD_OK " + style 0-0 fg=green +28| "▌ " + style 0-0 fg=green +29| +30| "▌ " + style 0-0 fg=green +31| "▌ ✓ workflow: advanced-acp-snapshot " + style 0-0 fg=green + style 2-2 fg=green bold + style 3-34 bold +32| "▌ workflow \"advanced-acp-snapshot\" completed (1 agent). " + style 0-0 fg=green +33| "▌ Return value: " + style 0-0 fg=green +34| "▌ { " + style 0-0 fg=green +35| "▌ \"reply\": \"WORKFLOW_CHILD_OK\" " + style 0-0 fg=green +36| "▌ } " + style 0-0 fg=green +37| "▌ " + style 0-0 fg=green +38| +39| "▌ " + style 0-0 fg=green +40| "▌ ✓ Unmount dyn-1 " + style 0-0 fg=green + style 2-2 fg=green bold + style 3-16 bold +41| "▌ unmounted dyn-1 (plugin \"snapshot-marker\") " + style 0-0 fg=green +42| "▌ " + style 0-0 fg=green +43| +44| " Assistant " + style 1-9 fg=bright-magenta bold +45| " ADVANCED_ACP_OK " +46| "────────────────────────────────────────────────────────────────────────────────────────────────────" + style 0-99 dim +47| " " + style 1-1 inverse +48| "────────────────────────────────────────────────────────────────────────────────────────────────────" + style 0-99 dim +49| "/workspace/project ↑" + style 0-99 dim diff --git a/examples/tui-agent/tests/snapshots/dynamic-workflow/session.1.jsonl b/examples/tui-agent/tests/snapshots/dynamic-workflow/session.1.jsonl new file mode 100644 index 0000000000..3d89428bbd --- /dev/null +++ b/examples/tui-agent/tests/snapshots/dynamic-workflow/session.1.jsonl @@ -0,0 +1,36 @@ +{"type":"session","version":0,"id":"583a4db2-3350-436c-b4a5-5615fd159052","createdAt":1783600636316,"cwd":"/var/folders/bn/vj1dvck95yd5jh3x4wskflxm0000gn/T/acp-snap-cwd-vdJYjz","parentSession":"3fd7d599-56b1-493a-930d-f1fc5e1556e8"} +{"type":"turn/start","seq":0,"time":1783600636316,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}} +{"type":"user/message","seq":1,"time":1783600636316,"data":{"content":[{"type":"text","text":"Reply with exactly the word WF_CHILD_OK and nothing else."}],"source":{"kind":"user"}},"surfaceOp":"append"} +{"type":"step/start","seq":2,"time":1783600636316,"data":{"turn":1,"step":1}} +{"type":"request/header","seq":3,"time":1783600636317,"data":{"header":{"config":{"provider":"deepseek","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} +{"type":"assistant/chunk","seq":4,"time":1783600638073,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}} +{"type":"assistant/chunk","seq":5,"time":1783600638073,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"The"}}} +{"type":"assistant/chunk","seq":6,"time":1783600638173,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" user"}}} +{"type":"assistant/chunk","seq":7,"time":1783600638189,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" wants"}}} +{"type":"assistant/chunk","seq":8,"time":1783600638189,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" me"}}} +{"type":"assistant/chunk","seq":9,"time":1783600638189,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" to"}}} +{"type":"assistant/chunk","seq":10,"time":1783600638189,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" reply"}}} +{"type":"assistant/chunk","seq":11,"time":1783600638189,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" with"}}} +{"type":"assistant/chunk","seq":12,"time":1783600638213,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" exactly"}}} +{"type":"assistant/chunk","seq":13,"time":1783600638213,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" \""}}} +{"type":"assistant/chunk","seq":14,"time":1783600638213,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"WF"}}} +{"type":"assistant/chunk","seq":15,"time":1783600638213,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"_CH"}}} +{"type":"assistant/chunk","seq":16,"time":1783600638213,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"ILD"}}} +{"type":"assistant/chunk","seq":17,"time":1783600638242,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"_OK"}}} +{"type":"assistant/chunk","seq":18,"time":1783600638242,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"\""}}} +{"type":"assistant/chunk","seq":19,"time":1783600638242,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" and"}}} +{"type":"assistant/chunk","seq":20,"time":1783600638242,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" nothing"}}} +{"type":"assistant/chunk","seq":21,"time":1783600638242,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" else"}}} +{"type":"assistant/chunk","seq":22,"time":1783600638242,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"."}}} +{"type":"assistant/chunk","seq":23,"time":1783600638276,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":1,"blockType":"text"}}} +{"type":"assistant/chunk","seq":24,"time":1783600638276,"data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":1,"text":"WF"}}} +{"type":"assistant/chunk","seq":25,"time":1783600638276,"data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":1,"text":"_CH"}}} +{"type":"assistant/chunk","seq":26,"time":1783600638276,"data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":1,"text":"ILD"}}} +{"type":"assistant/chunk","seq":27,"time":1783600638276,"data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":1,"text":"_OK"}}} +{"type":"assistant/chunk","seq":28,"time":1783600638280,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to reply with exactly \"WF_CHILD_OK\" and nothing else."}}}} +{"type":"assistant/chunk","seq":29,"time":1783600638280,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"WF_CHILD_OK"}}}} +{"type":"assistant/chunk","seq":30,"time":1783600638280,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":17,"outputTokens":23,"cacheReadTokens":3072,"reasoningTokens":18}}}} +{"type":"assistant/chunk","seq":31,"time":1783600638280,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}} +{"type":"assistant/message","seq":32,"time":1783600638281,"data":{"turn":1,"step":1,"content":[{"type":"reasoning","text":"The user wants me to reply with exactly \"WF_CHILD_OK\" and nothing else."},{"type":"text","text":"WF_CHILD_OK"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":17,"outputTokens":23,"cacheReadTokens":3072,"reasoningTokens":18}},"sourceEventSeqs":[4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31],"surfaceOp":"append"} +{"type":"step/end","seq":33,"time":1783600638281,"data":{"turn":1,"step":1}} +{"type":"turn/end","seq":34,"time":1783600638281,"data":{"turn":1,"reason":{"kind":"completed"}}} diff --git a/examples/tui-agent/tests/snapshots/dynamic-workflow/session.jsonl b/examples/tui-agent/tests/snapshots/dynamic-workflow/session.jsonl new file mode 100644 index 0000000000..3e0ae3da73 --- /dev/null +++ b/examples/tui-agent/tests/snapshots/dynamic-workflow/session.jsonl @@ -0,0 +1,209 @@ +{"type":"session","version":0,"id":"3fd7d599-56b1-493a-930d-f1fc5e1556e8","createdAt":1783600631835,"cwd":"/var/folders/bn/vj1dvck95yd5jh3x4wskflxm0000gn/T/acp-snap-cwd-vdJYjz"} +{"type":"turn/start","seq":0,"time":1783600631838,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}} +{"type":"user/message","seq":1,"time":1783600631838,"data":{"content":[{"type":"text","text":"Use the workflow tool exactly once, with args omitted, meta set to { \"name\": \"snapshot-flow\", \"description\": \"one child for the snapshot\" }, and this EXACT script body (copy it verbatim):\nphase('Run')\nconst reply = await agent('Reply with exactly the word WF_CHILD_OK and nothing else.')\nreturn { reply }\nAfter the workflow returns, reply with the single word WORKFLOW_DONE and stop. Do not use any other tool."}],"source":{"kind":"user"}},"surfaceOp":"append"} +{"type":"step/start","seq":2,"time":1783600631839,"data":{"turn":1,"step":1}} +{"type":"request/header","seq":3,"time":1783600631839,"data":{"header":{"config":{"provider":"deepseek","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} +{"type":"assistant/chunk","seq":4,"time":1783600634643,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}} +{"type":"assistant/chunk","seq":5,"time":1783600634643,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"The"}}} +{"type":"assistant/chunk","seq":6,"time":1783600635634,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" user"}}} +{"type":"assistant/chunk","seq":7,"time":1783600635634,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" wants"}}} +{"type":"assistant/chunk","seq":8,"time":1783600635634,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" me"}}} +{"type":"assistant/chunk","seq":9,"time":1783600635634,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" to"}}} +{"type":"assistant/chunk","seq":10,"time":1783600635634,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" use"}}} +{"type":"assistant/chunk","seq":11,"time":1783600635634,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" the"}}} +{"type":"assistant/chunk","seq":12,"time":1783600635634,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" workflow"}}} +{"type":"assistant/chunk","seq":13,"time":1783600635634,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" tool"}}} +{"type":"assistant/chunk","seq":14,"time":1783600635634,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" exactly"}}} +{"type":"assistant/chunk","seq":15,"time":1783600635634,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" once"}}} +{"type":"assistant/chunk","seq":16,"time":1783600635635,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" with"}}} +{"type":"assistant/chunk","seq":17,"time":1783600635635,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" specific"}}} +{"type":"assistant/chunk","seq":18,"time":1783600635635,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" parameters"}}} +{"type":"assistant/chunk","seq":19,"time":1783600635635,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"."}}} +{"type":"assistant/chunk","seq":20,"time":1783600635743,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" Let"}}} +{"type":"assistant/chunk","seq":21,"time":1783600635743,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" me"}}} +{"type":"assistant/chunk","seq":22,"time":1783600635743,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" carefully"}}} +{"type":"assistant/chunk","seq":23,"time":1783600635743,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" follow"}}} +{"type":"assistant/chunk","seq":24,"time":1783600635743,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" the"}}} +{"type":"assistant/chunk","seq":25,"time":1783600635743,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" instructions"}}} +{"type":"assistant/chunk","seq":26,"time":1783600635743,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":":\n\n"}}} +{"type":"assistant/chunk","seq":27,"time":1783600635744,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"1"}}} +{"type":"assistant/chunk","seq":28,"time":1783600635744,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"."}}} +{"type":"assistant/chunk","seq":29,"time":1783600635744,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" args"}}} +{"type":"assistant/chunk","seq":30,"time":1783600635744,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" omitted"}}} +{"type":"assistant/chunk","seq":31,"time":1783600635744,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" ("}}} +{"type":"assistant/chunk","seq":32,"time":1783600635744,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"so"}}} +{"type":"assistant/chunk","seq":33,"time":1783600635744,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" I"}}} +{"type":"assistant/chunk","seq":34,"time":1783600635744,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" don"}}} +{"type":"assistant/chunk","seq":35,"time":1783600635744,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"'t"}}} +{"type":"assistant/chunk","seq":36,"time":1783600635744,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" include"}}} +{"type":"assistant/chunk","seq":37,"time":1783600635744,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" it"}}} +{"type":"assistant/chunk","seq":38,"time":1783600635744,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":")\n"}}} +{"type":"assistant/chunk","seq":39,"time":1783600635744,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"2"}}} +{"type":"assistant/chunk","seq":40,"time":1783600635744,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"."}}} +{"type":"assistant/chunk","seq":41,"time":1783600635744,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" meta"}}} +{"type":"assistant/chunk","seq":42,"time":1783600635744,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" ="}}} +{"type":"assistant/chunk","seq":43,"time":1783600635744,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" {"}}} +{"type":"assistant/chunk","seq":44,"time":1783600635744,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" \""}}} +{"type":"assistant/chunk","seq":45,"time":1783600635744,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"name"}}} +{"type":"assistant/chunk","seq":46,"time":1783600635744,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"\":"}}} +{"type":"assistant/chunk","seq":47,"time":1783600635745,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" \""}}} +{"type":"assistant/chunk","seq":48,"time":1783600635745,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"sn"}}} +{"type":"assistant/chunk","seq":49,"time":1783600635745,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"apshot"}}} +{"type":"assistant/chunk","seq":50,"time":1783600635745,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"-flow"}}} +{"type":"assistant/chunk","seq":51,"time":1783600635745,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"\","}}} +{"type":"assistant/chunk","seq":52,"time":1783600635745,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" \""}}} +{"type":"assistant/chunk","seq":53,"time":1783600635745,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"description"}}} +{"type":"assistant/chunk","seq":54,"time":1783600635745,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"\":"}}} +{"type":"assistant/chunk","seq":55,"time":1783600635745,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" \""}}} +{"type":"assistant/chunk","seq":56,"time":1783600635745,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"one"}}} +{"type":"assistant/chunk","seq":57,"time":1783600635745,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" child"}}} +{"type":"assistant/chunk","seq":58,"time":1783600635745,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" for"}}} +{"type":"assistant/chunk","seq":59,"time":1783600635745,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" the"}}} +{"type":"assistant/chunk","seq":60,"time":1783600635745,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" snapshot"}}} +{"type":"assistant/chunk","seq":61,"time":1783600635745,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"\""}}} +{"type":"assistant/chunk","seq":62,"time":1783600635745,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" }\n"}}} +{"type":"assistant/chunk","seq":63,"time":1783600635745,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"3"}}} +{"type":"assistant/chunk","seq":64,"time":1783600635745,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"."}}} +{"type":"assistant/chunk","seq":65,"time":1783600635745,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" script"}}} +{"type":"assistant/chunk","seq":66,"time":1783600635745,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" ="}}} +{"type":"assistant/chunk","seq":67,"time":1783600635745,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" as"}}} +{"type":"assistant/chunk","seq":68,"time":1783600635745,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" given"}}} +{"type":"assistant/chunk","seq":69,"time":1783600635745,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" verb"}}} +{"type":"assistant/chunk","seq":70,"time":1783600635745,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"atim"}}} +{"type":"assistant/chunk","seq":71,"time":1783600635745,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"\n"}}} +{"type":"assistant/chunk","seq":72,"time":1783600635746,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"4"}}} +{"type":"assistant/chunk","seq":73,"time":1783600635746,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"."}}} +{"type":"assistant/chunk","seq":74,"time":1783600635746,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" After"}}} +{"type":"assistant/chunk","seq":75,"time":1783600635746,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" it"}}} +{"type":"assistant/chunk","seq":76,"time":1783600635754,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" returns"}}} +{"type":"assistant/chunk","seq":77,"time":1783600635754,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":","}}} +{"type":"assistant/chunk","seq":78,"time":1783600635754,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" reply"}}} +{"type":"assistant/chunk","seq":79,"time":1783600635754,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" with"}}} +{"type":"assistant/chunk","seq":80,"time":1783600635754,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" \""}}} +{"type":"assistant/chunk","seq":81,"time":1783600635754,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"WORK"}}} +{"type":"assistant/chunk","seq":82,"time":1783600635754,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"FL"}}} +{"type":"assistant/chunk","seq":83,"time":1783600635756,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"OW"}}} +{"type":"assistant/chunk","seq":84,"time":1783600635756,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"_D"}}} +{"type":"assistant/chunk","seq":85,"time":1783600635756,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"ONE"}}} +{"type":"assistant/chunk","seq":86,"time":1783600635756,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"\"\n\n"}}} +{"type":"assistant/chunk","seq":87,"time":1783600635756,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"Let"}}} +{"type":"assistant/chunk","seq":88,"time":1783600635756,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" me"}}} +{"type":"assistant/chunk","seq":89,"time":1783600635756,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" do"}}} +{"type":"assistant/chunk","seq":90,"time":1783600635756,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" exactly"}}} +{"type":"assistant/chunk","seq":91,"time":1783600635756,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" that"}}} +{"type":"assistant/chunk","seq":92,"time":1783600635756,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"."}}} +{"type":"assistant/chunk","seq":93,"time":1783600635756,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}}} +{"type":"assistant/chunk","seq":94,"time":1783600635756,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_dD2BLuNeJCTh2iiYC1QR3449","name":"workflow","argumentsDelta":""}}} +{"type":"assistant/chunk","seq":95,"time":1783600635756,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_dD2BLuNeJCTh2iiYC1QR3449","name":"workflow","argumentsDelta":"{"}}} +{"type":"assistant/chunk","seq":96,"time":1783600635756,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_dD2BLuNeJCTh2iiYC1QR3449","name":"workflow","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":97,"time":1783600635756,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_dD2BLuNeJCTh2iiYC1QR3449","name":"workflow","argumentsDelta":"meta"}}} +{"type":"assistant/chunk","seq":98,"time":1783600635756,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_dD2BLuNeJCTh2iiYC1QR3449","name":"workflow","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":99,"time":1783600635756,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_dD2BLuNeJCTh2iiYC1QR3449","name":"workflow","argumentsDelta":": "}}} +{"type":"assistant/chunk","seq":100,"time":1783600635757,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_dD2BLuNeJCTh2iiYC1QR3449","name":"workflow","argumentsDelta":"{\""}}} +{"type":"assistant/chunk","seq":101,"time":1783600635757,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_dD2BLuNeJCTh2iiYC1QR3449","name":"workflow","argumentsDelta":"name"}}} +{"type":"assistant/chunk","seq":102,"time":1783600635757,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_dD2BLuNeJCTh2iiYC1QR3449","name":"workflow","argumentsDelta":"\":"}}} +{"type":"assistant/chunk","seq":103,"time":1783600635757,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_dD2BLuNeJCTh2iiYC1QR3449","name":"workflow","argumentsDelta":" \""}}} +{"type":"assistant/chunk","seq":104,"time":1783600635757,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_dD2BLuNeJCTh2iiYC1QR3449","name":"workflow","argumentsDelta":"sn"}}} +{"type":"assistant/chunk","seq":105,"time":1783600635757,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_dD2BLuNeJCTh2iiYC1QR3449","name":"workflow","argumentsDelta":"apshot"}}} +{"type":"assistant/chunk","seq":106,"time":1783600635757,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_dD2BLuNeJCTh2iiYC1QR3449","name":"workflow","argumentsDelta":"-flow"}}} +{"type":"assistant/chunk","seq":107,"time":1783600635757,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_dD2BLuNeJCTh2iiYC1QR3449","name":"workflow","argumentsDelta":"\","}}} +{"type":"assistant/chunk","seq":108,"time":1783600635757,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_dD2BLuNeJCTh2iiYC1QR3449","name":"workflow","argumentsDelta":" \""}}} +{"type":"assistant/chunk","seq":109,"time":1783600635757,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_dD2BLuNeJCTh2iiYC1QR3449","name":"workflow","argumentsDelta":"description"}}} +{"type":"assistant/chunk","seq":110,"time":1783600635757,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_dD2BLuNeJCTh2iiYC1QR3449","name":"workflow","argumentsDelta":"\":"}}} +{"type":"assistant/chunk","seq":111,"time":1783600635757,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_dD2BLuNeJCTh2iiYC1QR3449","name":"workflow","argumentsDelta":" \""}}} +{"type":"assistant/chunk","seq":112,"time":1783600635757,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_dD2BLuNeJCTh2iiYC1QR3449","name":"workflow","argumentsDelta":"one"}}} +{"type":"assistant/chunk","seq":113,"time":1783600635757,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_dD2BLuNeJCTh2iiYC1QR3449","name":"workflow","argumentsDelta":" child"}}} +{"type":"assistant/chunk","seq":114,"time":1783600635757,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_dD2BLuNeJCTh2iiYC1QR3449","name":"workflow","argumentsDelta":" for"}}} +{"type":"assistant/chunk","seq":115,"time":1783600635757,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_dD2BLuNeJCTh2iiYC1QR3449","name":"workflow","argumentsDelta":" the"}}} +{"type":"assistant/chunk","seq":116,"time":1783600635757,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_dD2BLuNeJCTh2iiYC1QR3449","name":"workflow","argumentsDelta":" snapshot"}}} +{"type":"assistant/chunk","seq":117,"time":1783600635757,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_dD2BLuNeJCTh2iiYC1QR3449","name":"workflow","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":118,"time":1783600635757,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_dD2BLuNeJCTh2iiYC1QR3449","name":"workflow","argumentsDelta":"}"}}} +{"type":"assistant/chunk","seq":119,"time":1783600635759,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_dD2BLuNeJCTh2iiYC1QR3449","name":"workflow","argumentsDelta":", "}}} +{"type":"assistant/chunk","seq":120,"time":1783600635759,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_dD2BLuNeJCTh2iiYC1QR3449","name":"workflow","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":121,"time":1783600635759,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_dD2BLuNeJCTh2iiYC1QR3449","name":"workflow","argumentsDelta":"script"}}} +{"type":"assistant/chunk","seq":122,"time":1783600635759,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_dD2BLuNeJCTh2iiYC1QR3449","name":"workflow","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":123,"time":1783600635759,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_dD2BLuNeJCTh2iiYC1QR3449","name":"workflow","argumentsDelta":": "}}} +{"type":"assistant/chunk","seq":124,"time":1783600635764,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_dD2BLuNeJCTh2iiYC1QR3449","name":"workflow","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":125,"time":1783600635764,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_dD2BLuNeJCTh2iiYC1QR3449","name":"workflow","argumentsDelta":"\\n"}}} +{"type":"assistant/chunk","seq":126,"time":1783600636039,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_dD2BLuNeJCTh2iiYC1QR3449","name":"workflow","argumentsDelta":"phase"}}} +{"type":"assistant/chunk","seq":127,"time":1783600636039,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_dD2BLuNeJCTh2iiYC1QR3449","name":"workflow","argumentsDelta":"('"}}} +{"type":"assistant/chunk","seq":128,"time":1783600636039,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_dD2BLuNeJCTh2iiYC1QR3449","name":"workflow","argumentsDelta":"Run"}}} +{"type":"assistant/chunk","seq":129,"time":1783600636039,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_dD2BLuNeJCTh2iiYC1QR3449","name":"workflow","argumentsDelta":"')\\n"}}} +{"type":"assistant/chunk","seq":130,"time":1783600636039,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_dD2BLuNeJCTh2iiYC1QR3449","name":"workflow","argumentsDelta":"const"}}} +{"type":"assistant/chunk","seq":131,"time":1783600636245,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_dD2BLuNeJCTh2iiYC1QR3449","name":"workflow","argumentsDelta":" reply"}}} +{"type":"assistant/chunk","seq":132,"time":1783600636245,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_dD2BLuNeJCTh2iiYC1QR3449","name":"workflow","argumentsDelta":" ="}}} +{"type":"assistant/chunk","seq":133,"time":1783600636245,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_dD2BLuNeJCTh2iiYC1QR3449","name":"workflow","argumentsDelta":" await"}}} +{"type":"assistant/chunk","seq":134,"time":1783600636245,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_dD2BLuNeJCTh2iiYC1QR3449","name":"workflow","argumentsDelta":" agent"}}} +{"type":"assistant/chunk","seq":135,"time":1783600636245,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_dD2BLuNeJCTh2iiYC1QR3449","name":"workflow","argumentsDelta":"('"}}} +{"type":"assistant/chunk","seq":136,"time":1783600636245,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_dD2BLuNeJCTh2iiYC1QR3449","name":"workflow","argumentsDelta":"Reply"}}} +{"type":"assistant/chunk","seq":137,"time":1783600636245,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_dD2BLuNeJCTh2iiYC1QR3449","name":"workflow","argumentsDelta":" with"}}} +{"type":"assistant/chunk","seq":138,"time":1783600636245,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_dD2BLuNeJCTh2iiYC1QR3449","name":"workflow","argumentsDelta":" exactly"}}} +{"type":"assistant/chunk","seq":139,"time":1783600636245,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_dD2BLuNeJCTh2iiYC1QR3449","name":"workflow","argumentsDelta":" the"}}} +{"type":"assistant/chunk","seq":140,"time":1783600636245,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_dD2BLuNeJCTh2iiYC1QR3449","name":"workflow","argumentsDelta":" word"}}} +{"type":"assistant/chunk","seq":141,"time":1783600636245,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_dD2BLuNeJCTh2iiYC1QR3449","name":"workflow","argumentsDelta":" WF"}}} +{"type":"assistant/chunk","seq":142,"time":1783600636245,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_dD2BLuNeJCTh2iiYC1QR3449","name":"workflow","argumentsDelta":"_CH"}}} +{"type":"assistant/chunk","seq":143,"time":1783600636245,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_dD2BLuNeJCTh2iiYC1QR3449","name":"workflow","argumentsDelta":"ILD"}}} +{"type":"assistant/chunk","seq":144,"time":1783600636245,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_dD2BLuNeJCTh2iiYC1QR3449","name":"workflow","argumentsDelta":"_OK"}}} +{"type":"assistant/chunk","seq":145,"time":1783600636245,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_dD2BLuNeJCTh2iiYC1QR3449","name":"workflow","argumentsDelta":" and"}}} +{"type":"assistant/chunk","seq":146,"time":1783600636245,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_dD2BLuNeJCTh2iiYC1QR3449","name":"workflow","argumentsDelta":" nothing"}}} +{"type":"assistant/chunk","seq":147,"time":1783600636245,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_dD2BLuNeJCTh2iiYC1QR3449","name":"workflow","argumentsDelta":" else"}}} +{"type":"assistant/chunk","seq":148,"time":1783600636246,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_dD2BLuNeJCTh2iiYC1QR3449","name":"workflow","argumentsDelta":".')\\n"}}} +{"type":"assistant/chunk","seq":149,"time":1783600636246,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_dD2BLuNeJCTh2iiYC1QR3449","name":"workflow","argumentsDelta":"return"}}} +{"type":"assistant/chunk","seq":150,"time":1783600636246,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_dD2BLuNeJCTh2iiYC1QR3449","name":"workflow","argumentsDelta":" {"}}} +{"type":"assistant/chunk","seq":151,"time":1783600636246,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_dD2BLuNeJCTh2iiYC1QR3449","name":"workflow","argumentsDelta":" reply"}}} +{"type":"assistant/chunk","seq":152,"time":1783600636246,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_dD2BLuNeJCTh2iiYC1QR3449","name":"workflow","argumentsDelta":" }\\n"}}} +{"type":"assistant/chunk","seq":153,"time":1783600636246,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_dD2BLuNeJCTh2iiYC1QR3449","name":"workflow","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":154,"time":1783600636246,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_dD2BLuNeJCTh2iiYC1QR3449","name":"workflow","argumentsDelta":"}"}}} +{"type":"assistant/chunk","seq":155,"time":1783600636246,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to use the workflow tool exactly once with specific parameters. Let me carefully follow the instructions:\n\n1. args omitted (so I don't include it)\n2. meta = { \"name\": \"snapshot-flow\", \"description\": \"one child for the snapshot\" }\n3. script = as given verbatim\n4. After it returns, reply with \"WORKFLOW_DONE\"\n\nLet me do exactly that."}}}} +{"type":"assistant/chunk","seq":156,"time":1783600636246,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_00_dD2BLuNeJCTh2iiYC1QR3449","name":"workflow","arguments":"{\"meta\": {\"name\": \"snapshot-flow\", \"description\": \"one child for the snapshot\"}, \"script\": \"\\nphase('Run')\\nconst reply = await agent('Reply with exactly the word WF_CHILD_OK and nothing else.')\\nreturn { reply }\\n\"}"}}}} +{"type":"assistant/chunk","seq":157,"time":1783600636246,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":3174,"outputTokens":191,"cacheReadTokens":0,"reasoningTokens":88}}}} +{"type":"assistant/chunk","seq":158,"time":1783600636246,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} +{"type":"assistant/message","seq":159,"time":1783600636247,"data":{"turn":1,"step":1,"content":[{"type":"reasoning","text":"The user wants me to use the workflow tool exactly once with specific parameters. Let me carefully follow the instructions:\n\n1. args omitted (so I don't include it)\n2. meta = { \"name\": \"snapshot-flow\", \"description\": \"one child for the snapshot\" }\n3. script = as given verbatim\n4. After it returns, reply with \"WORKFLOW_DONE\"\n\nLet me do exactly that."},{"type":"tool-call","id":"call_00_dD2BLuNeJCTh2iiYC1QR3449","name":"workflow","arguments":"{\"meta\": {\"name\": \"snapshot-flow\", \"description\": \"one child for the snapshot\"}, \"script\": \"\\nphase('Run')\\nconst reply = await agent('Reply with exactly the word WF_CHILD_OK and nothing else.')\\nreturn { reply }\\n\"}"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":3174,"outputTokens":191,"cacheReadTokens":0,"reasoningTokens":88}},"sourceEventSeqs":[4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158],"surfaceOp":"append"} +{"type":"tool/call","seq":160,"time":1783600636247,"data":{"turn":1,"step":1,"callId":"call_00_dD2BLuNeJCTh2iiYC1QR3449","name":"workflow","arguments":"{\"meta\": {\"name\": \"snapshot-flow\", \"description\": \"one child for the snapshot\"}, \"script\": \"\\nphase('Run')\\nconst reply = await agent('Reply with exactly the word WF_CHILD_OK and nothing else.')\\nreturn { reply }\\n\"}"}} +{"type":"tool/result","seq":161,"time":1783600638304,"data":{"turn":1,"step":1,"callId":"call_00_dD2BLuNeJCTh2iiYC1QR3449","content":[{"type":"text","text":"workflow \"snapshot-flow\" completed (1 agent).\nReturn value:\n{\n \"reply\": \"WF_CHILD_OK\"\n}"}],"isError":false},"sourceEventSeqs":[160],"surfaceOp":"append"} +{"type":"step/end","seq":162,"time":1783600638304,"data":{"turn":1,"step":1}} +{"type":"step/start","seq":163,"time":1783600638305,"data":{"turn":1,"step":2}} +{"type":"assistant/chunk","seq":164,"time":1783600640028,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}} +{"type":"assistant/chunk","seq":165,"time":1783600640028,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"The"}}} +{"type":"assistant/chunk","seq":166,"time":1783600640134,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" workflow"}}} +{"type":"assistant/chunk","seq":167,"time":1783600640162,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" returned"}}} +{"type":"assistant/chunk","seq":168,"time":1783600640195,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" successfully"}}} +{"type":"assistant/chunk","seq":169,"time":1783600640862,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" with"}}} +{"type":"assistant/chunk","seq":170,"time":1783600640862,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" the"}}} +{"type":"assistant/chunk","seq":171,"time":1783600640862,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" reply"}}} +{"type":"assistant/chunk","seq":172,"time":1783600640862,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" \""}}} +{"type":"assistant/chunk","seq":173,"time":1783600640862,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"WF"}}} +{"type":"assistant/chunk","seq":174,"time":1783600640862,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"_CH"}}} +{"type":"assistant/chunk","seq":175,"time":1783600640862,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"ILD"}}} +{"type":"assistant/chunk","seq":176,"time":1783600640864,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"_OK"}}} +{"type":"assistant/chunk","seq":177,"time":1783600640864,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"\"."}}} +{"type":"assistant/chunk","seq":178,"time":1783600640864,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" Now"}}} +{"type":"assistant/chunk","seq":179,"time":1783600640864,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" I"}}} +{"type":"assistant/chunk","seq":180,"time":1783600640864,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" need"}}} +{"type":"assistant/chunk","seq":181,"time":1783600640864,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" to"}}} +{"type":"assistant/chunk","seq":182,"time":1783600640864,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" reply"}}} +{"type":"assistant/chunk","seq":183,"time":1783600640864,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" with"}}} +{"type":"assistant/chunk","seq":184,"time":1783600640864,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" exactly"}}} +{"type":"assistant/chunk","seq":185,"time":1783600640865,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" \""}}} +{"type":"assistant/chunk","seq":186,"time":1783600640865,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"WORK"}}} +{"type":"assistant/chunk","seq":187,"time":1783600640865,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"FL"}}} +{"type":"assistant/chunk","seq":188,"time":1783600640865,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"OW"}}} +{"type":"assistant/chunk","seq":189,"time":1783600640865,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"_D"}}} +{"type":"assistant/chunk","seq":190,"time":1783600640865,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"ONE"}}} +{"type":"assistant/chunk","seq":191,"time":1783600640865,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"\""}}} +{"type":"assistant/chunk","seq":192,"time":1783600640865,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" and"}}} +{"type":"assistant/chunk","seq":193,"time":1783600640865,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" stop"}}} +{"type":"assistant/chunk","seq":194,"time":1783600640865,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"."}}} +{"type":"assistant/chunk","seq":195,"time":1783600640865,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":1,"blockType":"text"}}} +{"type":"assistant/chunk","seq":196,"time":1783600640865,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":"WORK"}}} +{"type":"assistant/chunk","seq":197,"time":1783600640865,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":"FL"}}} +{"type":"assistant/chunk","seq":198,"time":1783600640865,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":"OW"}}} +{"type":"assistant/chunk","seq":199,"time":1783600640865,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":"_D"}}} +{"type":"assistant/chunk","seq":200,"time":1783600640865,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":"ONE"}}} +{"type":"assistant/chunk","seq":201,"time":1783600640865,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The workflow returned successfully with the reply \"WF_CHILD_OK\". Now I need to reply with exactly \"WORKFLOW_DONE\" and stop."}}}} +{"type":"assistant/chunk","seq":202,"time":1783600640865,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"WORKFLOW_DONE"}}}} +{"type":"assistant/chunk","seq":203,"time":1783600640865,"data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":328,"outputTokens":36,"cacheReadTokens":3072,"reasoningTokens":30}}}} +{"type":"assistant/chunk","seq":204,"time":1783600640865,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"stop"}}}} +{"type":"assistant/message","seq":205,"time":1783600640865,"data":{"turn":1,"step":2,"content":[{"type":"reasoning","text":"The workflow returned successfully with the reply \"WF_CHILD_OK\". Now I need to reply with exactly \"WORKFLOW_DONE\" and stop."},{"type":"text","text":"WORKFLOW_DONE"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":328,"outputTokens":36,"cacheReadTokens":3072,"reasoningTokens":30}},"sourceEventSeqs":[164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204],"surfaceOp":"append"} +{"type":"step/end","seq":206,"time":1783600640865,"data":{"turn":1,"step":2}} +{"type":"turn/end","seq":207,"time":1783600640865,"data":{"turn":1,"reason":{"kind":"completed"}}} diff --git a/examples/tui-agent/tests/snapshots/dynamic-workflow/terminal.golden.txt b/examples/tui-agent/tests/snapshots/dynamic-workflow/terminal.golden.txt new file mode 100644 index 0000000000..8589ac94ea --- /dev/null +++ b/examples/tui-agent/tests/snapshots/dynamic-workflow/terminal.golden.txt @@ -0,0 +1,105 @@ +terminal 100x36 buffer=normal length=47 base=11 viewport=11 +lifecycle started=1 stopped=0 progress=inactive +title "DSH TUI snapshot" +cursor hidden column=1 viewportRow=33 bufferRow=44 +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| "│ Recorded replay: dynamic-workflow │" + style 0-0 fg=bright-blue + style 2-34 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| +6| "▌ " + style 0-0 fg=bright-blue +7| "▌ You " + style 0-0 fg=bright-blue + style 2-4 fg=bright-blue bold +8| "▌ Use the workflow tool exactly once, with args omitted, meta set to { \"name\": \"snapshot-flow\", " + style 0-0 fg=bright-blue +9| "▌ \"description\": \"one child for the snapshot\" }, and this EXACT script body (copy it verbatim): " + style 0-0 fg=bright-blue +10| "▌ phase('Run') " + style 0-0 fg=bright-blue +11| "▌ const reply = await agent('Reply with exactly the word WF_CHILD_OK and nothing else.') " + style 0-0 fg=bright-blue +12| "▌ return { reply } " + style 0-0 fg=bright-blue +13| "▌ After the workflow returns, reply with the single word WORKFLOW_DONE and stop. Do not use any " + style 0-0 fg=bright-blue +14| "▌ other tool. " + style 0-0 fg=bright-blue +15| "▌ " + style 0-0 fg=bright-blue +16| +17| " Reasoning " + style 1-9 fg=bright-black italic +18| " The user wants me to use the workflow tool exactly once with specific parameters. Let me carefully " + style 1-99 fg=bright-black italic +19| " follow the instructions: " + style 1-24 fg=bright-black italic +20| " " +21| " 1. args omitted (so I don't include it) " + style 1-3 fg=bright-blue + style 4-39 fg=bright-black italic +22| " 2. meta = { \"name\": \"snapshot-flow\", \"description\": \"one child for the snapshot\" } " + style 1-3 fg=bright-blue + style 4-82 fg=bright-black italic +23| " 3. script = as given verbatim " + style 1-3 fg=bright-blue + style 4-29 fg=bright-black italic +24| " 4. After it returns, reply with \"WORKFLOW_DONE\" " + style 1-3 fg=bright-blue + style 4-47 fg=bright-black italic +25| " " +26| " Let me do exactly that. " + style 1-23 fg=bright-black italic +27| +28| "▌ " + style 0-0 fg=green +29| "▌ ✓ workflow: snapshot-flow " + style 0-0 fg=green + style 2-2 fg=green bold + style 3-26 bold +30| "▌ workflow \"snapshot-flow\" completed (1 agent). " + style 0-0 fg=green +31| "▌ Return value: " + style 0-0 fg=green +32| "▌ { " + style 0-0 fg=green +33| "▌ \"reply\": \"WF_CHILD_OK\" " + style 0-0 fg=green +34| "▌ } " + style 0-0 fg=green +35| "▌ " + style 0-0 fg=green +36| +37| " Reasoning " + style 1-9 fg=bright-black italic +38| " The workflow returned successfully with the reply \"WF_CHILD_OK\". Now I need to reply with exactly " + style 1-99 fg=bright-black italic +39| " \"WORKFLOW_DONE\" and stop. " + style 1-25 fg=bright-black italic +40| +41| " Assistant " + style 1-9 fg=bright-magenta bold +42| " WORKFLOW_DONE " +43| "────────────────────────────────────────────────────────────────────────────────────────────────────" + style 0-99 dim +44| " " + style 1-1 inverse +45| "────────────────────────────────────────────────────────────────────────────────────────────────────" + style 0-99 dim +46| "/workspace/project ↑3.5k ↓22" + style 0-99 dim diff --git a/examples/tui-agent/tests/snapshots/multi-turn-conversation/session.jsonl b/examples/tui-agent/tests/snapshots/multi-turn-conversation/session.jsonl new file mode 100644 index 0000000000..cd32072eaa --- /dev/null +++ b/examples/tui-agent/tests/snapshots/multi-turn-conversation/session.jsonl @@ -0,0 +1,65 @@ +{"type":"session","version":0,"id":"228b7b82-84ed-49b7-a567-981c03b28c77","createdAt":1783352113760,"cwd":"/tmp/acp-snap-cwd-aN2GRR"} +{"type":"turn/start","seq":0,"time":1783352113765,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}} +{"type":"user/message","seq":1,"time":1783352113765,"data":{"content":[{"type":"text","text":"Reply with exactly the word: ONE. No tools."}],"source":{"kind":"user"}},"surfaceOp":"append"} +{"type":"step/start","seq":2,"time":1783352113767,"data":{"turn":1,"step":1}} +{"type":"request/header","seq":3,"time":1783352113768,"data":{"header":{"config":{"provider":"deepseek","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} +{"type":"assistant/chunk","seq":4,"time":1783352114428,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}} +{"type":"assistant/chunk","seq":5,"time":1783352114428,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"The"}}} +{"type":"assistant/chunk","seq":6,"time":1783352114542,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" user"}}} +{"type":"assistant/chunk","seq":7,"time":1783352114570,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" wants"}}} +{"type":"assistant/chunk","seq":8,"time":1783352114571,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" me"}}} +{"type":"assistant/chunk","seq":9,"time":1783352114571,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" to"}}} +{"type":"assistant/chunk","seq":10,"time":1783352114571,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" reply"}}} +{"type":"assistant/chunk","seq":11,"time":1783352114572,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" with"}}} +{"type":"assistant/chunk","seq":12,"time":1783352114600,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" exactly"}}} +{"type":"assistant/chunk","seq":13,"time":1783352114601,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" the"}}} +{"type":"assistant/chunk","seq":14,"time":1783352114602,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" word"}}} +{"type":"assistant/chunk","seq":15,"time":1783352114602,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" \""}}} +{"type":"assistant/chunk","seq":16,"time":1783352114602,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"ONE"}}} +{"type":"assistant/chunk","seq":17,"time":1783352114603,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"\""}}} +{"type":"assistant/chunk","seq":18,"time":1783352114627,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" and"}}} +{"type":"assistant/chunk","seq":19,"time":1783352114628,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" use"}}} +{"type":"assistant/chunk","seq":20,"time":1783352114657,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" no"}}} +{"type":"assistant/chunk","seq":21,"time":1783352114658,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" tools"}}} +{"type":"assistant/chunk","seq":22,"time":1783352114658,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"."}}} +{"type":"assistant/chunk","seq":23,"time":1783352114658,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":1,"blockType":"text"}}} +{"type":"assistant/chunk","seq":24,"time":1783352114658,"data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":1,"text":"ONE"}}} +{"type":"assistant/chunk","seq":25,"time":1783352114687,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to reply with exactly the word \"ONE\" and use no tools."}}}} +{"type":"assistant/chunk","seq":26,"time":1783352114687,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"ONE"}}}} +{"type":"assistant/chunk","seq":27,"time":1783352114687,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":2864,"outputTokens":20,"cacheReadTokens":0,"reasoningTokens":18}}}} +{"type":"assistant/chunk","seq":28,"time":1783352114687,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}} +{"type":"assistant/message","seq":29,"time":1783352114690,"data":{"turn":1,"step":1,"content":[{"type":"reasoning","text":"The user wants me to reply with exactly the word \"ONE\" and use no tools."},{"type":"text","text":"ONE"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":2864,"outputTokens":20,"cacheReadTokens":0,"reasoningTokens":18}},"sourceEventSeqs":[4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28],"surfaceOp":"append"} +{"type":"step/end","seq":30,"time":1783352114690,"data":{"turn":1,"step":1}} +{"type":"turn/end","seq":31,"time":1783352114690,"data":{"turn":1,"reason":{"kind":"completed"}}} +{"type":"turn/start","seq":32,"time":1783352114699,"data":{"turn":2,"trigger":{"kind":"message","source":{"kind":"user"}}}} +{"type":"user/message","seq":33,"time":1783352114699,"data":{"content":[{"type":"text","text":"Reply with exactly the word: TWO. No tools."}],"source":{"kind":"user"}},"surfaceOp":"append"} +{"type":"step/start","seq":34,"time":1783352114700,"data":{"turn":2,"step":1}} +{"type":"assistant/chunk","seq":35,"time":1783352115341,"data":{"turn":2,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}} +{"type":"assistant/chunk","seq":36,"time":1783352115341,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"The"}}} +{"type":"assistant/chunk","seq":37,"time":1783352115465,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" user"}}} +{"type":"assistant/chunk","seq":38,"time":1783352115492,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" wants"}}} +{"type":"assistant/chunk","seq":39,"time":1783352115493,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" me"}}} +{"type":"assistant/chunk","seq":40,"time":1783352115493,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" to"}}} +{"type":"assistant/chunk","seq":41,"time":1783352115493,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" reply"}}} +{"type":"assistant/chunk","seq":42,"time":1783352115521,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" with"}}} +{"type":"assistant/chunk","seq":43,"time":1783352115521,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" exactly"}}} +{"type":"assistant/chunk","seq":44,"time":1783352115521,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" the"}}} +{"type":"assistant/chunk","seq":45,"time":1783352115552,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" word"}}} +{"type":"assistant/chunk","seq":46,"time":1783352115552,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" \""}}} +{"type":"assistant/chunk","seq":47,"time":1783352115552,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"T"}}} +{"type":"assistant/chunk","seq":48,"time":1783352115552,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"WO"}}} +{"type":"assistant/chunk","seq":49,"time":1783352115552,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"\""}}} +{"type":"assistant/chunk","seq":50,"time":1783352115580,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" and"}}} +{"type":"assistant/chunk","seq":51,"time":1783352115580,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" no"}}} +{"type":"assistant/chunk","seq":52,"time":1783352115580,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" tools"}}} +{"type":"assistant/chunk","seq":53,"time":1783352115580,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"."}}} +{"type":"assistant/chunk","seq":54,"time":1783352115609,"data":{"turn":2,"step":1,"chunk":{"type":"block-start","index":1,"blockType":"text"}}} +{"type":"assistant/chunk","seq":55,"time":1783352115609,"data":{"turn":2,"step":1,"chunk":{"type":"text-delta","index":1,"text":"T"}}} +{"type":"assistant/chunk","seq":56,"time":1783352115609,"data":{"turn":2,"step":1,"chunk":{"type":"text-delta","index":1,"text":"WO"}}} +{"type":"assistant/chunk","seq":57,"time":1783352115610,"data":{"turn":2,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to reply with exactly the word \"TWO\" and no tools."}}}} +{"type":"assistant/chunk","seq":58,"time":1783352115610,"data":{"turn":2,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"TWO"}}}} +{"type":"assistant/chunk","seq":59,"time":1783352115610,"data":{"turn":2,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":64,"outputTokens":21,"cacheReadTokens":2816,"reasoningTokens":18}}}} +{"type":"assistant/chunk","seq":60,"time":1783352115610,"data":{"turn":2,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}} +{"type":"assistant/message","seq":61,"time":1783352115611,"data":{"turn":2,"step":1,"content":[{"type":"reasoning","text":"The user wants me to reply with exactly the word \"TWO\" and no tools."},{"type":"text","text":"TWO"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":64,"outputTokens":21,"cacheReadTokens":2816,"reasoningTokens":18}},"sourceEventSeqs":[35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60],"surfaceOp":"append"} +{"type":"step/end","seq":62,"time":1783352115611,"data":{"turn":2,"step":1}} +{"type":"turn/end","seq":63,"time":1783352115611,"data":{"turn":2,"reason":{"kind":"completed"}}} diff --git a/examples/tui-agent/tests/snapshots/multi-turn-conversation/terminal.golden.txt b/examples/tui-agent/tests/snapshots/multi-turn-conversation/terminal.golden.txt new file mode 100644 index 0000000000..fca6c6dd4a --- /dev/null +++ b/examples/tui-agent/tests/snapshots/multi-turn-conversation/terminal.golden.txt @@ -0,0 +1,69 @@ +terminal 100x36 buffer=normal length=36 base=0 viewport=0 +lifecycle started=1 stopped=0 progress=inactive +title "DSH TUI snapshot" +cursor hidden column=1 viewportRow=28 bufferRow=28 +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| "│ Recorded replay: multi-turn-conversation │" + style 0-0 fg=bright-blue + style 2-41 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| +6| "▌ " + style 0-0 fg=bright-blue +7| "▌ You " + style 0-0 fg=bright-blue + style 2-4 fg=bright-blue bold +8| "▌ Reply with exactly the word: ONE. No tools. " + style 0-0 fg=bright-blue +9| "▌ " + style 0-0 fg=bright-blue +10| +11| " Reasoning " + style 1-9 fg=bright-black italic +12| " The user wants me to reply with exactly the word \"ONE\" and use no tools. " + style 1-72 fg=bright-black italic +13| +14| " Assistant " + style 1-9 fg=bright-magenta bold +15| " ONE " +16| +17| "▌ " + style 0-0 fg=bright-blue +18| "▌ You " + style 0-0 fg=bright-blue + style 2-4 fg=bright-blue bold +19| "▌ Reply with exactly the word: TWO. No tools. " + style 0-0 fg=bright-blue +20| "▌ " + style 0-0 fg=bright-blue +21| +22| " Reasoning " + style 1-9 fg=bright-black italic +23| " The user wants me to reply with exactly the word \"TWO\" and no tools. " + style 1-68 fg=bright-black italic +24| +25| " Assistant " + style 1-9 fg=bright-magenta bold +26| " TWO " +27| "────────────────────────────────────────────────────────────────────────────────────────────────────" + style 0-99 dim +28| " " + style 1-1 inverse +29| "────────────────────────────────────────────────────────────────────────────────────────────────────" + style 0-99 dim +30| "/workspace/project ↑2" + style 0-99 dim +31-35| diff --git a/examples/tui-agent/tests/snapshots/parallel-file-reads/session.jsonl b/examples/tui-agent/tests/snapshots/parallel-file-reads/session.jsonl new file mode 100644 index 0000000000..81503dc4cf --- /dev/null +++ b/examples/tui-agent/tests/snapshots/parallel-file-reads/session.jsonl @@ -0,0 +1,28 @@ +{"type":"session","version":0,"id":"{{sessionId}}","createdAt":0,"cwd":"{{cwd}}"} +{"type":"turn/start","seq":0,"time":0,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}} +{"type":"user/message","seq":1,"time":0,"data":{"content":[{"type":"text","text":"Use the read tool twice in the same assistant message: read a.txt and b.txt. Then reply DONE."}],"source":{"kind":"user"}},"surfaceOp":"append"} +{"type":"step/start","seq":2,"time":0,"data":{"turn":1,"step":1}} +{"type":"request/header","seq":3,"time":0,"data":{"header":{"config":{"provider":"deepseek","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} +{"type":"assistant/chunk","seq":4,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}} +{"type":"assistant/chunk","seq":5,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":0,"id":"call_read_a","name":"read","argumentsDelta":"{\"file_path\":\"a.txt\"}"}}} +{"type":"assistant/chunk","seq":6,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_read_a","name":"read","arguments":"{\"file_path\":\"a.txt\"}"}}}} +{"type":"assistant/chunk","seq":7,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}}} +{"type":"assistant/chunk","seq":8,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_read_b","name":"read","argumentsDelta":"{\"file_path\":\"b.txt\"}"}}} +{"type":"assistant/chunk","seq":9,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_read_b","name":"read","arguments":"{\"file_path\":\"b.txt\"}"}}}} +{"type":"assistant/chunk","seq":10,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}} +{"type":"assistant/chunk","seq":11,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} +{"type":"assistant/message","seq":12,"time":0,"data":{"turn":1,"step":1,"content":[{"type":"tool-call","id":"call_read_a","name":"read","arguments":"{\"file_path\":\"a.txt\"}"},{"type":"tool-call","id":"call_read_b","name":"read","arguments":"{\"file_path\":\"b.txt\"}"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[4,5,6,7,8,9,10,11],"surfaceOp":"append"} +{"type":"tool/call","seq":13,"time":0,"data":{"turn":1,"step":1,"callId":"call_read_a","name":"read","arguments":"{\"file_path\":\"a.txt\"}"}} +{"type":"tool/call","seq":14,"time":0,"data":{"turn":1,"step":1,"callId":"call_read_b","name":"read","arguments":"{\"file_path\":\"b.txt\"}"}} +{"type":"tool/result","seq":15,"time":0,"data":{"turn":1,"step":1,"callId":"call_read_a","content":[{"type":"text","text":"{{cwd}}/a.txt\nfile\n\n1: alpha\n\n(End of file - total 1 lines)\n"}],"isError":false},"sourceEventSeqs":[13],"surfaceOp":"append"} +{"type":"tool/result","seq":16,"time":0,"data":{"turn":1,"step":1,"callId":"call_read_b","content":[{"type":"text","text":"{{cwd}}/b.txt\nfile\n\n1: beta\n\n(End of file - total 1 lines)\n"}],"isError":false},"sourceEventSeqs":[14],"surfaceOp":"append"} +{"type":"step/end","seq":17,"time":0,"data":{"turn":1,"step":1}} +{"type":"step/start","seq":18,"time":0,"data":{"turn":1,"step":2}} +{"type":"assistant/chunk","seq":19,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"text"}}} +{"type":"assistant/chunk","seq":20,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":0,"text":"DONE"}}} +{"type":"assistant/chunk","seq":21,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"DONE"}}}} +{"type":"assistant/chunk","seq":22,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":1}}}} +{"type":"assistant/chunk","seq":23,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"stop"}}}} +{"type":"assistant/message","seq":24,"time":0,"data":{"turn":1,"step":2,"content":[{"type":"text","text":"DONE"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":10,"outputTokens":1}},"sourceEventSeqs":[19,20,21,22,23],"surfaceOp":"append"} +{"type":"step/end","seq":25,"time":0,"data":{"turn":1,"step":2}} +{"type":"turn/end","seq":26,"time":0,"data":{"turn":1,"reason":{"kind":"completed"}}} diff --git a/examples/tui-agent/tests/snapshots/parallel-file-reads/terminal.golden.txt b/examples/tui-agent/tests/snapshots/parallel-file-reads/terminal.golden.txt new file mode 100644 index 0000000000..7906694d0a --- /dev/null +++ b/examples/tui-agent/tests/snapshots/parallel-file-reads/terminal.golden.txt @@ -0,0 +1,94 @@ +terminal 100x36 buffer=normal length=41 base=5 viewport=5 +lifecycle started=1 stopped=0 progress=inactive +title "DSH TUI snapshot" +cursor hidden column=1 viewportRow=33 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| "│ Recorded replay: parallel-file-reads │" + style 0-0 fg=bright-blue + style 2-37 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| +6| "▌ " + style 0-0 fg=bright-blue +7| "▌ You " + style 0-0 fg=bright-blue + style 2-4 fg=bright-blue bold +8| "▌ Use the read tool twice in the same assistant message: read a.txt and b.txt. Then reply DONE. " + style 0-0 fg=bright-blue +9| "▌ " + style 0-0 fg=bright-blue +10| +11| "▌ " + style 0-0 fg=green +12| "▌ ✓ Read a.txt " + style 0-0 fg=green + style 2-2 fg=green bold + style 3-13 bold +13| "▌ /workspace/project" + style 0-0 fg=green +14| "▌ /a.txt " + style 0-0 fg=green +15| "▌ file " + style 0-0 fg=green +16| "▌ " + style 0-0 fg=green +17| "▌ 1: alpha " + style 0-0 fg=green +18| "▌ " + style 0-0 fg=green +19| "▌ (End of file - total 1 lines) " + style 0-0 fg=green +20| "▌ " + style 0-0 fg=green +21| "▌ " + style 0-0 fg=green +22| +23| "▌ " + style 0-0 fg=green +24| "▌ ✓ Read b.txt " + style 0-0 fg=green + style 2-2 fg=green bold + style 3-13 bold +25| "▌ /workspace/project" + style 0-0 fg=green +26| "▌ /b.txt " + style 0-0 fg=green +27| "▌ file " + style 0-0 fg=green +28| "▌ " + style 0-0 fg=green +29| "▌ 1: beta " + style 0-0 fg=green +30| "▌ " + style 0-0 fg=green +31| "▌ (End of file - total 1 lines) " + style 0-0 fg=green +32| "▌ " + style 0-0 fg=green +33| "▌ " + style 0-0 fg=green +34| +35| " Assistant " + style 1-9 fg=bright-magenta bold +36| " DONE " +37| "────────────────────────────────────────────────────────────────────────────────────────────────────" + style 0-99 dim +38| " " + style 1-1 inverse +39| "────────────────────────────────────────────────────────────────────────────────────────────────────" + style 0-99 dim +40| "/workspace/project ↑20 ↓6" + style 0-99 dim diff --git a/examples/tui-agent/tests/snapshots/parallel-file-reads/workspace/a.txt b/examples/tui-agent/tests/snapshots/parallel-file-reads/workspace/a.txt new file mode 100644 index 0000000000..4a58007052 --- /dev/null +++ b/examples/tui-agent/tests/snapshots/parallel-file-reads/workspace/a.txt @@ -0,0 +1 @@ +alpha diff --git a/examples/tui-agent/tests/snapshots/parallel-file-reads/workspace/b.txt b/examples/tui-agent/tests/snapshots/parallel-file-reads/workspace/b.txt new file mode 100644 index 0000000000..65b2df87f7 --- /dev/null +++ b/examples/tui-agent/tests/snapshots/parallel-file-reads/workspace/b.txt @@ -0,0 +1 @@ +beta diff --git a/examples/tui-agent/tests/snapshots/todo-plan/session.jsonl b/examples/tui-agent/tests/snapshots/todo-plan/session.jsonl new file mode 100644 index 0000000000..909afc44cd --- /dev/null +++ b/examples/tui-agent/tests/snapshots/todo-plan/session.jsonl @@ -0,0 +1,134 @@ +{"type":"session","version":0,"id":"b0f1f758-dcf0-474e-851d-e62c11ec0a09","createdAt":1783352057652,"cwd":"/tmp/acp-snap-cwd-AYilT7"} +{"type":"turn/start","seq":0,"time":1783352057655,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}} +{"type":"user/message","seq":1,"time":1783352057655,"data":{"content":[{"type":"text","text":"Use the todo_write tool to record a plan with exactly three todos: \"read the code\" (in_progress), \"write the fix\" (pending), \"run the tests\" (pending). Send all three in one todo_write call. Then reply with the single word DONE and stop."}],"source":{"kind":"user"}},"surfaceOp":"append"} +{"type":"step/start","seq":2,"time":1783352057657,"data":{"turn":1,"step":1}} +{"type":"request/header","seq":3,"time":1783352057657,"data":{"header":{"config":{"provider":"deepseek","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} +{"type":"assistant/chunk","seq":4,"time":1783352058320,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}} +{"type":"assistant/chunk","seq":5,"time":1783352058320,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"The"}}} +{"type":"assistant/chunk","seq":6,"time":1783352058426,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" user"}}} +{"type":"assistant/chunk","seq":7,"time":1783352058466,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" wants"}}} +{"type":"assistant/chunk","seq":8,"time":1783352058467,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" me"}}} +{"type":"assistant/chunk","seq":9,"time":1783352058467,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" to"}}} +{"type":"assistant/chunk","seq":10,"time":1783352058467,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" use"}}} +{"type":"assistant/chunk","seq":11,"time":1783352058467,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" the"}}} +{"type":"assistant/chunk","seq":12,"time":1783352058484,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" todo"}}} +{"type":"assistant/chunk","seq":13,"time":1783352058484,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"_write"}}} +{"type":"assistant/chunk","seq":14,"time":1783352058484,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" tool"}}} +{"type":"assistant/chunk","seq":15,"time":1783352058484,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" to"}}} +{"type":"assistant/chunk","seq":16,"time":1783352058485,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" record"}}} +{"type":"assistant/chunk","seq":17,"time":1783352058511,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" a"}}} +{"type":"assistant/chunk","seq":18,"time":1783352058512,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" plan"}}} +{"type":"assistant/chunk","seq":19,"time":1783352058513,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" with"}}} +{"type":"assistant/chunk","seq":20,"time":1783352058513,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" exactly"}}} +{"type":"assistant/chunk","seq":21,"time":1783352058513,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" three"}}} +{"type":"assistant/chunk","seq":22,"time":1783352058514,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" todos"}}} +{"type":"assistant/chunk","seq":23,"time":1783352058540,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" in"}}} +{"type":"assistant/chunk","seq":24,"time":1783352058540,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" the"}}} +{"type":"assistant/chunk","seq":25,"time":1783352058571,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" specified"}}} +{"type":"assistant/chunk","seq":26,"time":1783352058572,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" status"}}} +{"type":"assistant/chunk","seq":27,"time":1783352058597,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"es"}}} +{"type":"assistant/chunk","seq":28,"time":1783352058597,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":","}}} +{"type":"assistant/chunk","seq":29,"time":1783352058597,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" then"}}} +{"type":"assistant/chunk","seq":30,"time":1783352058597,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" reply"}}} +{"type":"assistant/chunk","seq":31,"time":1783352058626,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" with"}}} +{"type":"assistant/chunk","seq":32,"time":1783352058626,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" \""}}} +{"type":"assistant/chunk","seq":33,"time":1783352058626,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"D"}}} +{"type":"assistant/chunk","seq":34,"time":1783352058626,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"ONE"}}} +{"type":"assistant/chunk","seq":35,"time":1783352058626,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"\"."}}} +{"type":"assistant/chunk","seq":36,"time":1783352058717,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}}} +{"type":"assistant/chunk","seq":37,"time":1783352058717,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_fjAnBThbDjxepBtp3hDt3264","name":"todo_write","argumentsDelta":""}}} +{"type":"assistant/chunk","seq":38,"time":1783352058746,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_fjAnBThbDjxepBtp3hDt3264","name":"todo_write","argumentsDelta":"{"}}} +{"type":"assistant/chunk","seq":39,"time":1783352058747,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_fjAnBThbDjxepBtp3hDt3264","name":"todo_write","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":40,"time":1783352058747,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_fjAnBThbDjxepBtp3hDt3264","name":"todo_write","argumentsDelta":"t"}}} +{"type":"assistant/chunk","seq":41,"time":1783352058747,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_fjAnBThbDjxepBtp3hDt3264","name":"todo_write","argumentsDelta":"odos"}}} +{"type":"assistant/chunk","seq":42,"time":1783352058775,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_fjAnBThbDjxepBtp3hDt3264","name":"todo_write","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":43,"time":1783352058775,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_fjAnBThbDjxepBtp3hDt3264","name":"todo_write","argumentsDelta":": "}}} +{"type":"assistant/chunk","seq":44,"time":1783352058776,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_fjAnBThbDjxepBtp3hDt3264","name":"todo_write","argumentsDelta":"["}}} +{"type":"assistant/chunk","seq":45,"time":1783352058805,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_fjAnBThbDjxepBtp3hDt3264","name":"todo_write","argumentsDelta":"{\""}}} +{"type":"assistant/chunk","seq":46,"time":1783352058806,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_fjAnBThbDjxepBtp3hDt3264","name":"todo_write","argumentsDelta":"content"}}} +{"type":"assistant/chunk","seq":47,"time":1783352058806,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_fjAnBThbDjxepBtp3hDt3264","name":"todo_write","argumentsDelta":"\":"}}} +{"type":"assistant/chunk","seq":48,"time":1783352058806,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_fjAnBThbDjxepBtp3hDt3264","name":"todo_write","argumentsDelta":" \""}}} +{"type":"assistant/chunk","seq":49,"time":1783352058806,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_fjAnBThbDjxepBtp3hDt3264","name":"todo_write","argumentsDelta":"read"}}} +{"type":"assistant/chunk","seq":50,"time":1783352058806,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_fjAnBThbDjxepBtp3hDt3264","name":"todo_write","argumentsDelta":" the"}}} +{"type":"assistant/chunk","seq":51,"time":1783352058832,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_fjAnBThbDjxepBtp3hDt3264","name":"todo_write","argumentsDelta":" code"}}} +{"type":"assistant/chunk","seq":52,"time":1783352058832,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_fjAnBThbDjxepBtp3hDt3264","name":"todo_write","argumentsDelta":"\","}}} +{"type":"assistant/chunk","seq":53,"time":1783352058832,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_fjAnBThbDjxepBtp3hDt3264","name":"todo_write","argumentsDelta":" \""}}} +{"type":"assistant/chunk","seq":54,"time":1783352058832,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_fjAnBThbDjxepBtp3hDt3264","name":"todo_write","argumentsDelta":"status"}}} +{"type":"assistant/chunk","seq":55,"time":1783352058832,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_fjAnBThbDjxepBtp3hDt3264","name":"todo_write","argumentsDelta":"\":"}}} +{"type":"assistant/chunk","seq":56,"time":1783352058832,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_fjAnBThbDjxepBtp3hDt3264","name":"todo_write","argumentsDelta":" \""}}} +{"type":"assistant/chunk","seq":57,"time":1783352058862,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_fjAnBThbDjxepBtp3hDt3264","name":"todo_write","argumentsDelta":"in"}}} +{"type":"assistant/chunk","seq":58,"time":1783352058863,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_fjAnBThbDjxepBtp3hDt3264","name":"todo_write","argumentsDelta":"_pro"}}} +{"type":"assistant/chunk","seq":59,"time":1783352058863,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_fjAnBThbDjxepBtp3hDt3264","name":"todo_write","argumentsDelta":"gress"}}} +{"type":"assistant/chunk","seq":60,"time":1783352058863,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_fjAnBThbDjxepBtp3hDt3264","name":"todo_write","argumentsDelta":"\"},"}}} +{"type":"assistant/chunk","seq":61,"time":1783352058863,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_fjAnBThbDjxepBtp3hDt3264","name":"todo_write","argumentsDelta":" {\""}}} +{"type":"assistant/chunk","seq":62,"time":1783352058863,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_fjAnBThbDjxepBtp3hDt3264","name":"todo_write","argumentsDelta":"content"}}} +{"type":"assistant/chunk","seq":63,"time":1783352058889,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_fjAnBThbDjxepBtp3hDt3264","name":"todo_write","argumentsDelta":"\":"}}} +{"type":"assistant/chunk","seq":64,"time":1783352058890,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_fjAnBThbDjxepBtp3hDt3264","name":"todo_write","argumentsDelta":" \""}}} +{"type":"assistant/chunk","seq":65,"time":1783352058890,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_fjAnBThbDjxepBtp3hDt3264","name":"todo_write","argumentsDelta":"write"}}} +{"type":"assistant/chunk","seq":66,"time":1783352058890,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_fjAnBThbDjxepBtp3hDt3264","name":"todo_write","argumentsDelta":" the"}}} +{"type":"assistant/chunk","seq":67,"time":1783352058890,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_fjAnBThbDjxepBtp3hDt3264","name":"todo_write","argumentsDelta":" fix"}}} +{"type":"assistant/chunk","seq":68,"time":1783352058890,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_fjAnBThbDjxepBtp3hDt3264","name":"todo_write","argumentsDelta":"\","}}} +{"type":"assistant/chunk","seq":69,"time":1783352058918,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_fjAnBThbDjxepBtp3hDt3264","name":"todo_write","argumentsDelta":" \""}}} +{"type":"assistant/chunk","seq":70,"time":1783352058918,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_fjAnBThbDjxepBtp3hDt3264","name":"todo_write","argumentsDelta":"status"}}} +{"type":"assistant/chunk","seq":71,"time":1783352058918,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_fjAnBThbDjxepBtp3hDt3264","name":"todo_write","argumentsDelta":"\":"}}} +{"type":"assistant/chunk","seq":72,"time":1783352058918,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_fjAnBThbDjxepBtp3hDt3264","name":"todo_write","argumentsDelta":" \""}}} +{"type":"assistant/chunk","seq":73,"time":1783352058918,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_fjAnBThbDjxepBtp3hDt3264","name":"todo_write","argumentsDelta":"pending"}}} +{"type":"assistant/chunk","seq":74,"time":1783352058918,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_fjAnBThbDjxepBtp3hDt3264","name":"todo_write","argumentsDelta":"\"},"}}} +{"type":"assistant/chunk","seq":75,"time":1783352058947,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_fjAnBThbDjxepBtp3hDt3264","name":"todo_write","argumentsDelta":" {\""}}} +{"type":"assistant/chunk","seq":76,"time":1783352058947,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_fjAnBThbDjxepBtp3hDt3264","name":"todo_write","argumentsDelta":"content"}}} +{"type":"assistant/chunk","seq":77,"time":1783352058947,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_fjAnBThbDjxepBtp3hDt3264","name":"todo_write","argumentsDelta":"\":"}}} +{"type":"assistant/chunk","seq":78,"time":1783352058947,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_fjAnBThbDjxepBtp3hDt3264","name":"todo_write","argumentsDelta":" \""}}} +{"type":"assistant/chunk","seq":79,"time":1783352058947,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_fjAnBThbDjxepBtp3hDt3264","name":"todo_write","argumentsDelta":"run"}}} +{"type":"assistant/chunk","seq":80,"time":1783352058948,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_fjAnBThbDjxepBtp3hDt3264","name":"todo_write","argumentsDelta":" the"}}} +{"type":"assistant/chunk","seq":81,"time":1783352058976,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_fjAnBThbDjxepBtp3hDt3264","name":"todo_write","argumentsDelta":" tests"}}} +{"type":"assistant/chunk","seq":82,"time":1783352058976,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_fjAnBThbDjxepBtp3hDt3264","name":"todo_write","argumentsDelta":"\","}}} +{"type":"assistant/chunk","seq":83,"time":1783352058976,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_fjAnBThbDjxepBtp3hDt3264","name":"todo_write","argumentsDelta":" \""}}} +{"type":"assistant/chunk","seq":84,"time":1783352058976,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_fjAnBThbDjxepBtp3hDt3264","name":"todo_write","argumentsDelta":"status"}}} +{"type":"assistant/chunk","seq":85,"time":1783352058977,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_fjAnBThbDjxepBtp3hDt3264","name":"todo_write","argumentsDelta":"\":"}}} +{"type":"assistant/chunk","seq":86,"time":1783352058977,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_fjAnBThbDjxepBtp3hDt3264","name":"todo_write","argumentsDelta":" \""}}} +{"type":"assistant/chunk","seq":87,"time":1783352059004,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_fjAnBThbDjxepBtp3hDt3264","name":"todo_write","argumentsDelta":"pending"}}} +{"type":"assistant/chunk","seq":88,"time":1783352059005,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_fjAnBThbDjxepBtp3hDt3264","name":"todo_write","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":89,"time":1783352059005,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_fjAnBThbDjxepBtp3hDt3264","name":"todo_write","argumentsDelta":"}]"}}} +{"type":"assistant/chunk","seq":90,"time":1783352059033,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_fjAnBThbDjxepBtp3hDt3264","name":"todo_write","argumentsDelta":"}"}}} +{"type":"assistant/chunk","seq":91,"time":1783352059095,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to use the todo_write tool to record a plan with exactly three todos in the specified statuses, then reply with \"DONE\"."}}}} +{"type":"assistant/chunk","seq":92,"time":1783352059096,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_00_fjAnBThbDjxepBtp3hDt3264","name":"todo_write","arguments":"{\"todos\": [{\"content\": \"read the code\", \"status\": \"in_progress\"}, {\"content\": \"write the fix\", \"status\": \"pending\"}, {\"content\": \"run the tests\", \"status\": \"pending\"}]}"}}}} +{"type":"assistant/chunk","seq":93,"time":1783352059096,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":2913,"outputTokens":121,"cacheReadTokens":0,"reasoningTokens":31}}}} +{"type":"assistant/chunk","seq":94,"time":1783352059096,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} +{"type":"assistant/message","seq":95,"time":1783352059099,"data":{"turn":1,"step":1,"content":[{"type":"reasoning","text":"The user wants me to use the todo_write tool to record a plan with exactly three todos in the specified statuses, then reply with \"DONE\"."},{"type":"tool-call","id":"call_00_fjAnBThbDjxepBtp3hDt3264","name":"todo_write","arguments":"{\"todos\": [{\"content\": \"read the code\", \"status\": \"in_progress\"}, {\"content\": \"write the fix\", \"status\": \"pending\"}, {\"content\": \"run the tests\", \"status\": \"pending\"}]}"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":2913,"outputTokens":121,"cacheReadTokens":0,"reasoningTokens":31}},"sourceEventSeqs":[4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94],"surfaceOp":"append"} +{"type":"tool/call","seq":96,"time":1783352059099,"data":{"turn":1,"step":1,"callId":"call_00_fjAnBThbDjxepBtp3hDt3264","name":"todo_write","arguments":"{\"todos\": [{\"content\": \"read the code\", \"status\": \"in_progress\"}, {\"content\": \"write the fix\", \"status\": \"pending\"}, {\"content\": \"run the tests\", \"status\": \"pending\"}]}"}} +{"type":"todo/write","seq":97,"time":1783352059100,"data":{"todos":[{"content":"read the code","status":"in_progress"},{"content":"write the fix","status":"pending"},{"content":"run the tests","status":"pending"}]}} +{"type":"tool/result","seq":98,"time":1783352059101,"data":{"turn":1,"step":1,"callId":"call_00_fjAnBThbDjxepBtp3hDt3264","content":[{"type":"text","text":"Updated todo list: 2 pending, 1 in progress, 0 completed."}],"isError":false},"sourceEventSeqs":[96],"surfaceOp":"append"} +{"type":"step/end","seq":99,"time":1783352059101,"data":{"turn":1,"step":1}} +{"type":"step/start","seq":100,"time":1783352059102,"data":{"turn":1,"step":2}} +{"type":"assistant/chunk","seq":101,"time":1783352059732,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}} +{"type":"assistant/chunk","seq":102,"time":1783352059733,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"The"}}} +{"type":"assistant/chunk","seq":103,"time":1783352059835,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" todos"}}} +{"type":"assistant/chunk","seq":104,"time":1783352059863,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" have"}}} +{"type":"assistant/chunk","seq":105,"time":1783352059863,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" been"}}} +{"type":"assistant/chunk","seq":106,"time":1783352059864,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" written"}}} +{"type":"assistant/chunk","seq":107,"time":1783352059864,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" successfully"}}} +{"type":"assistant/chunk","seq":108,"time":1783352059892,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"."}}} +{"type":"assistant/chunk","seq":109,"time":1783352059892,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" Now"}}} +{"type":"assistant/chunk","seq":110,"time":1783352059893,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" I"}}} +{"type":"assistant/chunk","seq":111,"time":1783352059893,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" just"}}} +{"type":"assistant/chunk","seq":112,"time":1783352059920,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" need"}}} +{"type":"assistant/chunk","seq":113,"time":1783352059920,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" to"}}} +{"type":"assistant/chunk","seq":114,"time":1783352059921,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" reply"}}} +{"type":"assistant/chunk","seq":115,"time":1783352059921,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" with"}}} +{"type":"assistant/chunk","seq":116,"time":1783352059921,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" the"}}} +{"type":"assistant/chunk","seq":117,"time":1783352059950,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" single"}}} +{"type":"assistant/chunk","seq":118,"time":1783352059950,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" word"}}} +{"type":"assistant/chunk","seq":119,"time":1783352059950,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" \""}}} +{"type":"assistant/chunk","seq":120,"time":1783352059950,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"D"}}} +{"type":"assistant/chunk","seq":121,"time":1783352059951,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"ONE"}}} +{"type":"assistant/chunk","seq":122,"time":1783352059951,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"\"."}}} +{"type":"assistant/chunk","seq":123,"time":1783352059979,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":1,"blockType":"text"}}} +{"type":"assistant/chunk","seq":124,"time":1783352059979,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":"D"}}} +{"type":"assistant/chunk","seq":125,"time":1783352059980,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":"ONE"}}} +{"type":"assistant/chunk","seq":126,"time":1783352059980,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The todos have been written successfully. Now I just need to reply with the single word \"DONE\"."}}}} +{"type":"assistant/chunk","seq":127,"time":1783352059980,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"DONE"}}}} +{"type":"assistant/chunk","seq":128,"time":1783352059980,"data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":237,"outputTokens":24,"cacheReadTokens":2816,"reasoningTokens":21}}}} +{"type":"assistant/chunk","seq":129,"time":1783352059980,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"stop"}}}} +{"type":"assistant/message","seq":130,"time":1783352059981,"data":{"turn":1,"step":2,"content":[{"type":"reasoning","text":"The todos have been written successfully. Now I just need to reply with the single word \"DONE\"."},{"type":"text","text":"DONE"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":237,"outputTokens":24,"cacheReadTokens":2816,"reasoningTokens":21}},"sourceEventSeqs":[101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129],"surfaceOp":"append"} +{"type":"step/end","seq":131,"time":1783352059981,"data":{"turn":1,"step":2}} +{"type":"turn/end","seq":132,"time":1783352059981,"data":{"turn":1,"reason":{"kind":"completed"}}} diff --git a/examples/tui-agent/tests/snapshots/todo-plan/terminal.golden.txt b/examples/tui-agent/tests/snapshots/todo-plan/terminal.golden.txt new file mode 100644 index 0000000000..2d6520f067 --- /dev/null +++ b/examples/tui-agent/tests/snapshots/todo-plan/terminal.golden.txt @@ -0,0 +1,81 @@ +terminal 100x36 buffer=normal length=36 base=0 viewport=0 +lifecycle started=1 stopped=0 progress=inactive +title "DSH TUI snapshot" +cursor hidden column=1 viewportRow=33 bufferRow=33 +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| "│ Recorded replay: todo-plan │" + style 0-0 fg=bright-blue + style 2-27 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| +6| "▌ " + style 0-0 fg=bright-blue +7| "▌ You " + style 0-0 fg=bright-blue + style 2-4 fg=bright-blue bold +8| "▌ Use the todo_write tool to record a plan with exactly three todos: \"read the code\" (in_progress), " + style 0-0 fg=bright-blue +9| "▌ \"write the fix\" (pending), \"run the tests\" (pending). Send all three in one todo_write call. Then " + style 0-0 fg=bright-blue +10| "▌ reply with the single word DONE and stop. " + style 0-0 fg=bright-blue +11| "▌ " + style 0-0 fg=bright-blue +12| +13| " Reasoning " + style 1-9 fg=bright-black italic +14| " The user wants me to use the todo_write tool to record a plan with exactly three todos in the " + style 1-99 fg=bright-black italic +15| " specified statuses, then reply with \"DONE\". " + style 1-43 fg=bright-black italic +16| +17| "▌ " + style 0-0 fg=green +18| "▌ ✓ Update todo list " + style 0-0 fg=green + style 2-2 fg=green bold + style 3-19 bold +19| "▌ Updated todo list: 2 pending, 1 in progress, 0 completed. " + style 0-0 fg=green +20| "▌ " + style 0-0 fg=green +21| +22| " Reasoning " + style 1-9 fg=bright-black italic +23| " The todos have been written successfully. Now I just need to reply with the single word \"DONE\". " + style 1-95 fg=bright-black italic +24| +25| " Assistant " + style 1-9 fg=bright-magenta bold +26| " DONE " +27| +28| "Plan" + style 0-3 fg=bright-blue bold +29| " ● read the code" + style 2-2 fg=yellow +30| " ○ write the fix" + style 2-2 dim +31| " ○ run the tests" + style 2-2 dim +32| "────────────────────────────────────────────────────────────────────────────────────────────────────" + style 0-99 dim +33| " " + style 1-1 inverse +34| "────────────────────────────────────────────────────────────────────────────────────────────────────" + style 0-99 dim +35| "/workspace/project ↑3.1k ↓145 idle" + style 0-93 dim + style 96-99 dim diff --git a/examples/tui-agent/tests/tui.snapshot.ts b/examples/tui-agent/tests/tui.snapshot.ts new file mode 100644 index 0000000000..453c587c2a --- /dev/null +++ b/examples/tui-agent/tests/tui.snapshot.ts @@ -0,0 +1,329 @@ +import { cp, mkdir, mkdtemp, readFile, readdir, rm, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { basename, dirname, join } from 'node:path' +import { fileURLToPath } from 'node:url' +import { afterAll, describe, expect, it } from 'vitest' +import { Context } from 'cordis' +import { scrubRequestHeaders } from '@deepseek-ai/dsh-acp-snapshot' +import { AgentId, type Agent } from '@deepseek-ai/dsh-agent' +import * as AgentCore from '@deepseek-ai/dsh-agent-spine-demo' +import { LocalBashExecutor } from '@deepseek-ai/dsh-bash-local' +import WorkerCodeRuntime from '@deepseek-ai/dsh-code-runtime-worker' +import LocalFileSystem from '@deepseek-ai/dsh-fs-local' +import * as FsPolicy from '@deepseek-ai/dsh-fs-policy' +import * as ToolFs from '@deepseek-ai/dsh-tool-fs' +import * as LlmDeepSeek from '@deepseek-ai/dsh-llm-deepseek' +import { installLlmReplay, parseSessionLog } from '@deepseek-ai/dsh-llm-replay' +import type { Session, SessionEvent } from '@deepseek-ai/dsh-session' +import { SessionId } from '@deepseek-ai/dsh-session' +import SubagentService from '@deepseek-ai/dsh-subagent' +import * as SubagentSpawn from '@deepseek-ai/dsh-subagent-spawn' +import * as ToolSubagent from '@deepseek-ai/dsh-tool-subagent' +import * as ToolCordis from '@deepseek-ai/dsh-tool-cordis' +import * as ToolTodo from '@deepseek-ai/dsh-tool-todo' +import * as ToolWorkflow from '@deepseek-ai/dsh-tool-workflow' +import { createTuiChat } from '@deepseek-ai/dsh-tui' +import UserInteractionService from '@deepseek-ai/dsh-user-interaction' +import WorkerWorkflowEngine from '@deepseek-ai/dsh-workflow-workerthread' +import { HeadlessTerminal } from '../../../packages/ui/tui/tests/headless-terminal.ts' + +const SNAPSHOTS_DIR = join(dirname(fileURLToPath(import.meta.url)), 'snapshots') +const PROVIDERS = [{ id: 'deepseek', models: [{ id: 'deepseek-v4-flash' }] }] +const UUID_RE = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi + +type SnapshotMode = 'replay' | 'record' | 'refresh' +type Composition = 'native' | 'code' | 'advanced' + +interface Scenario { + name: string + composition: Composition + expectedTools: string[] + expectedEventCounts?: Record + childSessions?: number + recorded: boolean + seedWorkspace?: boolean +} + +const SCENARIOS: Scenario[] = [ + { + name: 'multi-turn-conversation', + composition: 'native', + expectedTools: [], + recorded: true, + }, + { + name: 'todo-plan', + composition: 'native', + expectedTools: ['todo_write'], + expectedEventCounts: { 'todo/write': 1 }, + recorded: true, + }, + { + name: 'bash-terminal-card', + composition: 'native', + expectedTools: ['bash'], + recorded: true, + }, + { + name: 'parallel-file-reads', + composition: 'native', + expectedTools: ['read', 'read'], + recorded: true, + seedWorkspace: true, + }, + { + name: 'code-mode', + composition: 'code', + expectedTools: ['run_code'], + expectedEventCounts: { 'tool/code-dispatch': 2 }, + recorded: true, + }, + { + name: 'dynamic-workflow', + composition: 'native', + expectedTools: ['workflow'], + childSessions: 1, + recorded: true, + }, + { + name: 'cordis-dynamic-toolchain', + composition: 'advanced', + expectedTools: ['cordis_mount', 'run_code', 'subagent', 'workflow', 'cordis_unmount'], + expectedEventCounts: { 'tool/code-dispatch': 1 }, + childSessions: 2, + recorded: false, + }, +] + +function snapshotModeFromEnv(value: string | undefined): SnapshotMode { + if (value === undefined || value === '' || value === 'replay') return 'replay' + if (value === 'record' || value === 'refresh') return value + throw new Error(`DSH_SNAPSHOT must be replay, record, or refresh; got ${JSON.stringify(value)}`) +} + +const MODE = snapshotModeFromEnv(process.env.DSH_SNAPSHOT) +const observedScenarios = new Set() + +function scenarioDir(scenario: Scenario): string { + return join(SNAPSHOTS_DIR, scenario.name) +} + +function childFixturePaths(scenario: Scenario): string[] { + return Array.from( + { length: scenario.childSessions ?? 0 }, + (_, index) => join(scenarioDir(scenario), `session.${index + 1}.jsonl`), + ) +} + +function userPrompts(rawLog: string): string[] { + return parseSessionLog(rawLog).flatMap((event) => { + if (event.type !== 'user/message' || event.data.source.kind !== 'user') return [] + const text = event.data.content + .filter(block => block.type === 'text') + .map(block => block.text) + .join('') + return text.length > 0 ? [text] : [] + }) +} + +function rawSessionLog(session: Session): string { + return [ + JSON.stringify({ type: 'session', ...session.header }), + ...session.events.map(event => JSON.stringify(event)), + '', + ].join('\n') +} + +function normalizeTerminalSnapshot(snapshot: string, cwd: string): string { + return snapshot + .split(`/private${cwd}`).join('/workspace/project') + .split(cwd).join('/workspace/project') + .replace(UUID_RE, '{{uuid}}') +} + +async function settleTerminal(terminal: HeadlessTerminal): Promise { + let stable = 0 + for (let attempt = 0; attempt < 20 && stable < 3; attempt++) { + const before = terminal.frames + await new Promise(resolve => setTimeout(resolve, 10)) + await terminal.flush() + stable = terminal.frames === before ? stable + 1 : 0 + } + if (stable < 3) throw new Error('TUI frames did not quiesce within 200ms') +} + +async function mountScenarioContext( + scenario: Scenario, + cwd: string, + fixtureFile: string, + childFiles: string[], +): Promise { + const ctx = new Context() + await ctx.plugin(AgentCore, { + agents: [], + dshHome: join(cwd, '.dsh'), + workspaceContext: false, + tools: { mode: scenario.composition === 'code' ? 'code' : scenario.composition === 'advanced' ? 'both' : 'native' }, + skills: { local: { agentsHome: join(cwd, '.agents') } }, + }) + await ctx.plugin(LocalBashExecutor, { cwd, timeoutMs: 30_000 }) + await ctx.plugin(LocalFileSystem, { cwd: '/' }) + await ctx.plugin(FsPolicy) + await ctx.plugin(ToolFs) + await ctx.plugin(UserInteractionService) + await ctx.plugin(ToolTodo) + await ctx.plugin(SubagentService) + await ctx.plugin(SubagentSpawn, { providerName: 'spawn' }) + await ctx.plugin(ToolSubagent, { provider: 'spawn', toolName: 'subagent', enableRunInBackground: false }) + await ctx.plugin(WorkerWorkflowEngine, { provider: 'spawn' }) + await ctx.plugin(ToolWorkflow) + if (scenario.composition === 'code' || scenario.composition === 'advanced') { + await ctx.plugin(WorkerCodeRuntime, {}) + } + if (scenario.composition === 'advanced') await ctx.plugin(ToolCordis, { vmTimeoutMs: 5_000 }) + if (MODE === 'record' && scenario.recorded) { + await ctx.plugin(LlmDeepSeek) + } else { + installLlmReplay(ctx, { file: fixtureFile, childFiles, providers: PROVIDERS }) + } + return ctx +} + +interface ScenarioResult { + terminal: string + parent: Session + children: Session[] + workflowEvents: string[] +} + +async function runScenario(scenario: Scenario): Promise { + const dir = scenarioDir(scenario) + const fixtureFile = join(dir, 'session.jsonl') + const childFiles = childFixturePaths(scenario) + const fixture = await readFile(fixtureFile, 'utf8') + const prompts = userPrompts(fixture) + expect(prompts.length, `${scenario.name} must carry at least one recorded user prompt`).toBeGreaterThan(0) + + const cwd = await mkdtemp(join(tmpdir(), `dsh-tui-snapshot-${scenario.name}-`)) + let ctx: Context | undefined + let controller: ReturnType | undefined + const terminal = new HeadlessTerminal(100, 36) + try { + if (scenario.seedWorkspace === true) { + const source = join(scenarioDir(scenario), 'workspace') + await cp(source, cwd, { recursive: true }) + } + ctx = await mountScenarioContext(scenario, cwd, fixtureFile, childFiles) + const disposedSessions: Session[] = [] + ctx.on('session/disposed', (session) => { disposedSessions.push(session) }) + const workflowEvents: string[] = [] + for (const name of ['workflow/start', 'workflow/phase', 'workflow/agent-start', 'workflow/agent-end', 'workflow/end'] as const) { + ctx.on(name, () => { workflowEvents.push(name) }) + } + const handle = await ctx.agents.create({ + agentId: AgentId('main'), + sessionId: SessionId('main-session'), + meta: { cwd }, + agentOptions: { provider: 'deepseek', model: 'deepseek-v4-flash' }, + }) + const agent: Agent = handle.agent + controller = createTuiChat(ctx, { + agent: 'main', + color: true, + showReasoning: true, + title: 'DSH TUI snapshot', + welcome: `Recorded replay: ${scenario.name}`, + maxToolOutputLines: 8, + }, { terminal, exit: () => {} }) + await settleTerminal(terminal) + + for (const prompt of prompts) { + terminal.send(prompt) + terminal.send('\r') + await agent.whenIdle() + await settleTerminal(terminal) + } + + const events: SessionEvent[] = [...agent.session.events] + expect(events.filter(event => event.type === 'tool/call').map(event => event.data.name)).toEqual(scenario.expectedTools) + for (const [type, count] of Object.entries(scenario.expectedEventCounts ?? {})) { + expect(events.filter(event => event.type === type), `${scenario.name} must emit ${type}`).toHaveLength(count) + } + expect(events.filter(event => event.type === 'tool/result').every(event => !event.data.isError)).toBe(true) + expect(events.filter(event => event.type === 'turn/end').every(event => event.data.reason.kind !== 'error')).toBe(true) + if (scenario.name === 'dynamic-workflow' || scenario.name === 'cordis-dynamic-toolchain') { + expect(workflowEvents).toEqual([ + 'workflow/start', + 'workflow/phase', + 'workflow/agent-start', + 'workflow/agent-end', + 'workflow/end', + ]) + } + + expect(terminal.themeViolations(), `${scenario.name} must remain theme-agnostic`).toEqual([]) + const snapshot = normalizeTerminalSnapshot( + await terminal.snapshot({ includeScrollback: true }), + cwd, + ) + await handle.dispose() + const children = disposedSessions + .filter(session => session !== agent.session) + .sort((a, b) => a.header.createdAt - b.header.createdAt) + expect(children).toHaveLength(scenario.childSessions ?? 0) + return { terminal: snapshot, parent: agent.session, children, workflowEvents } + } finally { + await controller?.dispose() + await ctx?.fiber.dispose() + await terminal.dispose() + await rm(cwd, { recursive: true, force: true }) + } +} + +async function writeRecording(scenario: Scenario, result: ScenarioResult): Promise { + const dir = scenarioDir(scenario) + await mkdir(dir, { recursive: true }) + await writeFile(join(dir, 'session.jsonl'), scrubRequestHeaders(rawSessionLog(result.parent))) + expect(result.children).toHaveLength(scenario.childSessions ?? 0) + for (const [index, child] of result.children.entries()) { + await writeFile(join(dir, `session.${index + 1}.jsonl`), scrubRequestHeaders(rawSessionLog(child))) + } +} + +describe('TUI recorded-session terminal snapshots', () => { + for (const scenario of SCENARIOS) { + it(scenario.name, async () => { + observedScenarios.add(scenario.name) + const result = await runScenario(scenario) + const terminalFile = join(scenarioDir(scenario), 'terminal.golden.txt') + if (MODE === 'record' || MODE === 'refresh') { + await mkdir(scenarioDir(scenario), { recursive: true }) + await writeFile(terminalFile, result.terminal) + } + if (MODE === 'record' && scenario.recorded) await writeRecording(scenario, result) + await expect(result.terminal).toMatchFileSnapshot(terminalFile) + }, 120_000) + } +}) + +afterAll(async () => { + expect([...observedScenarios].sort()).toEqual(SCENARIOS.map(scenario => scenario.name).sort()) + const directories = (await readdir(SNAPSHOTS_DIR, { withFileTypes: true })) + .filter(entry => entry.isDirectory()) + .map(entry => entry.name) + .sort() + expect(directories).toEqual(SCENARIOS.map(scenario => scenario.name).sort()) + for (const scenario of SCENARIOS) { + const expected = [ + 'session.jsonl', + 'terminal.golden.txt', + ...scenario.seedWorkspace === true ? ['workspace'] : [], + ...Array.from({ length: scenario.childSessions ?? 0 }, (_, index) => `session.${index + 1}.jsonl`), + ].sort() + expect((await readdir(scenarioDir(scenario))).sort()).toEqual(expected) + for (const fixture of ['session.jsonl', ...childFixturePaths(scenario).map(path => basename(path))]) { + const content = await readFile(join(scenarioDir(scenario), fixture), 'utf8') + expect(scrubRequestHeaders(content), `${scenario.name}/${fixture} carries request-header bulk`).toBe(content) + } + } +}) diff --git a/packages/ui/tui/tests/snapshots/code-mode-complete.golden.txt b/packages/ui/tui/tests/snapshots/code-mode-complete.golden.txt deleted file mode 100644 index 72f7fce352..0000000000 --- a/packages/ui/tui/tests/snapshots/code-mode-complete.golden.txt +++ /dev/null @@ -1,50 +0,0 @@ -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| -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| diff --git a/packages/ui/tui/tests/snapshots/conversation-complete.golden.txt b/packages/ui/tui/tests/snapshots/conversation-complete.golden.txt deleted file mode 100644 index 8642d2ffa1..0000000000 --- a/packages/ui/tui/tests/snapshots/conversation-complete.golden.txt +++ /dev/null @@ -1,75 +0,0 @@ -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| -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| -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| -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| -27| " Reasoning " - style 1-9 fg=bright-black italic -28| " Inspecting width and styles. " - style 1-28 fg=bright-black italic -29| -30| " Assistant " - style 1-9 fg=bright-magenta bold -31| " Streaming visible state is complete. " - style 11-23 bold -32| -33| " The model reached its output-token limit. " - style 1-41 fg=yellow -34| -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 diff --git a/packages/ui/tui/tests/snapshots/conversation-replay.golden.txt b/packages/ui/tui/tests/snapshots/conversation-replay.golden.txt deleted file mode 100644 index 2395db555b..0000000000 --- a/packages/ui/tui/tests/snapshots/conversation-replay.golden.txt +++ /dev/null @@ -1,73 +0,0 @@ -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| -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| -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| -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| -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| diff --git a/packages/ui/tui/tests/snapshots/conversation-streaming.golden.txt b/packages/ui/tui/tests/snapshots/conversation-streaming.golden.txt index 55044a44f9..12a4251bb9 100644 --- a/packages/ui/tui/tests/snapshots/conversation-streaming.golden.txt +++ b/packages/ui/tui/tests/snapshots/conversation-streaming.golden.txt @@ -1,75 +1,52 @@ -terminal 96x36 buffer=normal length=41 base=5 viewport=5 +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=33 bufferRow=38 +cursor hidden column=1 viewportRow=17 bufferRow=17 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| 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. " +8| "▌ Show the live update. " style 0-0 fg=bright-blue - style 10-26 bold - style 33-37 fg=cyan 9| "▌ " style 0-0 fg=bright-blue 10| 11| " Reasoning " style 1-9 fg=bright-black italic -12| " Compare the terminal state, not write fragments. " - style 1-48 fg=bright-black italic +12| " Inspecting width and styles. " + style 1-28 fg=bright-black italic 13| 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| -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| -27| " Reasoning " - style 1-9 fg=bright-black italic -28| " Inspecting width and styles. " - style 1-28 fg=bright-black italic -29| -30| " Assistant " - style 1-9 fg=bright-magenta bold -31| " Streaming visible state… " +15| " Streaming visible state… " style 11-23 bold -32| -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| "────────────────────────────────────────────────────────────────────────────────────────────────" +16| "────────────────────────────────────────────────────────────────────────────────────────────────" style 0-95 dim -38| " " +17| " " style 1-1 inverse -39| "────────────────────────────────────────────────────────────────────────────────────────────────" +18| "────────────────────────────────────────────────────────────────────────────────────────────────" style 0-95 dim -40| "/workspace/project ↑13k ↓640 idle reasoning:on tools:compact" - style 0-28 dim +19| "/workspace/project ↑0 ↓0 idle reasoning:on tools:compact" + style 0-24 dim style 63-95 dim +20-35| diff --git a/packages/ui/tui/tests/snapshots/cordis-tools-complete.golden.txt b/packages/ui/tui/tests/snapshots/cordis-tools-complete.golden.txt deleted file mode 100644 index d33b56d7ce..0000000000 --- a/packages/ui/tui/tests/snapshots/cordis-tools-complete.golden.txt +++ /dev/null @@ -1,73 +0,0 @@ -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| -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| -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| -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| diff --git a/packages/ui/tui/tests/snapshots/dynamic-workflow-complete.golden.txt b/packages/ui/tui/tests/snapshots/dynamic-workflow-complete.golden.txt deleted file mode 100644 index 00da73a956..0000000000 --- a/packages/ui/tui/tests/snapshots/dynamic-workflow-complete.golden.txt +++ /dev/null @@ -1,53 +0,0 @@ -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| -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| diff --git a/packages/ui/tui/tests/tui.snapshot.ts b/packages/ui/tui/tests/tui.snapshot.ts index eb6d5e9965..0967e7f64c 100644 --- a/packages/ui/tui/tests/tui.snapshot.ts +++ b/packages/ui/tui/tests/tui.snapshot.ts @@ -23,15 +23,10 @@ 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', @@ -194,25 +189,8 @@ const ADVANCED_CARD_TOOLS: Record = { } 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) - + it('pins an in-flight reasoning and Markdown stream', async () => { + const harness = await setupSnapshot() await renderAfter(harness, () => { appendUser(harness.session, 'Show the live update.') harness.session.append('assistant/chunk', { @@ -237,15 +215,6 @@ describe('TUI terminal-state snapshots', () => { }) }) 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) }) @@ -260,13 +229,6 @@ describe('TUI terminal-state snapshots', () => { } 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) }) @@ -290,14 +252,6 @@ describe('TUI terminal-state snapshots', () => { } 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) }) @@ -314,13 +268,6 @@ describe('TUI terminal-state snapshots', () => { ] 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) }) From 81ca81e23dea201a92b9cc6de38dddf5d8cb790c Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Sun, 19 Jul 2026 01:42:16 +0800 Subject: [PATCH 08/13] test(tui): stabilize snapshot workspace width --- .../bash-terminal-card/terminal.golden.txt | 5 +- .../snapshots/code-mode/terminal.golden.txt | 6 +- .../terminal.golden.txt | 5 +- .../dynamic-workflow/terminal.golden.txt | 5 +- .../terminal.golden.txt | 5 +- .../parallel-file-reads/terminal.golden.txt | 65 +++++++++---------- .../snapshots/todo-plan/terminal.golden.txt | 6 +- examples/tui-agent/tests/tui.snapshot.ts | 4 +- 8 files changed, 52 insertions(+), 49 deletions(-) diff --git a/examples/tui-agent/tests/snapshots/bash-terminal-card/terminal.golden.txt b/examples/tui-agent/tests/snapshots/bash-terminal-card/terminal.golden.txt index 30770dcccb..ca123479dc 100644 --- a/examples/tui-agent/tests/snapshots/bash-terminal-card/terminal.golden.txt +++ b/examples/tui-agent/tests/snapshots/bash-terminal-card/terminal.golden.txt @@ -67,6 +67,7 @@ buffer style 1-1 inverse 28| "────────────────────────────────────────────────────────────────────────────────────────────────────" style 0-99 dim -29| "/workspace/project ↑3.0k ↓" - style 0-99 dim +29| "/workspace/project ↑3.0k ↓115 idle reasoning:on tools:compact" + style 0-58 dim + style 67-99 dim 30-35| diff --git a/examples/tui-agent/tests/snapshots/code-mode/terminal.golden.txt b/examples/tui-agent/tests/snapshots/code-mode/terminal.golden.txt index a7f1457d7a..428bdf80b4 100644 --- a/examples/tui-agent/tests/snapshots/code-mode/terminal.golden.txt +++ b/examples/tui-agent/tests/snapshots/code-mode/terminal.golden.txt @@ -73,7 +73,7 @@ buffer style 1-1 inverse 30| "────────────────────────────────────────────────────────────────────────────────────────────────────" style 0-99 dim -31| "/workspace/project ↑3.1k ↓158 idle" - style 0-93 dim - style 96-99 dim +31| "/workspace/project ↑3.1k ↓158 idle reasoning:on tools:compact" + style 0-49 dim + style 67-99 dim 32-35| diff --git a/examples/tui-agent/tests/snapshots/cordis-dynamic-toolchain/terminal.golden.txt b/examples/tui-agent/tests/snapshots/cordis-dynamic-toolchain/terminal.golden.txt index 5c5afc10ca..3359f54ca5 100644 --- a/examples/tui-agent/tests/snapshots/cordis-dynamic-toolchain/terminal.golden.txt +++ b/examples/tui-agent/tests/snapshots/cordis-dynamic-toolchain/terminal.golden.txt @@ -111,5 +111,6 @@ buffer style 1-1 inverse 48| "────────────────────────────────────────────────────────────────────────────────────────────────────" style 0-99 dim -49| "/workspace/project ↑" - style 0-99 dim +49| "/workspace/project ↑18 ↓18 idle reasoning:on tools:compact" + style 0-61 dim + style 67-99 dim diff --git a/examples/tui-agent/tests/snapshots/dynamic-workflow/terminal.golden.txt b/examples/tui-agent/tests/snapshots/dynamic-workflow/terminal.golden.txt index 8589ac94ea..1970a3761d 100644 --- a/examples/tui-agent/tests/snapshots/dynamic-workflow/terminal.golden.txt +++ b/examples/tui-agent/tests/snapshots/dynamic-workflow/terminal.golden.txt @@ -101,5 +101,6 @@ buffer style 1-1 inverse 45| "────────────────────────────────────────────────────────────────────────────────────────────────────" style 0-99 dim -46| "/workspace/project ↑3.5k ↓22" - style 0-99 dim +46| "/workspace/project ↑3.5k ↓227 idle reasoning:on tools:compact" + style 0-56 dim + style 67-99 dim diff --git a/examples/tui-agent/tests/snapshots/multi-turn-conversation/terminal.golden.txt b/examples/tui-agent/tests/snapshots/multi-turn-conversation/terminal.golden.txt index fca6c6dd4a..4c1f3b2c62 100644 --- a/examples/tui-agent/tests/snapshots/multi-turn-conversation/terminal.golden.txt +++ b/examples/tui-agent/tests/snapshots/multi-turn-conversation/terminal.golden.txt @@ -64,6 +64,7 @@ buffer style 1-1 inverse 29| "────────────────────────────────────────────────────────────────────────────────────────────────────" style 0-99 dim -30| "/workspace/project ↑2" - style 0-99 dim +30| "/workspace/project ↑2.9k ↓41 idle reasoning:on tools:compact" + style 0-62 dim + style 67-99 dim 31-35| diff --git a/examples/tui-agent/tests/snapshots/parallel-file-reads/terminal.golden.txt b/examples/tui-agent/tests/snapshots/parallel-file-reads/terminal.golden.txt index 7906694d0a..5fe973c177 100644 --- a/examples/tui-agent/tests/snapshots/parallel-file-reads/terminal.golden.txt +++ b/examples/tui-agent/tests/snapshots/parallel-file-reads/terminal.golden.txt @@ -1,7 +1,7 @@ -terminal 100x36 buffer=normal length=41 base=5 viewport=5 +terminal 100x36 buffer=normal length=39 base=3 viewport=3 lifecycle started=1 stopped=0 progress=inactive title "DSH TUI snapshot" -cursor hidden column=1 viewportRow=33 bufferRow=38 +cursor hidden column=1 viewportRow=33 bufferRow=36 buffer 0| "╭──────────────────────────────────────────────────────────────────────────────────────────────────╮" style 0-99 fg=bright-blue @@ -37,58 +37,55 @@ buffer style 0-0 fg=green style 2-2 fg=green bold style 3-13 bold -13| "▌ /workspace/project" +13| "▌ /workspace/project/a.txt " style 0-0 fg=green -14| "▌ /a.txt " +14| "▌ file " style 0-0 fg=green -15| "▌ file " +15| "▌ " style 0-0 fg=green -16| "▌ " +16| "▌ 1: alpha " style 0-0 fg=green -17| "▌ 1: alpha " +17| "▌ " style 0-0 fg=green -18| "▌ " +18| "▌ (End of file - total 1 lines) " style 0-0 fg=green -19| "▌ (End of file - total 1 lines) " +19| "▌ " style 0-0 fg=green -20| "▌ " +20| "▌ " style 0-0 fg=green -21| "▌ " +21| +22| "▌ " style 0-0 fg=green -22| -23| "▌ " - style 0-0 fg=green -24| "▌ ✓ Read b.txt " +23| "▌ ✓ Read b.txt " style 0-0 fg=green style 2-2 fg=green bold style 3-13 bold -25| "▌ /workspace/project" +24| "▌ /workspace/project/b.txt " style 0-0 fg=green -26| "▌ /b.txt " +25| "▌ file " style 0-0 fg=green -27| "▌ file " +26| "▌ " style 0-0 fg=green -28| "▌ " +27| "▌ 1: beta " style 0-0 fg=green -29| "▌ 1: beta " +28| "▌ " style 0-0 fg=green -30| "▌ " +29| "▌ (End of file - total 1 lines) " style 0-0 fg=green -31| "▌ (End of file - total 1 lines) " +30| "▌ " style 0-0 fg=green -32| "▌ " +31| "▌ " style 0-0 fg=green -33| "▌ " - style 0-0 fg=green -34| -35| " Assistant " +32| +33| " Assistant " style 1-9 fg=bright-magenta bold -36| " DONE " +34| " DONE " +35| "────────────────────────────────────────────────────────────────────────────────────────────────────" + style 0-99 dim +36| " " + style 1-1 inverse 37| "────────────────────────────────────────────────────────────────────────────────────────────────────" style 0-99 dim -38| " " - style 1-1 inverse -39| "────────────────────────────────────────────────────────────────────────────────────────────────────" - style 0-99 dim -40| "/workspace/project ↑20 ↓6" - style 0-99 dim +38| "/workspace/project ↑20 ↓6 idle reasoning:on tools:compact" + style 0-55 dim + style 67-99 dim diff --git a/examples/tui-agent/tests/snapshots/todo-plan/terminal.golden.txt b/examples/tui-agent/tests/snapshots/todo-plan/terminal.golden.txt index 2d6520f067..278015e244 100644 --- a/examples/tui-agent/tests/snapshots/todo-plan/terminal.golden.txt +++ b/examples/tui-agent/tests/snapshots/todo-plan/terminal.golden.txt @@ -76,6 +76,6 @@ buffer style 1-1 inverse 34| "────────────────────────────────────────────────────────────────────────────────────────────────────" style 0-99 dim -35| "/workspace/project ↑3.1k ↓145 idle" - style 0-93 dim - style 96-99 dim +35| "/workspace/project ↑3.1k ↓145 idle reasoning:on tools:compact" + style 0-49 dim + style 67-99 dim diff --git a/examples/tui-agent/tests/tui.snapshot.ts b/examples/tui-agent/tests/tui.snapshot.ts index 453c587c2a..276c9c5f03 100644 --- a/examples/tui-agent/tests/tui.snapshot.ts +++ b/examples/tui-agent/tests/tui.snapshot.ts @@ -28,6 +28,8 @@ import WorkerWorkflowEngine from '@deepseek-ai/dsh-workflow-workerthread' import { HeadlessTerminal } from '../../../packages/ui/tui/tests/headless-terminal.ts' const SNAPSHOTS_DIR = join(dirname(fileURLToPath(import.meta.url)), 'snapshots') +// Keep pre-normalization layout widths identical across macOS and Linux. +const SNAPSHOT_TMP_ROOT = process.platform === 'win32' ? tmpdir() : '/tmp' const PROVIDERS = [{ id: 'deepseek', models: [{ id: 'deepseek-v4-flash' }] }] const UUID_RE = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi @@ -204,7 +206,7 @@ async function runScenario(scenario: Scenario): Promise { const prompts = userPrompts(fixture) expect(prompts.length, `${scenario.name} must carry at least one recorded user prompt`).toBeGreaterThan(0) - const cwd = await mkdtemp(join(tmpdir(), `dsh-tui-snapshot-${scenario.name}-`)) + const cwd = await mkdtemp(join(SNAPSHOT_TMP_ROOT, `dsh-tui-snapshot-${scenario.name}-`)) let ctx: Context | undefined let controller: ReturnType | undefined const terminal = new HeadlessTerminal(100, 36) From 3760f198f39222613f0079ff18be9b02af864c10 Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Sun, 19 Jul 2026 02:16:43 +0800 Subject: [PATCH 09/13] docs: distinguish readline and TUI demos --- AGENTS.md | 1 + README.i18n.yaml | 4 ++-- README.md | 5 ++++- README.zh.md | 5 ++++- docs/cookbook/extension-cookbook.i18n.yaml | 4 ++-- docs/cookbook/extension-cookbook.md | 2 +- docs/cookbook/extension-cookbook.zh.md | 2 +- docs/development.i18n.yaml | 4 ++-- docs/development.md | 14 +++++++++++++- docs/development.zh.md | 14 +++++++++++++- docs/i18n/translation-prompt.md | 6 +++--- packages/examples/stdio-demo/README.md | 4 ++-- 12 files changed, 48 insertions(+), 17 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 50d157e191..3aed94d364 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -58,6 +58,7 @@ pnpm run doc-sync # all documentation gates; see the doc-sync script in pa pnpm run website:build # VitePress build (doubles as the site's dead-link check) pnpm run demo:echo # mock-model REPL, no key needed pnpm run demo:repl # real REPL coding agent (needs DEEPSEEK_API_KEY) +pnpm run demo:tui # full-screen TUI coding agent (needs DEEPSEEK_API_KEY) pnpm run demo:cordis # self-referential demo: the agent modifies its own runtime (needs key) pnpm run demo:acp # ACP server agent (needs DEEPSEEK_API_KEY) ``` diff --git a/README.i18n.yaml b/README.i18n.yaml index 628d7168da..19c4ccf72d 100644 --- a/README.i18n.yaml +++ b/README.i18n.yaml @@ -2,5 +2,5 @@ # 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 -README.md: 92cbfaa8196b49d2b801776f0e0c178cb5f673dc -README.zh.md: 28604ad1e2926edd7d4eb91c8027785433a54d32 +README.md: aaa129272ee9346cebe2d59774d742fbe21af80a +README.zh.md: 42f2ed9b57bf008210042083977b7adbb7f1ab0e diff --git a/README.md b/README.md index 92cbfaa819..aaa129272e 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,10 @@ This monorepo is built on the [Cordis](https://github.com/cordiverse/cordis) fra ```sh pnpm install pnpm run test # vitest -pnpm run demo:repl # interactive pi-tui coding agent (needs DEEPSEEK_API_KEY) +pnpm run demo:echo # keyless mock-model REPL +pnpm run demo:repl # readline coding agent (needs DEEPSEEK_API_KEY) +pnpm run demo:tui # full-screen TUI coding agent (needs DEEPSEEK_API_KEY) +pnpm run demo:cordis # self-referential agent demo (needs DEEPSEEK_API_KEY) pnpm run demo:acp # ACP server agent demo (needs DEEPSEEK_API_KEY) ``` diff --git a/README.zh.md b/README.zh.md index 28604ad1e2..42f2ed9b57 100644 --- a/README.zh.md +++ b/README.zh.md @@ -11,7 +11,10 @@ ```sh pnpm install pnpm run test # vitest -pnpm run demo:repl # interactive pi-tui coding agent (needs DEEPSEEK_API_KEY) +pnpm run demo:echo # keyless mock-model REPL +pnpm run demo:repl # readline coding agent (needs DEEPSEEK_API_KEY) +pnpm run demo:tui # full-screen TUI coding agent (needs DEEPSEEK_API_KEY) +pnpm run demo:cordis # self-referential agent demo (needs DEEPSEEK_API_KEY) pnpm run demo:acp # ACP server agent demo (needs DEEPSEEK_API_KEY) ``` diff --git a/docs/cookbook/extension-cookbook.i18n.yaml b/docs/cookbook/extension-cookbook.i18n.yaml index 598e9827b1..4ebefda6eb 100644 --- a/docs/cookbook/extension-cookbook.i18n.yaml +++ b/docs/cookbook/extension-cookbook.i18n.yaml @@ -2,5 +2,5 @@ # 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 -extension-cookbook.md: 02dc22ff72807629a89d602f49752e6f429587b5 -extension-cookbook.zh.md: a204abdf6656312aea94640c60038af9f81f4f7b +extension-cookbook.md: c500aac230b6efc224c9d9e54bce75ef09b7173a +extension-cookbook.zh.md: d886b8ed40e32b2697e82172a86ca3b9653f287d diff --git a/docs/cookbook/extension-cookbook.md b/docs/cookbook/extension-cookbook.md index 02dc22ff72..c500aac230 100644 --- a/docs/cookbook/extension-cookbook.md +++ b/docs/cookbook/extension-cookbook.md @@ -87,7 +87,7 @@ export function apply(ctx: Context) { ## Runnable wirings -Three complete examples load their plugin trees from `cordis.yml`: [`examples/echo-agent`](../../examples/echo-agent) (mock model + echo tool — the all-mock skeleton check, `pnpm run demo:echo`), [`examples/coding-agent`](../../examples/coding-agent) (DeepSeek V4 + the bash tool suite behind a pi-tui coding interface with a readline fallback, `pnpm run demo:repl`), and [`examples/acp-agent`](../../examples/acp-agent) (an agent exposed as an ACP server over JSON-RPC stdio — the client-driver shape, `pnpm run demo:acp`). Each leaf is just its swappable backends plus an app-package entry: the stdio demos load [`@deepseek-ai/dsh-stdio-demo`](../../packages/examples/stdio-demo), the ACP demo loads [`@deepseek-ai/dsh-acp-demo`](../../packages/examples/acp-demo), and both app packages share the spine via the [`@deepseek-ai/dsh-agent-spine-demo`](../../packages/examples/agent-spine-demo) bundle. +Five runnable leaves load their plugin trees from `cordis.yml`: [`examples/echo-agent`](../../examples/echo-agent) (mock model + echo tool, `pnpm run demo:echo`), [`examples/coding-agent`](../../examples/coding-agent) (DeepSeek V4 + coding tools through a line-oriented readline REPL, `pnpm run demo:repl`), [`examples/tui-agent`](../../examples/tui-agent) (the same coding composition through full-screen pi-tui, `pnpm run demo:tui`), [`examples/cordis-agent`](../../examples/cordis-agent) (self-inspection and dynamic plugin mounting, `pnpm run demo:cordis`), and [`examples/acp-agent`](../../examples/acp-agent) (an ACP server over JSON-RPC stdio, `pnpm run demo:acp`). The terminal leaves load [`@deepseek-ai/dsh-stdio-demo`](../../packages/examples/stdio-demo), the ACP leaf loads [`@deepseek-ai/dsh-acp-demo`](../../packages/examples/acp-demo), and both app packages share the spine through [`@deepseek-ai/dsh-agent-spine-demo`](../../packages/examples/agent-spine-demo). ## The feature → mechanism map diff --git a/docs/cookbook/extension-cookbook.zh.md b/docs/cookbook/extension-cookbook.zh.md index a204abdf66..d886b8ed40 100644 --- a/docs/cookbook/extension-cookbook.zh.md +++ b/docs/cookbook/extension-cookbook.zh.md @@ -87,7 +87,7 @@ export function apply(ctx: Context) { ## 可运行的组装示例 -三个完整示例从 `cordis.yml` 加载各自的插件树:[`examples/echo-agent`](../../examples/echo-agent)(mock 模型 + echo 工具——全 mock 骨架检查,`pnpm run demo:echo`)、[`examples/coding-agent`](../../examples/coding-agent)(DeepSeek V4 + bash 工具套件,配合 pi-tui 编码界面,并为管道输入保留 readline 回退,`pnpm run demo:repl`)、[`examples/acp-agent`](../../examples/acp-agent)(通过 JSON-RPC stdio 暴露为 ACP 服务器的 agent——客户端驱动形态,`pnpm run demo:acp`)。每个叶子只是其可替换后端加一个 app 包入口:stdio 演示加载 [`@deepseek-ai/dsh-stdio-demo`](../../packages/examples/stdio-demo),ACP 演示加载 [`@deepseek-ai/dsh-acp-demo`](../../packages/examples/acp-demo),两个 app 包通过 [`@deepseek-ai/dsh-agent-spine-demo`](../../packages/examples/agent-spine-demo) bundle 共享主干。 +五个可运行叶子从 `cordis.yml` 加载各自的插件树:[`examples/echo-agent`](../../examples/echo-agent)(mock 模型 + echo 工具,`pnpm run demo:echo`)、[`examples/coding-agent`](../../examples/coding-agent)(DeepSeek V4 + coding 工具,通过面向行的 readline REPL 交互,`pnpm run demo:repl`)、[`examples/tui-agent`](../../examples/tui-agent)(通过全屏 pi-tui 复用相同的 coding 组装,`pnpm run demo:tui`)、[`examples/cordis-agent`](../../examples/cordis-agent)(自我检查和动态插件挂载,`pnpm run demo:cordis`)与 [`examples/acp-agent`](../../examples/acp-agent)(通过 JSON-RPC stdio 暴露的 ACP 服务器,`pnpm run demo:acp`)。终端叶子加载 [`@deepseek-ai/dsh-stdio-demo`](../../packages/examples/stdio-demo),ACP 叶子加载 [`@deepseek-ai/dsh-acp-demo`](../../packages/examples/acp-demo),两个 app 包通过 [`@deepseek-ai/dsh-agent-spine-demo`](../../packages/examples/agent-spine-demo) 共享主干。 ## 功能→机制映射 diff --git a/docs/development.i18n.yaml b/docs/development.i18n.yaml index 1d687a3f90..3630b63815 100644 --- a/docs/development.i18n.yaml +++ b/docs/development.i18n.yaml @@ -2,5 +2,5 @@ # 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 -development.md: 512fdcd89b4d481dd2033bc4f4c2816a04b5feaa -development.zh.md: 9f2038a99b289d06f16d7d51f0d043caa34a300e +development.md: 79caf482d0d104ed170a57812dd4f34e2329de8b +development.zh.md: 75781932acedabd2e3593a89aed8360a9a18ca5b diff --git a/docs/development.md b/docs/development.md index 512fdcd89b..79caf482d0 100644 --- a/docs/development.md +++ b/docs/development.md @@ -109,12 +109,24 @@ The echo demo does not need API credentials: pnpm run demo:echo ``` -The coding-agent demo uses pi-tui interactively, falls back to readline for pipes, and needs `DEEPSEEK_API_KEY` in the environment or repo-root `.env`: +The coding-agent REPL uses the line-oriented readline front door and needs `DEEPSEEK_API_KEY` in the environment or repo-root `.env`: ```sh pnpm run demo:repl ``` +The full-screen TUI reuses the coding-agent composition through the pi-tui front door and needs the same credentials: + +```sh +pnpm run demo:tui +``` + +The self-referential cordis-agent demo can inspect and modify its live plugin runtime and needs the same credentials: + +```sh +pnpm run demo:cordis +``` + The ACP server agent demo exposes the agent over JSON-RPC stdio and also needs `DEEPSEEK_API_KEY`: ```sh diff --git a/docs/development.zh.md b/docs/development.zh.md index 9f2038a99b..75781932ac 100644 --- a/docs/development.zh.md +++ b/docs/development.zh.md @@ -109,12 +109,24 @@ echo 演示不需要 API 凭证: pnpm run demo:echo ``` -coding-agent 演示在交互终端中使用 pi-tui,对管道输入回退到 readline,并需要环境变量或仓库根目录 `.env` 中的 `DEEPSEEK_API_KEY`: +coding-agent REPL 使用面向行的 readline 前端,并需要环境变量或仓库根目录 `.env` 中的 `DEEPSEEK_API_KEY`: ```sh pnpm run demo:repl ``` +全屏 TUI 通过 pi-tui 前端复用 coding-agent 组装,并需要相同的凭证: + +```sh +pnpm run demo:tui +``` + +自指的 cordis-agent 演示可以检查并修改其实时插件运行时,并需要相同的凭证: + +```sh +pnpm run demo:cordis +``` + ACP 服务器 agent 演示通过 JSON-RPC stdio 暴露 agent,同样需要 `DEEPSEEK_API_KEY`: ```sh diff --git a/docs/i18n/translation-prompt.md b/docs/i18n/translation-prompt.md index 4dd877bea4..cd87c161de 100644 --- a/docs/i18n/translation-prompt.md +++ b/docs/i18n/translation-prompt.md @@ -123,9 +123,9 @@ Follow the Good versions; these sentence-level examples illustrate error categor - Good: `A green gate does not mean the translation is correct.` ### Code block comments — never translate -- Source code block contains: `# interactive pi-tui coding agent (needs DEEPSEEK_API_KEY)` -- Bad: `# 交互式 pi-tui 编码 agent(需要 DEEPSEEK_API_KEY)` -- Good: `# interactive pi-tui coding agent (needs DEEPSEEK_API_KEY)` (byte-identical) +- Source code block contains: `# readline coding agent (needs DEEPSEEK_API_KEY)` +- Bad: `# readline 编码 agent(需要 DEEPSEEK_API_KEY)` +- Good: `# readline coding agent (needs DEEPSEEK_API_KEY)` (byte-identical) ### Language switcher — English to Chinese - Source: `English | [中文](README.zh.md)` diff --git a/packages/examples/stdio-demo/README.md b/packages/examples/stdio-demo/README.md index f1d4ecd212..3721116a22 100644 --- a/packages/examples/stdio-demo/README.md +++ b/packages/examples/stdio-demo/README.md @@ -18,7 +18,7 @@ A terminal chat always wants the same cluster, so the package owns it rather tha | `@deepseek-ai/dsh-stdio` | the line-oriented terminal channel, bound to `main` for pipes and automation; matching `agent/start-failed` errors print and exit nonzero | | `@deepseek-ai/dsh-tui` | the interactive pi-tui channel, bound to `main` for TTY pairs; matching `agent/start-failed` startup errors are printed before fullscreen mode and exit nonzero | -`@cordisjs/plugin-hmr` (the dev/demo edit-reload loop) is deliberately a **leaf** entry, not baked in here: it is a Loader-only, subprocess-only dev plugin whose constructor needs `node --expose-internals` plus a live `loader`. The `demo:echo` / `demo:repl` leaves load it and pass `--expose-internals`. +`@cordisjs/plugin-hmr` (the dev/demo edit-reload loop) is deliberately a **leaf** entry, not baked in here: it is a Loader-only, subprocess-only dev plugin whose constructor needs `node --expose-internals` plus a live `loader`. The repository's terminal demo trees load it and their scripts pass `--expose-internals`. The leaf `cordis.yml` supplies only the **swappable backends** — an LLM adapter (`llm-deepseek` for the real model, or the mock `mock-llm` for a demo) and a bash executor (`bash-local`) — `hmr`, plus this app's [`Config`](#config). The whole plugin tree a run loads is therefore: this app's cluster, the spine inside `agent-spine-demo`, `hmr`, and the two leaf backends. @@ -45,7 +45,7 @@ Fresh stdio sessions use the process launch directory as `session.header.cwd`, s ## The bin -`dsh-stdio-demo [path-to-cordis.yml]` (default `./cordis.yml`) loads a gitignored `.env` from the cwd (`DEEPSEEK_API_KEY` / `DEEPSEEK_BASE_URL`), then drives the cordis Loader against the config and awaits the whole plugin tree before returning. Run it under `node --expose-internals`, or install the Loader's optional `node-addon-require-builtin` fallback, so the Loader can resolve the config's bare plugin specifiers (`@deepseek-ai/dsh-*`, npm packages). The `demo:echo` / `demo:repl` scripts use `--expose-internals`. +`dsh-stdio-demo [path-to-cordis.yml]` (default `./cordis.yml`) loads a gitignored `.env` from the cwd (`DEEPSEEK_API_KEY` / `DEEPSEEK_BASE_URL`), then drives the cordis Loader against the config and awaits the whole plugin tree before returning. Run it under `node --expose-internals`, or install the Loader's optional `node-addon-require-builtin` fallback, so the Loader can resolve the config's bare plugin specifiers (`@deepseek-ai/dsh-*`, npm packages). The repository's demo scripts using this bin pass `--expose-internals`. ## Example leaf `cordis.yml` From b5e8e4e9c1a7a96f88a33e40fe45f9c039b12b5d Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Sun, 19 Jul 2026 10:49:59 +0800 Subject: [PATCH 10/13] fix(tui): neutralize terminal controls at display boundary Model responses, replayed session data, tool presenter output, question metadata, configuration, and diagnostics all cross into ANSI-aware pi-tui renderers. Passing their C0 or C1 controls through unchanged lets an otherwise ordinary transcript emit OSC, CSI, cursor, or title operations in the user terminal. Introduce one displayText boundary that preserves line-feed layout but renders every other C0/C1 control as visible \\xNN text before application styling is applied. Route transcript blocks, streaming output, tool cards, diffs, plans, dialogs, headers, cwd/title data, notices, errors, and pre-mount startup failures through that boundary while leaving pi-tui and the theme responsible for legitimate terminal control sequences. Pin the contract at three levels: a settled headless-terminal golden spans the main untrusted display sources, unit coverage checks the pre-fullscreen failure path, and the real Loader/PTY conversation streams hostile OSC, cursor, and C1 probes and proves only their inert textual forms reach the terminal stream. --- .../tests/fixtures/tui-scripted-llm.ts | 3 +- .../tui-agent/tests/tui-keyless-smoke.e2e.ts | 6 + packages/ui/tui/README.md | 2 + packages/ui/tui/src/index.ts | 88 +++++++++------ .../snapshots/untrusted-controls.golden.txt | 106 ++++++++++++++++++ packages/ui/tui/tests/tui.snapshot.ts | 80 +++++++++++++ packages/ui/tui/tests/tui.spec.ts | 4 +- 7 files changed, 250 insertions(+), 39 deletions(-) create mode 100644 packages/ui/tui/tests/snapshots/untrusted-controls.golden.txt diff --git a/examples/tui-agent/tests/fixtures/tui-scripted-llm.ts b/examples/tui-agent/tests/fixtures/tui-scripted-llm.ts index 2147dfb222..c55b3d355c 100644 --- a/examples/tui-agent/tests/fixtures/tui-scripted-llm.ts +++ b/examples/tui-agent/tests/fixtures/tui-scripted-llm.ts @@ -2,7 +2,8 @@ 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 CONTROL_PROBE = '\u001b]2;MODEL_CONTROLLED\u0007\u001b[999CMODEL_CURSOR\u009b31mMODEL_C1' +const INITIAL_TEXT = `I need one decision before I continue. ${CONTROL_PROBE}` const FINAL_TEXT = 'Decision received. Scripted TUI run complete.' function textChunks(text: string): StreamChunk[] { diff --git a/examples/tui-agent/tests/tui-keyless-smoke.e2e.ts b/examples/tui-agent/tests/tui-keyless-smoke.e2e.ts index 26cee8aea2..f9b2ec1d43 100644 --- a/examples/tui-agent/tests/tui-keyless-smoke.e2e.ts +++ b/examples/tui-agent/tests/tui-keyless-smoke.e2e.ts @@ -153,6 +153,12 @@ describe('tui-agent keyless smoke (real Loader tree in a PTY)', () => { 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(String.raw`\x1b]2;MODEL_CONTROLLED\x07`) + expect(output).toContain(String.raw`\x1b[999CMODEL_CURSOR`) + expect(output).toContain(String.raw`\x9b31mMODEL_C1`) + expect(output).not.toContain('\u001B]2;MODEL_CONTROLLED\u0007') + expect(output).not.toContain('\u001B[999CMODEL_CURSOR') + expect(output).not.toContain('\u009B31mMODEL_C1') expect(output).toContain('How should the scripted run proceed?') expect(output).toContain('Safe') expect(output).toContain('Decision received. Scripted TUI run complete.') diff --git a/packages/ui/tui/README.md b/packages/ui/tui/README.md index 865a20a469..cf4cc609ce 100644 --- a/packages/ui/tui/README.md +++ b/packages/ui/tui/README.md @@ -6,6 +6,8 @@ This package owns interactive terminal presentation and input only. It injects ` The TUI rebuilds resumed history from the active session surface, renders Markdown responses and reasoning, applies each tool's `presentCall` / `presentResult` intent to terminal, diff, or generic cards, keeps the latest `todo/write` plan above the editor, and presents `ctx.userInteraction` questions as keyboard-driven overlays. Surface replacement events rebuild the transcript so compacted history does not reappear. +Before model output, session events, tool presenters, questions, configuration, or diagnostics reach pi-tui's ANSI-aware renderers or the terminal title, the TUI renders C0 and C1 controls other than line feeds as visible `\xNN` text. Those sources cannot add terminal control sequences; the TUI and pi-tui retain ownership of terminal rendering and styling. + While the agent is running, editor submissions call `agent.steer()`; otherwise they call `agent.send()`. Ctrl+C or Escape cancels a running turn. Ctrl+O expands tool cards, Ctrl+R toggles reasoning, Ctrl+L redraws, and Ctrl+D exits while idle. `/help`, `/clear`, `/cancel`, `/reasoning`, `/tools`, `/redraw`, and `/exit` provide the same actions without key chords. ## Config diff --git a/packages/ui/tui/src/index.ts b/packages/ui/tui/src/index.ts index b9b74e7c9a..612f2f3099 100644 --- a/packages/ui/tui/src/index.ts +++ b/packages/ui/tui/src/index.ts @@ -179,6 +179,17 @@ function ansi(open: string, close: string, enabled: boolean): (text: string) => return enabled ? text => `\x1b[${open}m${text}\x1b[${close}m` : text => text } +const TERMINAL_CONTROL_PATTERN = /[\u0000-\u0009\u000b-\u001f\u007f-\u009f]/gu + +/** + * Escape external C0/C1 controls before pi-tui adds application-owned ANSI. + * Line feeds remain structural so transcript and tool output retain their layout. + */ +function displayText(text: string): string { + return text.replace(TERMINAL_CONTROL_PATTERN, control => + `\\x${control.charCodeAt(0).toString(16).padStart(2, '0')}`) +} + /** * Theme-agnostic palette built from the standard 16-color ANSI set plus SGR * attributes, which every terminal remaps to its active color scheme. Body @@ -281,11 +292,11 @@ class HeaderComponent implements Component { render(width: number): string[] { const usable = Math.max(1, width - 4) const title = `${this.palette.bold(this.palette.accent('DEEPSEEK'))} ${this.palette.bold('HARNESS')}` - const model = this.agent.options.model ?? 'model unset' - const detail = `${this.agent.id} • ${model} • ${this.agent.session.id}` + const model = displayText(this.agent.options.model ?? 'model unset') + const detail = `${displayText(this.agent.id)} • ${model} • ${displayText(this.agent.session.id)}` const top = this.palette.accent(`╭${'─'.repeat(Math.max(0, width - 2))}╮`) const bottom = this.palette.accent(`╰${'─'.repeat(Math.max(0, width - 2))}╯`) - const lines = [title, this.palette.muted(this.welcome), this.palette.dim(detail)] + const lines = [title, this.palette.muted(displayText(this.welcome)), this.palette.dim(detail)] .flatMap(line => wrapTextWithAnsi(line, usable)) .map((line) => { const clipped = truncateToWidth(line, usable, '') @@ -330,8 +341,8 @@ class GutterBox implements Component { class UserMessageComponent extends GutterBox { constructor(text: string, palette: Palette, mdTheme: MarkdownTheme, label = 'You') { super(value => palette.accent(value)) - this.addChild(new Text(palette.bold(palette.accent(label)), 0, 0)) - this.addChild(new Markdown(text, 0, 0, mdTheme, { color: value => palette.text(value) }, { + this.addChild(new Text(palette.bold(palette.accent(displayText(label))), 0, 0)) + this.addChild(new Markdown(displayText(text), 0, 0, mdTheme, { color: value => palette.text(value) }, { preserveOrderedListMarkers: true, preserveBackslashEscapes: true, })) @@ -341,8 +352,8 @@ class UserMessageComponent extends GutterBox { class AssistantMessageComponent extends Container { constructor(content: readonly ContentBlock[], showReasoning: boolean, palette: Palette, mdTheme: MarkdownTheme) { super() - const reasoning = textBlocks(content, 'reasoning').trim() - const text = textBlocks(content, 'text').trim() + const reasoning = displayText(textBlocks(content, 'reasoning').trim()) + const text = displayText(textBlocks(content, 'text').trim()) if (reasoning && showReasoning) { this.addChild(new Spacer(1)) this.addChild(new Text(palette.italic(palette.muted('Reasoning')), 1, 0)) @@ -422,19 +433,19 @@ function parseArguments(raw: string): ParsedArguments { } function pretty(value: unknown): string { - if (typeof value === 'string') return value + if (typeof value === 'string') return displayText(value) // The lib declaration narrows `unknown` to a string-returning overload, but // JSON.stringify returns undefined for runtime values such as symbols. const serialized = JSON.stringify(value, null, 2) as string | undefined - return serialized ?? String(value) + return displayText(serialized ?? String(value)) } function diffLines(diff: FileDiff, palette: Palette): string[] { - const lines = [palette.bold(diff.path)] + const lines = [palette.bold(displayText(diff.path))] if (diff.oldText !== null) { - for (const line of diff.oldText.split('\n')) lines.push(palette.removed(`- ${line}`)) + for (const line of displayText(diff.oldText).split('\n')) lines.push(palette.removed(`- ${line}`)) } - for (const line of diff.newText.split('\n')) lines.push(palette.added(`+ ${line}`)) + for (const line of displayText(diff.newText).split('\n')) lines.push(palette.added(`+ ${line}`)) return lines } @@ -460,10 +471,10 @@ class ToolCardComponent implements Component { const view = this.definition.presentCall(this.parsed.value) if (view !== undefined) return view } catch (error: unknown) { - return { card: 'generic', title: this.name, rawInput: `Presenter failed: ${String(error)}` } + return { card: 'generic', title: displayText(this.name), rawInput: `Presenter failed: ${String(error)}` } } } - return { card: 'generic', title: this.name, rawInput: this.parsed.value } + return { card: 'generic', title: displayText(this.name), rawInput: this.parsed.value } } updateResult(event: Extract['data']): void { @@ -492,7 +503,7 @@ class ToolCardComponent implements Component { const isError = this.result?.isError ?? false const glyph = this.result === undefined ? this.palette.warning('◌') : isError ? this.palette.error('✕') : this.palette.success('✓') const body = this.renderBody() - const title = truncateToWidth(`${glyph} ${this.title()}`, Math.max(1, width - 4), '') + const title = truncateToWidth(`${glyph} ${displayText(this.title())}`, Math.max(1, width - 4), '') const visibleBody = this.expanded || body.length <= this.maxOutputLines ? body : [...body.slice(0, this.maxOutputLines), this.palette.dim(`… ${body.length - this.maxOutputLines} more lines (Ctrl+O to expand)`)] @@ -514,17 +525,19 @@ class ToolCardComponent implements Component { if (view.card === 'terminal') { const pending = this.callView.card === 'terminal' ? this.callView : undefined const lines: string[] = [] - if (pending?.description) lines.push(this.palette.muted(pending.description)) - if (pending?.cwd) lines.push(this.palette.dim(pending.cwd)) + if (pending?.description) lines.push(this.palette.muted(displayText(pending.description))) + if (pending?.cwd) lines.push(this.palette.dim(displayText(pending.cwd))) if (this.resultView?.card === 'terminal') { - if (this.resultView.output) lines.push(...this.resultView.output.split('\n')) + if (this.resultView.output) lines.push(...displayText(this.resultView.output).split('\n')) if (this.resultView.exitCode !== undefined) lines.push(this.palette.dim(`[exit ${this.resultView.exitCode}]`)) - if (this.resultView.signal !== undefined) lines.push(this.palette.error(`[signal ${this.resultView.signal}]`)) + if (this.resultView.signal !== undefined) { + lines.push(this.palette.error(`[signal ${displayText(this.resultView.signal)}]`)) + } } else if (this.result === undefined) { // A pending terminal view is the call view itself; TerminalCallView requires a title. - lines.push(this.palette.code(`$ ${(pending as TerminalCallView).title}`)) + lines.push(this.palette.code(`$ ${displayText((pending as TerminalCallView).title)}`)) } else { - lines.push(...contentText(this.result.content).split('\n')) + lines.push(...displayText(contentText(this.result.content)).split('\n')) } return lines.filter(Boolean) } @@ -536,7 +549,7 @@ class ToolCardComponent implements Component { } const content = view.content ?? this.result?.content const lines: string[] = [] - if (content !== undefined) lines.push(...contentText(content).split('\n')) + if (content !== undefined) lines.push(...displayText(contentText(content)).split('\n')) const rawInput = this.result === undefined && this.callView.card === 'generic' ? this.callView.rawInput : undefined @@ -565,7 +578,8 @@ class TodoComponent implements Component { : todo.status === 'in_progress' ? this.palette.warning('●') : this.palette.dim('○') - const text = todo.status === 'completed' ? this.palette.muted(todo.content) : todo.content + const content = displayText(todo.content) + const text = todo.status === 'completed' ? this.palette.muted(content) : content lines.push(truncateToWidth(` ${prefix} ${text}`, width, '')) } return ['', ...lines] @@ -584,8 +598,8 @@ function formatCwd(cwd: string | undefined): string { const home = homedir() const rel = relative(resolve(home), resolve(cwd)) if (rel === '') return '~' - if (rel !== '..' && !rel.startsWith(`..${sep}`)) return `~${sep}${rel}` - return cwd + if (rel !== '..' && !rel.startsWith(`..${sep}`)) return displayText(`~${sep}${rel}`) + return displayText(cwd) } function sessionTokens(session: Session): { input: number; output: number } { @@ -702,7 +716,7 @@ class QuestionDialog implements Component, Focusable { render(width: number): string[] { this.input.focused = this.focused const innerWidth = Math.max(1, width - 4) - const title = this.question.header ?? 'Question' + const title = displayText(this.question.header ?? 'Question') const topLabel = ` ${title} ` const top = `╭${topLabel}${'─'.repeat(Math.max(0, width - visibleWidth(topLabel) - 2))}╮` const lines: string[] = [this.palette.accent(top)] @@ -710,7 +724,7 @@ class QuestionDialog implements Component, Focusable { const clipped = truncateToWidth(line, innerWidth, '') lines.push(`${this.palette.accent('│')} ${clipped}${' '.repeat(Math.max(0, innerWidth - visibleWidth(clipped)))} ${this.palette.accent('│')}`) } - for (const line of wrapTextWithAnsi(this.palette.bold(this.question.question), innerWidth)) push(line) + for (const line of wrapTextWithAnsi(this.palette.bold(displayText(this.question.question)), innerWidth)) push(line) push('') if (this.mode === 'custom') { for (const line of this.input.render(innerWidth)) push(line) @@ -729,8 +743,10 @@ class QuestionDialog implements Component, Focusable { const mark = this.question.multiSelect ? this.selected.has(index) ? this.palette.success('[x]') : '[ ]' : index === this.selectedIndex ? this.palette.accent('●') : this.palette.dim('○') - const description = option.description ? this.palette.muted(` — ${option.description}`) : '' - const line = `${cursor} ${mark} ${option.label}${description}` + const description = option.description + ? this.palette.muted(` — ${displayText(option.description)}`) + : '' + const line = `${cursor} ${mark} ${displayText(option.label)}${description}` push(index === this.selectedIndex ? this.palette.selected(line) : line) } if (options.length > this.maxVisible) push(this.palette.dim(`${this.selectedIndex + 1}/${options.length}`)) @@ -826,7 +842,7 @@ export function createTuiChat( ui.addChild(editor) ui.addChild(footer) ui.setFocus(editor) - runtime.terminal.setTitle(resolved.title) + runtime.terminal.setTitle(displayText(resolved.title)) const requestRender = (): void => { footer.invalidate() @@ -836,7 +852,7 @@ export function createTuiChat( const appendNotice = (message: string, kind: 'info' | 'warning' | 'error' = 'info'): void => { const color = kind === 'error' ? palette.error : kind === 'warning' ? palette.warning : palette.muted chat.addChild(new Spacer(1)) - chat.addChild(new Text(color(message), 1, 0)) + chat.addChild(new Text(color(displayText(message)), 1, 0)) requestRender() } @@ -876,7 +892,7 @@ export function createTuiChat( const renderEvent = (event: SessionEvent, options: { addHistory: boolean; renderChunks: boolean }): void => { switch (event.type) { case 'user/message': { - const text = contentText(event.data.content).trim() + const text = displayText(contentText(event.data.content).trim()) if (text) { chat.addChild(new Spacer(1)) chat.addChild(new UserMessageComponent(text, palette, mdTheme)) @@ -885,7 +901,7 @@ export function createTuiChat( break } case 'steering/message': { - const text = contentText(event.data.content).trim() + const text = displayText(contentText(event.data.content).trim()) if (text) { chat.addChild(new Spacer(1)) chat.addChild(new UserMessageComponent(text, palette, mdTheme, 'Steering')) @@ -893,11 +909,11 @@ export function createTuiChat( break } case 'context/message': { - const text = contentText(event.data.content).trim() + const text = displayText(contentText(event.data.content).trim()) if (text) { const source = event.data.source.kind === 'plugin' ? event.data.source.plugin : event.data.source.kind chat.addChild(new Spacer(1)) - chat.addChild(new Text(palette.dim(`Context · ${source}`), 1, 0)) + chat.addChild(new Text(palette.dim(`Context · ${displayText(source)}`), 1, 0)) chat.addChild(new Text(palette.muted(text), 1, 0)) } break @@ -1291,7 +1307,7 @@ export function mountTui(ctx: Context, config: Config, runtime: TuiRuntime): voi }, 'ui-tui') }, onFailed: (error) => { - runtime.terminal.write(`ui-tui: agent "${agentId}" failed to start: ${error.message}\n`) + runtime.terminal.write(displayText(`ui-tui: agent "${agentId}" failed to start: ${error.message}\n`)) runtime.exit(1) }, }) diff --git a/packages/ui/tui/tests/snapshots/untrusted-controls.golden.txt b/packages/ui/tui/tests/snapshots/untrusted-controls.golden.txt new file mode 100644 index 0000000000..1a64b277d5 --- /dev/null +++ b/packages/ui/tui/tests/snapshots/untrusted-controls.golden.txt @@ -0,0 +1,106 @@ +terminal 100x34 buffer=normal length=40 base=6 viewport=6 +lifecycle started=1 stopped=0 progress=inactive +title "Unsafe terminal title \\x1b]2;snapshot-controlled\\x07\\x09\\x7f\\x9b31m" +cursor hidden column=100 viewportRow=33 bufferRow=39 +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| "│ Unsafe welcome \\x1b]2;snapshot-controlled\\x07\\x09\\x7f\\x9b31m │" + style 0-0 fg=bright-blue + style 2-61 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| +6| "▌ " + style 0-0 fg=bright-blue +7| "▌ You " + style 0-0 fg=bright-blue + style 2-4 fg=bright-blue bold +8| "▌ Unsafe user \\x1b]2;snapshot-controlled\\x07\\x09\\x7f\\x9b31m " + style 0-0 fg=bright-blue +9| "▌ " + style 0-0 fg=bright-blue +10| +11| " Reasoning " + style 1-9 fg=bright-black italic +12| " Unsafe reasoning \\x1b]2;snapshot-controlled\\x07\\x09\\x7f\\x9b31m " + style 1-62 fg=bright-black italic +13| +14| " Assistant " + style 1-9 fg=bright-magenta bold +15| " Unsafe assistant \\x1b]2;snapshot-controlled\\x07\\x09\\x7f\\x9b31m " +16| +17| "▌ " + style 0-0 fg=green +18| "▌ ✓ Unsafe title \\x1b]2;snapshot-controlled\\x07\\x09\\x7f\\x9b31m " + style 0-0 fg=green + style 2-2 fg=green bold + style 3-61 bold +19| "▌ Unsafe description \\x1b]2;snapshot-controlled\\x07\\x09\\x7f\\x9b31m " + style 0-0 fg=green + style 2-65 fg=bright-black +20| "▌ /unsafe/\\x1b╭ Unsafe header \\x1b]2;snapshot-controlled\\x07\\x09\\x7f\\x9b31m ─────────╮ " + style 0-0 fg=green + style 2-13 dim + style 14-85 fg=bright-blue +21| "▌ Unsafe outpu│ Unsafe question \\x1b]2;snapshot-controlled\\x07\\x09\\x7f\\x9b31m │ " + style 0-0 fg=green + style 14-14 fg=bright-blue + style 16-76 bold + style 85-85 fg=bright-blue +22| "▌ [signal SIG\\│ │ " + style 0-0 fg=green + style 2-13 fg=red + style 14-14 fg=bright-blue + style 85-85 fg=bright-blue +23| "▌ │ › ● Unsafe option \\x1b]2;snapshot-controlled\\x07\\x09\\x7f\\x9b31m — Un │ " + style 0-0 fg=green + style 14-14 fg=bright-blue + style 16-16 fg=bright-blue inverse + style 17-17 inverse + style 18-18 fg=bright-blue inverse + style 19-78 inverse + style 79-83 fg=bright-black inverse + style 85-85 fg=bright-blue +24| " │ ↑↓ navigate • Enter select • C custom • Esc cancel │ " + style 14-14 fg=bright-blue + style 16-65 dim + style 85-85 fg=bright-blue +25| " Context · uns╰──────────────────────────────────────────────────────────────────────╯ " + style 1-13 dim + style 14-85 fg=bright-blue +26| " Unsafe context \\x1b]2;snapshot-controlled\\x07\\x09\\x7f\\x9b31m " + style 1-60 fg=bright-black +27| +28| " Prompt blocked: Unsafe policy \\x1b]2;snapshot-controlled\\x07\\x09\\x7f\\x9b31m " + style 1-75 fg=yellow +29| +30| " Unsafe turn error \\x1b]2;snapshot-controlled\\x07\\x09\\x7f\\x9b31m " + style 1-63 fg=red +31| +32| " Unsafe live error \\x1b]2;snapshot-controlled\\x07\\x09\\x7f\\x9b31m " + style 1-63 fg=red +33| +34| "Plan" + style 0-3 fg=bright-blue bold +35| " ● Unsafe todo \\x1b]2;snapshot-controlled\\x07\\x09\\x7f\\x9b31m" + style 2-2 fg=yellow +36| "────────────────────────────────────────────────────────────────────────────────────────────────────" + style 0-99 dim +37| " " + style 1-1 inverse +38| "────────────────────────────────────────────────────────────────────────────────────────────────────" + style 0-99 dim +39| "/workspace/project ↑0 ↓0 idle reasoning:on tools:compact" + style 0-24 dim + style 67-99 dim diff --git a/packages/ui/tui/tests/tui.snapshot.ts b/packages/ui/tui/tests/tui.snapshot.ts index 0967e7f64c..e0016184fd 100644 --- a/packages/ui/tui/tests/tui.snapshot.ts +++ b/packages/ui/tui/tests/tui.snapshot.ts @@ -29,6 +29,7 @@ const CHECKPOINTS = [ 'cordis-tools-pending', 'advanced-cards-collapsed', 'advanced-cards-expanded', + 'untrusted-controls', 'question-dialog', 'question-dialog-validation', 'surface-before-compaction', @@ -188,6 +189,9 @@ const ADVANCED_CARD_TOOLS: Record = { })), } +const CONTROL_PROBE = '\u001b]2;snapshot-controlled\u0007\t\u007f\u009b31m' +const DISPLAYED_CONTROL_PROBE = String.raw`\x1b]2;snapshot-controlled\x07\x09\x7f\x9b31m` + describe('TUI terminal-state snapshots', () => { it('pins an in-flight reasoning and Markdown stream', async () => { const harness = await setupSnapshot() @@ -298,6 +302,82 @@ describe('TUI terminal-state snapshots', () => { await disposeSnapshot(harness) }) + it('renders terminal controls as inert text across transcripts, tools, dialogs, diagnostics, and title', async () => { + const tools = { + unsafe: visualTool( + 'unsafe', + () => ({ + card: 'terminal', + title: `Unsafe title ${CONTROL_PROBE}`, + description: `Unsafe description ${CONTROL_PROBE}`, + cwd: `/unsafe/${CONTROL_PROBE}`, + }), + () => ({ + card: 'terminal', + output: `Unsafe output ${CONTROL_PROBE}`, + signal: `SIG${CONTROL_PROBE}`, + }), + ), + } + const harness = await setupSnapshot({ + tools, + config: { + welcome: `Unsafe welcome ${CONTROL_PROBE}`, + title: `Unsafe terminal title ${CONTROL_PROBE}`, + }, + beforeMount(session) { + appendUser(session, `Unsafe user ${CONTROL_PROBE}`) + appendAssistant(session, [ + { type: 'reasoning', text: `Unsafe reasoning ${CONTROL_PROBE}` }, + { type: 'text', text: `Unsafe assistant ${CONTROL_PROBE}` }, + ]) + appendToolCalls(session, [{ id: 'unsafe-1', name: 'unsafe', arguments: { value: CONTROL_PROBE } }]) + appendToolResult(session, 'unsafe-1', [{ type: 'text', text: `Unsafe fallback ${CONTROL_PROBE}` }]) + session.append('todo/write', { + todos: [{ content: `Unsafe todo ${CONTROL_PROBE}`, status: 'in_progress' }], + }) + session.append('context/message', { + content: [{ type: 'text', text: `Unsafe context ${CONTROL_PROBE}` }], + source: { kind: 'plugin', plugin: `unsafe-${CONTROL_PROBE}` }, + }, { surfaceOp: 'append' }) + session.append('prompt/blocked', { + content: [{ type: 'text', text: 'blocked' }], + source: { kind: 'user' }, + reason: `Unsafe policy ${CONTROL_PROBE}`, + }) + session.append('turn/end', { + turn: 7, + reason: { kind: 'error', step: 2, message: `Unsafe turn error ${CONTROL_PROBE}` }, + }) + }, + }, { columns: 100, rows: 34 }) + expect(harness.terminal.title).toContain(DISPLAYED_CONTROL_PROBE) + expect(harness.terminal.title).not.toContain('\u001b') + expect(harness.terminal.title).not.toContain('\u009b') + + const controller = new AbortController() + const beforeQuestion = harness.terminal.frames + const answer = harness.ctx.userInteraction.ask({ + questions: [{ + id: 'unsafe-question', + header: `Unsafe header ${CONTROL_PROBE}`, + question: `Unsafe question ${CONTROL_PROBE}`, + options: [{ label: `Unsafe option ${CONTROL_PROBE}`, description: `Unsafe detail ${CONTROL_PROBE}` }], + }], + signal: controller.signal, + }) + const rejected = expect(answer).rejects.toMatchObject({ code: 'ASK_ABORTED' }) + await harness.terminal.waitForFrame(beforeQuestion) + await renderAfter(harness, () => { + harness.ctx.emit('agent/error', harness.agent, 8, 3, new Error(`Unsafe live error ${CONTROL_PROBE}`)) + }) + await checkpoint('untrusted-controls', harness.terminal, { includeScrollback: true }) + + controller.abort() + await rejected + await disposeSnapshot(harness) + }) + it('pins a constrained multi-select question and its validation state', async () => { const harness = await setupSnapshot({ config: { diff --git a/packages/ui/tui/tests/tui.spec.ts b/packages/ui/tui/tests/tui.spec.ts index 5fc11cbe40..94a2447d5f 100644 --- a/packages/ui/tui/tests/tui.spec.ts +++ b/packages/ui/tui/tests/tui.spec.ts @@ -856,8 +856,8 @@ describe('terminal mounting', () => { ctx.agents.reportStartFailure(AgentId('other'), new Error('other failed')) expect(terminal.output).toBe('') expect(exit).not.toHaveBeenCalled() - ctx.agents.reportStartFailure(AgentId('main'), new Error('resume failed')) - expect(terminal.output).toBe('ui-tui: agent "main" failed to start: resume failed\n') + ctx.agents.reportStartFailure(AgentId('main'), new Error('resume \u001b]2;failure-controlled\u0007')) + expect(terminal.output).toBe('ui-tui: agent "main" failed to start: resume \\x1b]2;failure-controlled\\x07\n') expect(exit).toHaveBeenCalledWith(1) const session = ctx.sessions.create(SessionId('must-not-start')) From e8ea0c3b7e021789aca0aaa4201998b6a1958bd3 Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Sun, 19 Jul 2026 10:51:20 +0800 Subject: [PATCH 11/13] test(tui): make cwd coverage location-independent formatCwd has distinct branches for the home directory itself, a descendant of home, an unset cwd, and a path outside home. The descendant branch was exercised only indirectly because the test harness defaulted to process.cwd(), so the per-file coverage gate changed with the checkout location and failed in detached worktrees under /private/tmp even though every behavior assertion passed. Derive the home-root case from homedir() and add an explicit synthetic child with platform-aware path joins. Keep the unset and outside-home cases unchanged. The suite now drives every formatting branch regardless of where the repository is checked out, and the expected child form follows the host path separator on macOS, Linux, and Windows. --- packages/ui/tui/tests/tui.spec.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/ui/tui/tests/tui.spec.ts b/packages/ui/tui/tests/tui.spec.ts index 94a2447d5f..96f03647be 100644 --- a/packages/ui/tui/tests/tui.spec.ts +++ b/packages/ui/tui/tests/tui.spec.ts @@ -1,3 +1,5 @@ +import { homedir } from 'node:os' +import { join } from 'node:path' import { describe, expect, it, vi } from 'vitest' import { Context } from 'cordis' import type { Terminal } from '@earendil-works/pi-tui' @@ -341,8 +343,9 @@ describe('pi-tui chat lifecycle and transcript', () => { }) it('formats large token totals and cwd variants', async () => { + const home = homedir() const homeResult = await setup({ - cwd: process.env.HOME ?? process.cwd(), + cwd: home, beforeMount(session) { appendAssistant(session, [{ type: 'text', text: 'home' }], { inputTokens: 25_000, outputTokens: 10_000 }) }, @@ -350,6 +353,10 @@ describe('pi-tui chat lifecycle and transcript', () => { expect(homeResult.terminal.output).toContain('~ ↑25k ↓10k') await dispose(homeResult) + const childResult = await setup({ cwd: join(home, 'projects', 'dsh-tui') }) + expect(childResult.terminal.output).toContain(join('~', 'projects', 'dsh-tui')) + await dispose(childResult) + const unsetResult = await setup({ cwd: null }) expect(unsetResult.terminal.output).toContain('cwd unset') await dispose(unsetResult) From 548bbe7ae898383ababa3a849905b0642dc966e7 Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Sun, 19 Jul 2026 12:05:59 +0800 Subject: [PATCH 12/13] docs(tui): record dedicated terminal front-door decision Record the July 17 package split as an implemented bilingual feature RFC. The decision explains why TUI and stdio have separate terminal contracts, why concrete front-door selection belongs to composition, and how the TUI preserves existing session, tool-presentation, and lifecycle ownership. Link the deterministic dark, light, and animated demos from the RFC so the assets have an in-repository owner. Cross-link the feature and testing decisions from the package README, then regenerate the RFC index and bilingual consistency record. --- docs/rfc/INDEX.md | 1 + ...cated-full-screen-tui-front-door.i18n.yaml | 6 ++ ...17-dedicated-full-screen-tui-front-door.md | 58 +++++++++++++++++++ ...dedicated-full-screen-tui-front-door.zh.md | 58 +++++++++++++++++++ packages/ui/tui/README.md | 2 + 5 files changed, 125 insertions(+) create mode 100644 docs/rfc/implemented/feature/2026-07-17-dedicated-full-screen-tui-front-door.i18n.yaml create mode 100644 docs/rfc/implemented/feature/2026-07-17-dedicated-full-screen-tui-front-door.md create mode 100644 docs/rfc/implemented/feature/2026-07-17-dedicated-full-screen-tui-front-door.zh.md diff --git a/docs/rfc/INDEX.md b/docs/rfc/INDEX.md index 2a262fca6e..9daf5de83b 100644 --- a/docs/rfc/INDEX.md +++ b/docs/rfc/INDEX.md @@ -89,6 +89,7 @@ Generated by `pnpm run gen-rfc-index` from the RFC tree — never edit by hand; | [Session query relationship tracing](implemented/feature/2026-07-13-session-query-tracing.md) | 2026-07-13 | | [Optional time-context plugin](implemented/feature/2026-07-14-time-context-plugin.md) | 2026-07-14 | | [Durable per-step time context](implemented/feature/2026-07-16-durable-per-step-time-context.md) | 2026-07-16 | +| [Dedicated full-screen TUI front door](implemented/feature/2026-07-17-dedicated-full-screen-tui-front-door.md) | 2026-07-17 | ### Simplification diff --git a/docs/rfc/implemented/feature/2026-07-17-dedicated-full-screen-tui-front-door.i18n.yaml b/docs/rfc/implemented/feature/2026-07-17-dedicated-full-screen-tui-front-door.i18n.yaml new file mode 100644 index 0000000000..e7770a2753 --- /dev/null +++ b/docs/rfc/implemented/feature/2026-07-17-dedicated-full-screen-tui-front-door.i18n.yaml @@ -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-17-dedicated-full-screen-tui-front-door.md: dd4d7350a61b0f19fcc87658e4c0f9d8bbf2e998 +2026-07-17-dedicated-full-screen-tui-front-door.zh.md: 45e0926f87e1b73c378c195a46a6d40d13a69bfa diff --git a/docs/rfc/implemented/feature/2026-07-17-dedicated-full-screen-tui-front-door.md b/docs/rfc/implemented/feature/2026-07-17-dedicated-full-screen-tui-front-door.md new file mode 100644 index 0000000000..dd4d7350a6 --- /dev/null +++ b/docs/rfc/implemented/feature/2026-07-17-dedicated-full-screen-tui-front-door.md @@ -0,0 +1,58 @@ +# RFC: Dedicated full-screen TUI front door + +Status: implemented + +English | [中文](2026-07-17-dedicated-full-screen-tui-front-door.zh.md) + +## Problem + +The line-oriented `@deepseek-ai/dsh-stdio` front door works in pipes and ordinary terminals, but a full-screen coding interface must own raw input, differential screen drawing, cursor state, overlays, and terminal restoration. Combining those contracts in one UI plugin couples the pipe-safe path to a TTY-only lifecycle and makes it unclear which terminal behavior a composition selects. + +The interactive channel must remain a Cordis plugin over the same agent, session, tool, and user-interaction services as every other front door. It needs to resume durable history, follow compaction replacements, display tool-owned presentation, and restore the terminal on startup failure and disposal. A standalone chat application or a second agent composition would duplicate behavior outside the plugin graph. + +## Decision + +DeepSeek Harness ships [`@deepseek-ai/dsh-tui`](../../../../packages/ui/tui/README.md) as a dedicated Cordis plugin. It owns terminal input and presentation only; agent lifecycle, session persistence, tool execution, and the model-facing question tool remain separate composition entries. The plugin requires both stdin and stdout to be TTYs and fails instead of silently changing to line-oriented behavior. + +The app layer selects a concrete terminal front door before mounting it. `@deepseek-ai/dsh-stdio-demo` can resolve `auto` from the two process streams, while the `coding-agent` and `tui-agent` leaves explicitly select readline and TUI respectively. The TUI leaf reuses the coding agent's backend and tool composition through an asserted include patch, so the three runnable agent leaves remain symmetric without duplicating deployment choices. + +The selected front door receives the exact generated or resumed `SessionId` used by the pre-created agent. It mounts before the agent composition, waits for the matching root agent, and enters full-screen mode only after that agent exists. A matching `agent-loop/config-start-failed` event is therefore reported before screen takeover and exits with status 1. + +### Session projection and interaction + +The TUI rebuilds the transcript from the active `session.surface` and reprojects it whenever an event carries a `surfaceOp`, so resumed and compacted history matches the model-visible conversation. It renders Markdown text and reasoning, token totals, the latest `todo/write` plan, and tool cards produced through each tool definition's `presentCall` and `presentResult` methods. Pending chunks and tool calls update the same components that completed events settle. + +Editor input calls `agent.send()` while idle and `agent.steer()` while a turn is running. Cancellation, reasoning visibility, tool-card expansion, redraw, transcript clearing, and exit are terminal-only controls. The plugin registers the shared `userInteraction` provider and presents questions as queued keyboard overlays; agent behavior and answer logging remain owned by their existing services. + +### Terminal ownership + +Before model output, session data, tool presentation, questions, configuration, or diagnostics reach pi-tui or the terminal title, `displayText()` renders C0 and C1 controls other than line feeds as visible hexadecimal escapes. Only the TUI and pi-tui create ANSI control sequences. + +The built-in palette uses standard 16-color ANSI foregrounds and SGR attributes, keeps body text and backgrounds at terminal defaults, and uses reverse video for selection. Host terminals therefore remap the interface for light and dark themes without a TUI-specific theme setting; `color: false` removes styling. + +### Shipped interface + +The deterministic scripted demo shows prompt submission, streamed model output, and the ANSI palette under dark and light terminal themes without a network dependency. + +![Scripted TUI interaction](../../../assets/tui/jokes-dynamic.gif) + +| Dark terminal | Light terminal | +| --- | --- | +| ![TUI on a dark terminal](../../../assets/tui/jokes-dark.png) | ![TUI on a light terminal](../../../assets/tui/jokes-light.png) | + +## Verification + +The implemented [TUI terminal-state snapshot RFC](../testing/2026-07-18-tui-terminal-state-snapshots.md) owns the four-layer verification contract: direct behavior tests, transient semantic terminal snapshots, recorded JSONL journeys through production tools, and Loader/PTY smoke tests. The package README owns configuration, commands, model-visible effects, and current limitations. + +## Alternatives considered + +- **Keep readline and full-screen modes inside `@deepseek-ai/dsh-stdio`** — rejected because line-oriented output and differential TTY rendering have different dependencies, input rules, logging ownership, and teardown obligations. Separate packages keep the pipe-safe contract small and explicit. +- **Let the TUI plugin silently downgrade when either stream is not a TTY** — rejected because a fallback hides deployment mistakes and changes interaction semantics. The app bundle may select a front door with `auto`; an explicitly mounted TUI fails loud. +- **Keep TUI wiring and tests under the readline `coding-agent` leaf** — rejected because one leaf would represent two distinct front doors and break symmetry with `acp-agent`. A dedicated `tui-agent` leaf owns TUI overlays and tests while reusing the coding agent's backend composition. + +## Consequences + +- Interactive terminal work gains a stateful Markdown, card, plan, and question interface without changing the line-oriented protocol used by pipes and automation. +- The TUI carries a pi-tui dependency and a strict TTY requirement; non-TTY deployments select `@deepseek-ai/dsh-stdio` at composition time. +- Session projection makes resume and compaction consistent with the durable conversation, but one configured session owns the transcript and editor. +- Tool packages extend terminal cards through their existing presentation methods without adding tool-specific branches to the TUI. diff --git a/docs/rfc/implemented/feature/2026-07-17-dedicated-full-screen-tui-front-door.zh.md b/docs/rfc/implemented/feature/2026-07-17-dedicated-full-screen-tui-front-door.zh.md new file mode 100644 index 0000000000..45e0926f87 --- /dev/null +++ b/docs/rfc/implemented/feature/2026-07-17-dedicated-full-screen-tui-front-door.zh.md @@ -0,0 +1,58 @@ +# RFC: 独立的全屏 TUI 入口 + +Status: implemented + +[English](2026-07-17-dedicated-full-screen-tui-front-door.md) | 中文 + +## 问题 + +逐行输出的 `@deepseek-ai/dsh-stdio` 入口适用于管道和普通终端,但全屏编码界面必须负责原始输入、差分绘制、光标状态、浮层和终端恢复。把这两类契约合并到一个 UI 插件中,会迫使管道安全路径依赖仅适用于 TTY 的生命周期,也使组合无法明确表达所选终端行为。 + +交互通道必须继续作为 Cordis 插件,使用与其他入口相同的 agent(智能体)、会话、工具和用户交互服务。它需要恢复持久历史、跟随压缩替换、显示工具自有的呈现内容,并在启动失败和资源释放时恢复终端。独立聊天应用或第二套 agent 组合会在插件图之外重复实现这些行为。 + +## 决策 + +DeepSeek Harness 将 [`@deepseek-ai/dsh-tui`](../../../../packages/ui/tui/README.md) 作为独立的 Cordis 插件交付。该插件只负责终端输入与呈现;agent 生命周期、会话持久化、工具执行以及模型可见的提问工具仍由不同组合项负责。插件要求 stdin 和 stdout 均为 TTY;条件不满足时会失败,不会静默切换为逐行输出。 + +应用组合层在挂载前选择具体的终端入口。`@deepseek-ai/dsh-stdio-demo` 可以根据两个进程流通过 `auto` 作出选择,`coding-agent` 和 `tui-agent` 叶节点则分别明确选择 readline 与 TUI。TUI 叶节点通过带断言的 include patch 复用 coding agent 的后端和工具组合,使三个可运行的 agent 叶节点保持对称,同时避免重复部署选项。 + +所选入口接收预创建 agent 使用的同一个新建或恢复 `SessionId`。入口先于 agent 组合挂载,等待相符的根 agent 出现,然后才进入全屏模式。因此,相符的 `agent-loop/config-start-failed` 事件会在接管屏幕前报告,并以状态码 1 退出。 + +### 会话投影与交互 + +TUI 从活跃的 `session.surface` 重建 transcript(文本记录),并在事件携带 `surfaceOp` 时重新投影,因此恢复或压缩后的历史与模型可见会话保持一致。TUI 渲染 Markdown 文本与推理、token 用量、最新 `todo/write` 计划,以及各工具定义通过 `presentCall` 和 `presentResult` 方法生成的工具卡片。进行中的分片与工具调用会更新同一组组件,随后由完成事件收束状态。 + +agent 空闲时,编辑器输入调用 `agent.send()`;轮次运行中则调用 `agent.steer()`。取消、推理显隐、工具卡片展开、重绘、清空 transcript 和退出都只是终端控制。插件注册共享的 `userInteraction` 提供方,以排队的键盘浮层呈现问题;agent 行为和答案日志仍由既有服务负责。 + +### 终端所有权 + +在模型输出、会话数据、工具呈现、问题、配置或诊断信息进入 pi-tui 或终端标题前,`displayText()` 会把换行之外的 C0 和 C1 控制字符显示为十六进制转义文本。只有 TUI 和 pi-tui 可以生成 ANSI 控制序列。 + +内置配色仅使用标准 16 色 ANSI 前景色和 SGR 属性,正文文字和背景沿用终端默认值,选中项使用反显。因此,宿主终端可以直接按浅色或深色主题重映射界面,无需 TUI 专用主题设置;`color: false` 会移除样式。 + +### 已交付界面 + +确定性的脚本化演示无需网络即可展示提示词提交、流式模型输出,以及 ANSI 配色在深色和浅色终端主题下的效果。 + +![脚本化 TUI 交互](../../../assets/tui/jokes-dynamic.gif) + +| 深色终端 | 浅色终端 | +| --- | --- | +| ![深色终端中的 TUI](../../../assets/tui/jokes-dark.png) | ![浅色终端中的 TUI](../../../assets/tui/jokes-light.png) | + +## 验证 + +已实现的 [TUI 终端状态快照 RFC](../testing/2026-07-18-tui-terminal-state-snapshots.md) 规定四层验证契约:直接行为测试、瞬态语义终端快照、通过生产工具执行的已录制 JSONL 流程,以及 Loader/PTY 冒烟测试。包(package)README 负责记录配置、命令、模型可见效果和当前限制。 + +## 曾考虑的替代方案 + +- **把 readline 与全屏模式都保留在 `@deepseek-ai/dsh-stdio` 中**:不予采纳,因为逐行输出和差分 TTY 渲染具有不同的依赖、输入规则、日志所有权和资源清理义务。拆分为独立包可以让管道安全契约保持精简、明确。 +- **当任一进程流不是 TTY 时,让 TUI 插件静默降级**:不予采纳,因为回退会掩盖部署错误并改变交互语义。应用包可以通过 `auto` 选择入口;明确挂载的 TUI 会快速失败。 +- **把 TUI 接线与测试保留在 readline `coding-agent` 叶节点下**:不予采纳,因为一个叶节点会代表两个不同入口,也会破坏它与 `acp-agent` 的对称性。独立的 `tui-agent` 叶节点负责 TUI 浮层和测试,同时复用 coding agent 的后端组合。 + +## 后果 + +- 交互式终端获得带状态的 Markdown、卡片、计划和提问界面,同时不会改变管道与自动化使用的逐行协议。 +- TUI 会引入 pi-tui 依赖并严格要求 TTY;非 TTY 部署在组合时选择 `@deepseek-ai/dsh-stdio`。 +- 会话投影使恢复和压缩与持久会话保持一致,但只有一个已配置会话拥有 transcript 和编辑器。 +- 工具包通过既有呈现方法扩展终端卡片,无需在 TUI 中增加工具专用分支。 diff --git a/packages/ui/tui/README.md b/packages/ui/tui/README.md index ff1f536754..ce7e9ac2fc 100644 --- a/packages/ui/tui/README.md +++ b/packages/ui/tui/README.md @@ -2,6 +2,8 @@ The interactive terminal front door for DeepSeek Harness agents, built on [`@earendil-works/pi-tui`](https://www.npmjs.com/package/@earendil-works/pi-tui). It requires stdin and stdout TTYs; scripts and Loader pipes should compose [`@deepseek-ai/dsh-stdio`](../stdio/README.md) instead. +The implemented [TUI feature RFC](../../../docs/rfc/implemented/feature/2026-07-17-dedicated-full-screen-tui-front-door.md) owns the front-door decision and shipped interface; the [terminal-state snapshot RFC](../../../docs/rfc/implemented/testing/2026-07-18-tui-terminal-state-snapshots.md) owns its verification strategy. + This package owns interactive terminal presentation and input only. It injects `agents`, `tools`, and `userInteraction`, then drives an agent created or resumed by app or developer code. Agent lifecycle, persistence, and the model-facing [`ask_user_question`](../tool-ask-user/README.md) tool remain separate composition entries. The TUI rebuilds resumed history from the active session surface, renders Markdown responses and reasoning, applies each tool's `presentCall` / `presentResult` intent to terminal, diff, or generic cards, keeps the latest `todo/write` plan above the editor, and presents `ctx.userInteraction` questions as keyboard-driven overlays. Surface replacement events rebuild the transcript so compacted history does not reappear. From aa21c0b83f882397511b86217d4a09028d92c4be Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Sun, 19 Jul 2026 12:41:38 +0800 Subject: [PATCH 13/13] docs(tui): remove demo image assets Remove the animated, dark, and light terminal captures and their bilingual RFC section. The implemented RFC retains the durable front-door decision and verification contract without carrying binary demo artifacts that need visual maintenance. Tighten the package README ownership wording and refresh the bilingual consistency record so no repository link or prose claim refers to the removed files. --- docs/assets/tui/jokes-dark.png | Bin 66598 -> 0 bytes docs/assets/tui/jokes-dynamic.gif | Bin 78162 -> 0 bytes docs/assets/tui/jokes-light.png | Bin 70844 -> 0 bytes ...dicated-full-screen-tui-front-door.i18n.yaml | 4 ++-- ...7-17-dedicated-full-screen-tui-front-door.md | 10 ---------- ...7-dedicated-full-screen-tui-front-door.zh.md | 10 ---------- packages/ui/tui/README.md | 2 +- 7 files changed, 3 insertions(+), 23 deletions(-) delete mode 100644 docs/assets/tui/jokes-dark.png delete mode 100644 docs/assets/tui/jokes-dynamic.gif delete mode 100644 docs/assets/tui/jokes-light.png diff --git a/docs/assets/tui/jokes-dark.png b/docs/assets/tui/jokes-dark.png deleted file mode 100644 index 876ea9c24c9422e76aa839bdead1edd6cb6df22f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 66598 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 a`$p*P1=7cj$ra$88&Gk1u|iQjum1%LHms5W diff --git a/docs/assets/tui/jokes-dynamic.gif b/docs/assets/tui/jokes-dynamic.gif deleted file mode 100644 index f0036572e8a2eeb0721d6929cafe404e0f6099ad..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 78162 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�%^@#dq}3j{IDaY(s{Y&2k(r-F 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