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.
This commit is contained in:
Turtle
2026-07-17 12:01:37 +08:00
parent 4139e093dd
commit 1119c537d0
49 changed files with 2808 additions and 93 deletions

View File

@@ -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.