Bläddra i källkod

refactor(ui): fold the stdio UI helper into the stdio app

The readline UI lives inside @deepseek-ai/dsh-stdio-agent as the
in-package stdio-chat module; the packages/support/ui-stdio package is
gone. The app's front-door cluster always includes this UI and nothing
else composes it, so the boundary bought manifest/tsconfig/module-graph/
README/publint surface for a helper that is not independently
swappable — and a product app no longer depends on a support package
documented as not-product-surface.

createStdioChat, the StdioRuntime test seam, and both unit suites moved
verbatim (imports rewired to the module path); the named
name/inject/Config/apply export shape stays, being the contract the
app's ctx.plugin mount consumes. Coverage stays per-file 100%; the
built-bin smoke under plain node and both keyless Loader-path smokes
prove the published artifact and the demos end-to-end.

Implements docs/rfc/implemented/simplification/2026-07-04-fold-stdio-ui-helper.md
(moved from proposed/ and amended to the shipped shape).
Tianyi Cui 2 månader sedan
förälder
incheckning
205f7cd04d

+ 0 - 2
AGENTS.md

@@ -114,8 +114,6 @@ packages/    Harness packages, grouped by role at packages/<group>/<pkg>/.
                     acp bridge, NO stdout logger + a bin (the demo:acp front door)
   support/        dev/test/example infrastructure (lower compat expectations)
     invariants/     dev-mode event-contract invariants + session-log freeze
-    ui-stdio/       minimal stdio (readline) UI plugin: renders agent/* events,
-                    feeds stdin lines to the agent (shared by the demos)
     llm-replay/     record/replay adapter: short-circuits llm/stream from a
                     recorded session JSONL (keyless snapshot tests)
     subagent-mock/  scripted SubagentProvider for deterministic seam/tool tests

+ 1 - 6
docs/module-graph.md

@@ -48,9 +48,6 @@ graph TD
   tools --> agent
   tools --> llm
   tools --> system-prompt
-  ui-stdio --> agent
-  ui-stdio --> llm
-  ui-stdio --> session
   acp --> agent
   acp --> llm
   acp --> session
@@ -121,7 +118,6 @@ graph TD
   stdio-agent --> agent-core
   stdio-agent --> session
   stdio-agent --> session-persistence-jsonl
-  stdio-agent --> ui-stdio
   subagent-fork --> agent
   subagent-fork --> session
   subagent-fork --> subagent
@@ -158,7 +154,6 @@ graph TD
 | `session-persistence-jsonl` | `session`, `session-persistence` |
 | `session-persistence-sqlite` | `session`, `session-persistence` |
 | `tools` | `agent`, `llm`, `system-prompt` |
-| `ui-stdio` | `agent`, `llm`, `session` |
 | `acp` | `agent`, `llm`, `session`, `session-persistence`, `tools` |
 | `agent-loop` | `agent`, `llm`, `session`, `session-persistence`, `system-prompt`, `tools` |
 | `hooks-codex` | `agent`, `hook-protocol`, `llm`, `session`, `tools` |
@@ -174,6 +169,6 @@ graph TD
 | `subagent-mock` | `agent`, `llm`, `subagent` |
 | `tool-subagent` | `agent`, `llm`, `subagent`, `tools` |
 | `acp-agent` | `acp`, `agent-core`, `session-persistence-jsonl` |
-| `stdio-agent` | `agent`, `agent-core`, `session`, `session-persistence-jsonl`, `ui-stdio` |
+| `stdio-agent` | `agent`, `agent-core`, `session`, `session-persistence-jsonl` |
 | `subagent-fork` | `agent`, `session`, `subagent`, `subagent-inprocess` |
 | `subagent-spawn` | `subagent`, `subagent-inprocess` |

+ 1 - 1
docs/rfc/README.md

@@ -55,7 +55,6 @@ Do NOT write one for a mechanical or local choice (a variable name, a one-file r
 | [Drop the `image` content block until a path can honor it](proposed/simplification/2026-07-04-drop-image-content-block.md) | 2026-07-04 |
 | [Drop `GenerateOptions.prefill` and `ToolSchema.strict` — request knobs with no working end-to-end path](proposed/simplification/2026-07-04-drop-inert-request-knobs.md) | 2026-07-04 |
 | [Drop the unconsumed web observation surface — the `providers-change` event and the status methods](proposed/simplification/2026-07-04-drop-unconsumed-web-observation-surface.md) | 2026-07-04 |
-| [Fold the stdio UI helper into the stdio app](proposed/simplification/2026-07-04-fold-stdio-ui-helper.md) | 2026-07-04 |
 | [Prune dead core-spine surface — `SurfaceManager.invalidate()`, the loop-internal exports, `ToolExecutionResult.callId`](proposed/simplification/2026-07-04-prune-dead-core-spine-surface.md) | 2026-07-04 |
 | [Prune producer-less vocabulary variants (block cache hints, the `agent` message source, the `continuation` turn trigger)](proposed/simplification/2026-07-04-prune-producerless-vocabulary-variants.md) | 2026-07-04 |
 | [Prune write-only fields and a dead routing knob from the fs seam](proposed/simplification/2026-07-04-prune-write-only-fs-surface.md) | 2026-07-04 |
@@ -120,6 +119,7 @@ Do NOT write one for a mechanical or local choice (a variable name, a one-file r
 | [Stop mirroring durable boundaries as agent events](implemented/simplification/2026-06-20-remove-agent-boundary-mirror-events.md) | 2026-06-20 |
 | [Split the filesystem seam — provider text mutations plus the `dsh-fs-policy` plugin](implemented/simplification/2026-06-26-fsspec-style-fs-seam.md) | 2026-06-26 |
 | [Stop mirroring the token stream as an agent event](implemented/simplification/2026-07-02-remove-stream-chunk-mirror.md) | 2026-07-02 |
+| [Fold the stdio UI helper into the stdio app](implemented/simplification/2026-07-04-fold-stdio-ui-helper.md) | 2026-07-04 |
 
 ### Architecture
 

+ 24 - 0
docs/rfc/implemented/simplification/2026-07-04-fold-stdio-ui-helper.md

@@ -0,0 +1,24 @@
+# RFC: Fold the stdio UI helper into the stdio app
+
+Status: implemented
+
+## Problem
+
+The readline UI was a whole package (`@deepseek-ai/dsh-ui-stdio` under `packages/support/`) whose only runtime importer was the app package `@deepseek-ai/dsh-stdio-agent`. The examples reach the readline UI by loading the app, never by composing the helper themselves; every other repo reference was mechanical or descriptive surface that existed BECAUSE the package boundary existed — manifest and tsconfig entries, generated module-graph rows, dependency-graph and README rows, and doc comments naming the package. The ui group README recorded the support placement rationale ("exists chiefly for the examples and the coverage gate — `ui/` is reserved for surfaces shipped as product"), which left a standing tension: a shipped product app depending on a support package documented as NOT product surface.
+
+The boundary bought package metadata, workspace and tsconfig references, module-graph rows, README entries, and publint surface for a helper that is not independently swappable: the stdio app's front-door cluster always includes the readline UI, and nothing else can meaningfully consume it.
+
+## Decision
+
+The helper lives inside `@deepseek-ai/dsh-stdio-agent` as the in-package `stdio-chat` module (`packages/ui/stdio-agent/src/stdio-chat.ts`): `createStdioChat`, its `StdioRuntime` test seam, and its unit tests (`packages/ui/stdio-agent/tests/stdio-chat.spec.ts`, `readline.spec.ts`) moved with it, so EOF handling, rendering, disposal, and piped-vs-TTY behavior stay unit-covered under the per-file coverage gate without hijacking process globals. The module keeps the named `name`/`inject`/`Config`/`apply` export shape — the contract the app's `ctx.plugin(uiStdio, …)` mount consumes — and the keyless Loader-path smokes in `examples/echo-agent` and `examples/coding-agent` keep guarding the app's export shape end-to-end.
+
+The `packages/support/ui-stdio` package is gone: manifest, tsconfig references, module-graph rows, and README rows deleted; the doc comments that named the package (the example e2e module docs, `packages/README.md`, the support and todo READMEs, [the ui group README](../../../../packages/ui/README.md)) describe the in-package module.
+
+## Why not promote it to `ui/` instead?
+
+Promotion would have resolved the support-vs-product mismatch while keeping the boundary — the right call only if the readline UI were an independently swappable integration or had a second composer, and the consumer census said neither. The structured ACP bridge stays its own package because it is the product protocol surface with its own contract and snapshot tiers; the readline helper is scaffolding for one app's front door. Re-extraction stays cheap pre-release: if a second product app wants the readline UI, split it back out then, with that consumer shaping the package contract.
+
+## Consequences
+
+- The stdio app owns its whole front door; a leaf `cordis.yml` still loads one app package and nothing changed shape for the demos.
+- A future standalone terminal UI that wants the helper as a package reintroduces it with that second consumer, rather than the repo keeping a boundary for hypothetical reuse.

+ 0 - 27
docs/rfc/proposed/simplification/2026-07-04-fold-stdio-ui-helper.md

@@ -1,27 +0,0 @@
-# RFC: Fold the stdio UI helper into the stdio app
-
-Status: proposed
-
-## Problem
-
-`@deepseek-ai/dsh-ui-stdio` is a whole package whose only runtime importer is the app package `@deepseek-ai/dsh-stdio-agent` (`packages/ui/stdio-agent/src/index.ts`). The examples reach the readline UI by loading the app, never by composing the helper themselves; every other repo reference is mechanical or descriptive surface that exists BECAUSE the package boundary exists — manifest and tsconfig entries, generated module-graph rows, dependency-graph and README rows, and doc comments naming the package. [The ui group README](../../../../packages/ui/README.md) records the placement rationale — the helper "exists chiefly for the examples and the coverage gate — `ui/` is reserved for surfaces shipped as product" — which leaves a standing tension: a shipped product app depends on a support package documented as NOT product surface.
-
-The boundary buys package metadata, workspace and tsconfig references, module-graph rows, README entries, and publint surface for a helper that is not independently swappable: the stdio app's front-door cluster always includes the readline UI, and nothing else can meaningfully consume it.
-
-## Proposal
-
-Fold the helper into `@deepseek-ai/dsh-stdio-agent`: move `createStdioChat`, its `StdioRuntime` test seam, and its unit tests into `packages/ui/stdio-agent`; delete the `packages/support/ui-stdio` package with its manifest, references, module-graph rows, and README rows; update every reference that names the package (the example e2e module docs, `packages/README.md`, the support and todo README rows, the stdio-agent README, the ui group README, tsconfig references, the generated module graph). Keep the runtime seam so EOF handling, rendering, disposal, and piped-vs-TTY behavior stay unit-covered without hijacking process globals; the keyless Loader-path smokes keep guarding the export shape end-to-end.
-
-## Why not promote it to `ui/` instead?
-
-Promotion would resolve the support-vs-product mismatch while keeping the boundary — the right call only if the readline UI were an independently swappable integration or had a second composer, and the consumer census says neither. The structured ACP bridge stays its own package because it is the product protocol surface with its own contract and snapshot tiers; the readline helper is scaffolding for one app's front door. Re-extraction stays cheap pre-release: if a second product app wants the readline UI, split it back out then, with that consumer shaping the package contract.
-
-## Acceptance criteria
-
-- `packages/support/ui-stdio` no longer exists; the helper and its tests live in `packages/ui/stdio-agent`; no reference to the deleted package remains outside RFC history.
-- The stdio app still renders transcript events, handles stdin lines and EOF, renders todo checklists, and disposes readline listeners under HMR; the echo/coding keyless smokes still boot through the real Loader path and guard the export shape.
-- Manifests, tsconfig references, the generated module graph, and docs are updated; `pnpm run test:coverage`, `pnpm run test:snapshot`, `pnpm run doc-sync`, `pnpm run build`, and `pnpm run hygiene` pass.
-
-## Risks
-
-A future standalone terminal UI may want the helper as a package again — reintroduce it with that second consumer rather than keeping the boundary for hypothetical reuse. Moving tests risks blurring app-composition tests with UI-rendering tests; keeping the runtime seam and the colocated unit tests avoids that.

+ 2 - 2
examples/coding-agent/tests/keyless-smoke.e2e.ts

@@ -9,8 +9,8 @@ import { afterEach, describe, expect, it } from 'vitest'
  * Keyless Loader-path smoke for examples/coding-agent: boot the REAL example
  * through the `@deepseek-ai/dsh-stdio-agent` bin against its `cordis.yml` (the
  * cordis Loader, `unwrapExports`, the full plugin tree incl. the
- * `@deepseek-ai/dsh-agent-core` bundle and the extracted
- * `@deepseek-ai/dsh-ui-stdio`), then close stdin with no prompt and assert the
+ * `@deepseek-ai/dsh-agent-core` bundle and the app's in-package readline UI
+ * module), then close stdin with no prompt and assert the
  * ready banner + a clean exit.
  *
  * No prompt is ever sent, so the model is NEVER called — this is why it runs

+ 1 - 1
examples/echo-agent/tests/echo.e2e.ts

@@ -13,7 +13,7 @@ import { afterEach, describe, expect, it } from 'vitest'
  *
  * This is the guard the per-file unit suite structurally cannot be: it drives
  * the `@deepseek-ai/dsh-stdio-agent` app plugin, the `@deepseek-ai/dsh-agent-core`
- * bundle it loads, the extracted `@deepseek-ai/dsh-ui-stdio` plugin, AND the
+ * bundle it loads, the app's in-package readline UI module, AND the
  * example-local `mock-llm.ts` / `echo-tool.ts` through their REAL load path, so
  * a broken plugin export shape (a stray `export default` that `unwrapExports`
  * would collapse, dropping `inject`/`Config`) fails here even though hand-mounted

+ 1 - 3
packages/README.md

@@ -53,7 +53,6 @@ dsh-llm-pi-ai     ← dsh-llm                        (pi-ai-backed adapter)
 dsh-agent-loop    ← dsh-llm, dsh-session, dsh-session-persistence, dsh-system-prompt, dsh-tools, dsh-agent
 dsh-invariants    ← dsh-llm, dsh-session, dsh-agent (dev-mode contract checks)
 dsh-acp           ← dsh-agent, dsh-llm, dsh-session, dsh-session-persistence, dsh-tools  (ACP JSON-RPC bridge)
-dsh-ui-stdio      ← dsh-agent, dsh-llm, dsh-session (stdio readline UI plugin)
 dsh-llm-replay    ← dsh-llm, dsh-session            (record/replay adapter for keyless snapshot tests)
 dsh-subagent      ← dsh-agent, dsh-llm, dsh-tools    (abstract subagent provider-registry seam)
 dsh-subagent-inprocess ← dsh-subagent, dsh-agent, dsh-session, dsh-llm  (shared in-process run driver)
@@ -64,7 +63,7 @@ dsh-subagent-acp  ← dsh-subagent, dsh-agent, dsh-llm, @agentclientprotocol/sdk
 dsh-tool-subagent ← dsh-subagent, dsh-tools, dsh-agent, dsh-llm (model-facing delegation tool)
 dsh-tool-todo     ← dsh-tools, dsh-agent, dsh-session  (model-facing todo_write tool; whole list on the session log)
 dsh-agent-core    ← timer, dsh-llm, dsh-session, dsh-system-prompt, dsh-tools, dsh-agent, dsh-invariants, dsh-tool-bash, dsh-agent-loop  (the providerless spine, as one bundle plugin)
-dsh-stdio-agent   ← dsh-agent-core, dsh-ui-stdio, dsh-session-persistence-jsonl, dsh-agent, dsh-session  (stdio chat APP + bin)
+dsh-stdio-agent   ← dsh-agent-core, dsh-session-persistence-jsonl, dsh-agent, dsh-session  (stdio chat APP + readline UI + bin)
 dsh-acp-agent     ← dsh-agent-core, dsh-acp, dsh-session-persistence-jsonl     (ACP server APP + bin)
 ```
 
@@ -105,7 +104,6 @@ The rule: **extension** plugins depend on interfaces, never on the concrete loop
 | `acp/` | `ui` | Agent Client Protocol bridge: serves the agent to an ACP editor over JSON-RPC stdio | (drives `ctx.agents`/`ctx.sessions`) |
 | `stdio-agent/` | `ui` | Terminal stdio chat APP: agent-core spine + console logger + readline UI + a pre-created `main` agent, with a `bin` | (composition + `bin`) |
 | `acp-agent/` | `ui` | ACP server APP: agent-core spine + JSONL persistence + the `acp` bridge (no stdout logger), with a `bin` | (composition + `bin`) |
-| `ui-stdio/` | `support` | Minimal stdio (readline) UI plugin: renders `agent/*` events, feeds stdin lines to the agent | (drives `ctx.agents`) |
 | `llm-replay/` | `support` | Record/replay adapter: short-circuits `llm/stream` with chunks from a recorded session JSONL (keyless snapshot tests) | (listens on `llm/stream`) |
 | `subagent/` | `subagent` | Abstract subagent seam: named-provider registry for delegating to child agents | `ctx.subagents` |
 | `subagent-inprocess/` | `subagent` | Shared in-process subagent run driver used by spawn/fork; pure library, registers nothing | (none) |

+ 1 - 2
packages/support/README.md

@@ -5,8 +5,7 @@ Packages that exist to serve development, testing, and the examples rather than
 | Package | Role | ctx key |
 |---|---|---|
 | `invariants/` | Dev-mode event-contract invariants + session-log freeze | (listens on `session/*`, `agent/*`) |
-| `ui-stdio/` | Minimal stdio (readline) UI plugin: renders `agent/*` events, feeds stdin lines to the agent | (drives `ctx.agents`) |
 | `llm-replay/` | Record/replay adapter: short-circuits `llm/stream` from a recorded session JSONL (keyless snapshot tests) | (listens on `llm/stream`) |
 | `subagent-mock/` | Scripted `SubagentProvider` for deterministic seam/tool tests | (registers on `ctx.subagents`) |
 
-`invariants` runs only in dev mode (contract checks, not runtime behavior). `ui-stdio` and `llm-replay` were extracted from the examples for reuse and to bring them under the per-file coverage gate; they back the demos and the snapshot test tier. `subagent-mock` exercises the real `ctx.subagents` load path without a model or child agent. A package graduates OUT of `support/` into a product group only when it gains documented product consumers.
+`invariants` runs only in dev mode (contract checks, not runtime behavior). `llm-replay` backs the demos and the snapshot test tier under the per-file coverage gate. `subagent-mock` exercises the real `ctx.subagents` load path without a model or child agent. A package graduates OUT of `support/` into a product group only when it gains documented product consumers.

+ 0 - 44
packages/support/ui-stdio/README.md

@@ -1,44 +0,0 @@
-# @deepseek-ai/dsh-ui-stdio
-
-A minimal stdio (readline) UI, as a plugin. It reads lines from stdin and feeds them to an agent (`send` when idle, `steer` while a turn is running), and renders that agent's streamed output and tool activity to stdout. A UI is "just a plugin" here — it consumes the `session/event` transcript feed plus a few `agent/*` control events (`agent/status`, `agent/created`/`agent/disposed`) and the `agents` service (`inject: ['agents']`), so the same plugin drives any example or product surface.
-
-This is a **convenience REPL for local testing and the demos, not a product surface** — its observable behavior is free to change. It is deliberately NOT treated as a load-bearing consumer when weighing whether a live event/API must exist: the boundary mirror events were removed precisely because "ui-stdio renders from them" is not a product constraint (it was migrated to `session/event`). The real product surfaces are the ACP bridge (`dsh-acp`) and the app packages.
-
-This package consolidates what were two near-identical copies under `examples/echo-agent` and `examples/coding-agent`. The coding copy was a superset; this package IS that superset — dimmed chain-of-thought rendering plus robust piped-stdin EOF handling — with the per-consumer differences moved into `Config`.
-
-## Config
-
-| Key | Type | Default | Notes |
-|---|---|---|---|
-| `welcome` | string | `'ready.'` | Banner printed once on start, before the first `> ` prompt. |
-| `agent` | string | `'main'` | Id of the agent that stdin **drives** (`send`/`steer`) and whose `agent/status` gates the EOF exit. Rendering is **not** scoped by it — see below. |
-
-```yaml
-- id: ui-stdio
-  name: '@deepseek-ai/dsh-ui-stdio'
-  config:
-    welcome: 'agent REPL ready. Give it a coding task.'
-```
-
-## Rendering
-
-Rendering is **global** — every agent's events are written to stdout, not just `config.agent`'s. `config.agent` scopes only *input* (which agent stdin drives) and the EOF-exit gate; the single-agent demos this serves have just one agent, so the distinction is moot for them. (A multi-agent UI that needs per-agent panes would filter these handlers by the agent argument — deliberately out of scope here.)
-
-- `session/event` — the durable transcript feed drives ALL rendering, from a single listener so `inReasoning` transitions stay deterministic in append order: `assistant/chunk` writes the model's `text-delta` verbatim and wraps `reasoning-delta` in the dim SGR (`\x1B[2m … \x1B[0m`) so the chain-of-thought is visually subordinate to the answer (inert when no `reasoning-delta` chunks arrive, e.g. a mock model); `turn/start` prints a `[<agent> turn N]` header (the short agent label comes from a session-id→agent-id map seeded from `ctx.agents.list()` at install and kept live via `agent/created`/`agent/disposed`, since the turn event carries only the turn number); `turn/end` prints the trailing `> ` prompt; `tool/call` renders `[tool call] name(args)`; `tool/result` renders the joined text blocks as `[tool result] …`; and `todo/write` renders a glyphed checklist.
-
-## The I/O seam
-
-The production entry point `apply(ctx, config)` binds the real `process` streams. The testable core is `createStdioChat(ctx, config, runtime)`, where `runtime: StdioRuntime` supplies `input` / `output` / `exit`. This seam is deliberately **not** part of the serializable `Config` (streams and functions do not belong in YAML config); it exists so the render, EOF, and disposal branches can be exercised with fakes instead of hijacking globals.
-
-## Piped-stdin exit
-
-On stdin EOF the plugin exits the process, but carefully:
-
-- **No work submitted** (empty stdin, blank-only lines): exit immediately — no turn will ever start, so there is nothing to wait for. Gating on an observed `running` here would hang forever.
-- **Work submitted**: exit the next time the agent settles to `idle` *after* having been observed `running`. `agent.send()` does not synchronously flip status to `running`, so requiring an observed `running` first (`sawRunning`) avoids exiting in the gap before the turn starts and dropping work; and the loop batches several queued messages into one turn, so the exit keys off the idle transition rather than counting sends.
-
-Disposal (HMR or fiber teardown) closes the readline interface, which also fires `close` — a `disposed` guard ensures teardown never calls `process.exit`.
-
-## Plugin export shape
-
-Named `name` / `inject` / `Config` / `apply`, with **no default export**: the cordis Loader's `unwrapExports` does `exports.default ?? exports`, so a stray default would collapse the module to the bare function and drop the `inject` namespace (see [docs/postmortem/0001](../../../docs/postmortem/0001-acp-default-export-drops-inject.md)). The keyless Loader-path e2e smokes in `examples/{echo,coding}-agent` guard this end-to-end.

+ 0 - 39
packages/support/ui-stdio/package.json

@@ -1,39 +0,0 @@
-{
-  "name": "@deepseek-ai/dsh-ui-stdio",
-  "description": "Minimal stdio (readline) UI plugin: renders agent/* events to stdout and feeds stdin lines to the agent",
-  "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",
-    "cordis": "^4.0.0-rc.6"
-  },
-  "dependencies": {
-    "schemastery": "^3.18.0"
-  },
-  "devDependencies": {
-    "@deepseek-ai/dsh-agent": "workspace:^",
-    "@deepseek-ai/dsh-llm": "workspace:^",
-    "@deepseek-ai/dsh-session": "workspace:^",
-    "cordis": "^4.0.0-rc.6"
-  }
-}

+ 0 - 30
packages/support/ui-stdio/tsconfig.json

@@ -1,30 +0,0 @@
-{
-  "extends": "../../../tsconfig.base.json",
-  "compilerOptions": {
-    "rootDir": "src",
-    "outDir": "lib/types"
-  },
-  "include": [
-    "src"
-  ],
-  "references": [
-    {
-      "path": "../../../vendor/cosmokit"
-    },
-    {
-      "path": "../../../vendor/cordis"
-    },
-    {
-      "path": "../../../vendor/schemastery"
-    },
-    {
-      "path": "../../core/agent"
-    },
-    {
-      "path": "../../llm/llm"
-    },
-    {
-      "path": "../../core/session"
-    }
-  ]
-}

+ 1 - 1
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 UI](../support/ui-stdio) 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 [stdio app's readline UI](../ui/stdio-agent) prints the list, the [ACP bridge](../ui/acp) maps it to a `plan` sessionUpdate.

+ 1 - 1
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 UI](../../support/ui-stdio) 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 [stdio app's readline UI](../../ui/stdio-agent) prints a glyphed checklist, and the [ACP bridge](../../ui/acp) maps the list to a `plan` sessionUpdate (synthesizing the `priority` ACP requires).
 
 ## Export shape
 

+ 1 - 1
packages/ui/README.md

@@ -8,6 +8,6 @@ Integrations that expose the agent to an external editor or client. These are **
 | `stdio-agent/` | Terminal stdio chat APP: the agent-core spine + console logger + readline UI + a pre-created `main` agent, with a `bin` | (composition + `bin`) |
 | `acp-agent/` | ACP server APP: the agent-core spine + JSONL persistence + the `acp` bridge (no stdout logger), with a `bin` | (composition + `bin`) |
 
-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 readline `ui-stdio` plugin is the unstructured analogue but lives in `support/` because it exists chiefly for the examples and the coverage gate — `ui/` is reserved for surfaces shipped as product.
+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 readline UI is the unstructured analogue of the `acp` bridge and lives INSIDE the stdio app (the `stdio-chat` module of [`stdio-agent/`](stdio-agent/README.md)): it is scaffolding for that one front door, not an independently swappable integration, so it carries no package boundary of its own.
 
 `stdio-agent` and `acp-agent` are the two **app packages**: each composes the [`core/agent-core`](../core/agent-core/README.md) spine with its coupled front-door cluster (and owns the boot `bin`), so a leaf `cordis.yml` is the swappable backends plus one app entry plus any optional product tools. They live in `ui/` because each IS a user-facing front door; the stdout-purity coupling (logger vs. no logger) becomes a property of the artifact rather than a leaf convention.

+ 1 - 1
packages/ui/stdio-agent/README.md

@@ -13,7 +13,7 @@ A terminal chat always wants the same cluster, so the package owns it rather tha
 | `@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-core` | the spine, pre-creating a `main` agent from this app's `model`/`systemPrompt` |
 | `@deepseek-ai/dsh-session-persistence-jsonl` | durable JSONL session log under `persistenceRoot` |
-| `@deepseek-ai/dsh-ui-stdio` | the readline UI, bound to the `main` agent |
+| `stdio-chat` (in-package module) | the readline UI, bound to the `main` agent |
 
 `@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`.
 

+ 0 - 2
packages/ui/stdio-agent/package.json

@@ -37,7 +37,6 @@
     "@deepseek-ai/dsh-agent-core": "^0.0.1",
     "@deepseek-ai/dsh-session": "^0.0.1",
     "@deepseek-ai/dsh-session-persistence-jsonl": "^0.0.1",
-    "@deepseek-ai/dsh-ui-stdio": "^0.0.1",
     "cordis": "^4.0.0-rc.6",
     "schemastery": "^3.17.0"
   },
@@ -49,7 +48,6 @@
     "@deepseek-ai/dsh-agent-core": "workspace:^",
     "@deepseek-ai/dsh-session": "workspace:^",
     "@deepseek-ai/dsh-session-persistence-jsonl": "workspace:^",
-    "@deepseek-ai/dsh-ui-stdio": "workspace:^",
     "cordis": "^4.0.0-rc.6",
     "schemastery": "^3.17.0"
   }

+ 5 - 4
packages/ui/stdio-agent/src/index.ts

@@ -1,12 +1,13 @@
 /**
  * The stdio chat app: the providerless agent spine ({@link
  * @deepseek-ai/dsh-agent-core}) plus the coupled front-door cluster a terminal
- * chat needs — a console logger, the readline `ui-stdio` UI, JSONL session
+ * chat needs — a console logger, the readline UI (the in-package `stdio-chat`
+ * module), JSONL session
  * persistence, and a pre-created `main` agent the UI drives.
  *
  * The cluster is BAKED IN, not left to the leaf: a stdio app always logs to the
  * console (stdout is just the terminal) and always pre-creates the `main` agent
- * `ui-stdio` sends to. The leaf supplies the swappable backends (the LLM
+ * the readline UI sends to. The leaf supplies the swappable backends (the LLM
  * adapter, the bash executor), optional product tools, the optional `hmr`
  * dev-reload plugin, and this app's {@link Config} (model, prompt, persistence
  * root, welcome banner).
@@ -42,7 +43,7 @@ import { AgentId } from '@deepseek-ai/dsh-agent'
 import { SessionId } from '@deepseek-ai/dsh-session'
 import * as agentCore from '@deepseek-ai/dsh-agent-core'
 import SessionPersistenceJsonl from '@deepseek-ai/dsh-session-persistence-jsonl'
-import * as uiStdio from '@deepseek-ai/dsh-ui-stdio'
+import * as uiStdio from './stdio-chat.ts'
 
 export const name = 'stdio-agent'
 
@@ -81,7 +82,7 @@ export const Config: z<Config> = z.object({
  * Compose the spine with the stdio front door. The console logger comes first
  * (infra), then the agent-core bundle pre-creating the `main` agent from this
  * app's `model`/`systemPrompt`/`resumeSessionId`, then the JSONL backend, then
- * the `ui-stdio` UI bound to `main`. The `hmr` dev-reload plugin is a leaf
+ * 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.
  */
 export function apply(ctx: Context, config: Config): void {

+ 12 - 17
packages/support/ui-stdio/src/index.ts → packages/ui/stdio-agent/src/stdio-chat.ts

@@ -1,23 +1,18 @@
 /**
- * Minimal stdio UI plugin: reads lines from stdin → `agent.send()`/`steer()`,
- * and renders the durable transcript to stdout. A UI is "just a plugin" — it
- * consumes the `session/event` feed (the assistant token stream, turn/step
- * boundaries, tool activity, todos) plus a few `agent/*` control events
- * (`agent/status`, `agent/created`/`agent/disposed`) and the `agents` service,
- * so the same plugin drives any example or product surface.
+ * The stdio app's readline UI: reads lines from stdin → `agent.send()`/
+ * `steer()`, and renders the durable transcript to stdout. A UI is "just a
+ * plugin" — it consumes the `session/event` feed (the assistant token stream,
+ * turn/step boundaries, tool activity, todos) plus a few `agent/*` control
+ * events (`agent/status`, `agent/created`/`agent/disposed`) and the `agents`
+ * service. Dimmed chain-of-thought rendering plus robust piped-stdin EOF→idle
+ * exit handling, configured via {@link Config}.
  *
- * Consolidates what were two near-identical copies under `examples/echo-agent`
- * and `examples/coding-agent` (the latter a superset). This package IS that
- * superset: dimmed chain-of-thought rendering plus the robust piped-stdin
- * EOF→idle exit handling, configured per consumer via {@link Config}.
+ * An internal module of the stdio app, not a package of its own: the app's
+ * front-door cluster always includes this UI, and nothing else composes it.
+ * The export shape stays named `name`/`inject`/`Config`/`apply` — the plugin
+ * contract the app's `ctx.plugin(uiStdio, …)` mount consumes.
  *
- * Plugin export shape: named `name`/`inject`/`Config`/`apply`, NO default
- * export — the cordis Loader's `unwrapExports` does `exports.default ?? exports`,
- * so a stray default would collapse the module to the bare function and drop
- * the `inject` namespace (see docs/postmortem/0001). The keyless Loader-path
- * e2e smokes in `examples/{echo,coding}-agent` guard this end-to-end.
- *
- * @module @deepseek-ai/dsh-ui-stdio
+ * @module @deepseek-ai/dsh-stdio-agent/stdio-chat
  */
 
 import { createInterface } from 'node:readline'

+ 1 - 1
packages/ui/stdio-agent/tests/built-bin.e2e.ts

@@ -35,7 +35,7 @@ const stdioBin = join(repoRoot, 'packages/ui/stdio-agent/lib/bin.js')
 const dshPackages = [
   'core/agent-core', 'core/agent', 'core/session', 'core/system-prompt',
   'core/tools', 'core/agent-loop', 'llm/llm', 'bash/bash', 'bash/bash-local',
-  'bash/tool-bash', 'support/invariants', 'support/ui-stdio',
+  'bash/tool-bash', 'support/invariants',
   'session-persistence/session-persistence',
   'session-persistence/session-persistence-jsonl', 'ui/stdio-agent',
 ]

+ 2 - 2
packages/support/ui-stdio/tests/readline.spec.ts → packages/ui/stdio-agent/tests/readline.spec.ts

@@ -2,7 +2,7 @@ import { EventEmitter } from 'node:events'
 import type { Readable, Writable } from 'node:stream'
 import { describe, expect, it, vi } from 'vitest'
 import type { Context } from 'cordis'
-import type { StdioRuntime } from '../src/index.ts'
+import type { StdioRuntime } from '../src/stdio-chat.ts'
 
 const createInterface = vi.hoisted(() => vi.fn(() => {
   const reader = new EventEmitter() as EventEmitter & { close(): void }
@@ -32,7 +32,7 @@ function fakeRuntime(inputIsTTY: boolean, outputIsTTY: boolean): StdioRuntime {
 
 describe('createStdioChat readline mode', () => {
   it('enables terminal editing only when both stdio streams are TTYs', async () => {
-    const { createStdioChat } = await import('../src/index.ts')
+    const { createStdioChat } = await import('../src/stdio-chat.ts')
 
     const tty = fakeRuntime(true, true)
     createStdioChat(fakeContext(), {}, tty)

+ 1 - 1
packages/support/ui-stdio/tests/ui-stdio.spec.ts → packages/ui/stdio-agent/tests/stdio-chat.spec.ts

@@ -5,7 +5,7 @@ import type { Agent, AgentStatus } from '@deepseek-ai/dsh-agent'
 import AgentRegistry from '@deepseek-ai/dsh-agent'
 import type { ContentBlock, StreamChunk } from '@deepseek-ai/dsh-llm'
 import type { Session, SessionEvent } from '@deepseek-ai/dsh-session'
-import { createStdioChat, type Config, type StdioRuntime } from '../src/index.ts'
+import { createStdioChat, type Config, type StdioRuntime } from '../src/stdio-chat.ts'
 
 /**
  * Unit tests for the stdio UI plugin. They drive the REAL plugin body

+ 0 - 3
packages/ui/stdio-agent/tsconfig.json

@@ -31,9 +31,6 @@
     },
     {
       "path": "../../session-persistence/session-persistence-jsonl"
-    },
-    {
-      "path": "../../support/ui-stdio"
     }
   ]
 }

+ 0 - 22
pnpm-lock.yaml

@@ -774,25 +774,6 @@ importers:
         specifier: ^4.0.0-rc.6
         version: 4.0.0-rc.6(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.4)
 
-  packages/support/ui-stdio:
-    dependencies:
-      schemastery:
-        specifier: ^3.18.0
-        version: 3.18.0
-    devDependencies:
-      '@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
-      cordis:
-        specifier: ^4.0.0-rc.6
-        version: 4.0.0-rc.6(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.4)
-
   packages/todo/tool-todo:
     devDependencies:
       '@deepseek-ai/dsh-agent':
@@ -922,9 +903,6 @@ importers:
       '@deepseek-ai/dsh-session-persistence-jsonl':
         specifier: workspace:^
         version: link:../../session-persistence/session-persistence-jsonl
-      '@deepseek-ai/dsh-ui-stdio':
-        specifier: workspace:^
-        version: link:../../support/ui-stdio
       cordis:
         specifier: ^4.0.0-rc.6
         version: 4.0.0-rc.6(@cordisjs/plugin-include@vendor+include)(@cordisjs/plugin-loader@vendor+loader)

+ 0 - 1
tsconfig.build.json

@@ -42,7 +42,6 @@
     { "path": "./packages/ui/acp" },
     { "path": "./packages/ui/acp-agent" },
     { "path": "./packages/ui/stdio-agent" },
-    { "path": "./packages/support/ui-stdio" },
     { "path": "./packages/support/llm-replay" },
     { "path": "./packages/subagent/subagent" },
     { "path": "./packages/support/subagent-mock" },

+ 0 - 1
tsconfig.json

@@ -53,7 +53,6 @@
     { "path": "./packages/ui/acp" },
     { "path": "./packages/ui/acp-agent" },
     { "path": "./packages/ui/stdio-agent" },
-    { "path": "./packages/support/ui-stdio" },
     { "path": "./packages/support/llm-replay" },
     { "path": "./packages/subagent/subagent" },
     { "path": "./packages/support/subagent-mock" },