dsh-oc-tui 0.1.4 → 0.2.0-pre.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,431 +1,503 @@
1
- # dsh-oc-tui
2
-
3
- **A terminal UI for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)** — an opencode-inspired chat client that boots inside the `dsh` process as a profile app plugin.
4
-
5
- [![npm latest](https://img.shields.io/npm/v/dsh-oc-tui?label=npm&color=BF392B)](https://www.npmjs.com/package/dsh-oc-tui)
6
- [![awesome-dsh-plugin](https://img.shields.io/badge/awesome--dsh--plugin-marketplace-BF392B)](https://awesome-dsh-plugin.com/zh/p/rayafriandion/dsh-oc-tui/)
7
- [![License: LGPL-3.0-or-later](https://img.shields.io/badge/license-LGPL--3.0--or--later-blue.svg)](LICENSE)
8
- [![Node](https://img.shields.io/badge/node-%3E%3D22-339933.svg)](https://nodejs.org)
9
-
10
- `dsh-oc-tui` renders the harness's durable event stream in your terminal — streaming replies, tool cards, todo lists, thinking blocks — and routes what you type back into the agent. Model routing, tool execution, approvals, commands, durable sessions, and credentials stay owned by DSH; this package owns terminal input and presentation.
11
-
12
- Published on **npm** as [`dsh-oc-tui`](https://www.npmjs.com/package/dsh-oc-tui) and listed in the [**awesome-dsh-plugin**](https://awesome-dsh-plugin.com/zh/p/rayafriandion/dsh-oc-tui/) marketplace.
13
-
14
- > 中文文档:[docs/用户手册.md](docs/用户手册.md)
15
-
16
- ## Table of contents
17
-
18
- - [Features](#features)
19
- - [Requirements](#requirements)
20
- - [Install](#install)
21
- - [Quick start](#quick-start)
22
- - [Usage](#usage)
23
- - [Keybindings](#keybindings)
24
- - [Slash commands](#slash-commands)
25
- - [Interactive prompts](#interactive-prompts)
26
- - [Thinking intensity](#thinking-intensity)
27
- - [Session stats and the context meter](#session-stats-and-the-context-meter)
28
- - [Settings](#settings)
29
- - [In-app updates](#in-app-updates)
30
- - [How it works](#how-it-works)
31
- - [@dsh-std interop](#dsh-std-interop)
32
- - [Development](#development)
33
- - [Troubleshooting](#troubleshooting)
34
- - [Known limitations](#known-limitations)
35
- - [Layout](#layout)
36
- - [License](#license)
37
-
38
- ## Features
39
-
40
- | | |
41
- | --- | --- |
42
- | **Durable sessions** | Create, resume, list, and delete sessions; the transcript is rebuilt from the persisted event log, so a resumed session looks exactly like the one you left. |
43
- | **Live streaming** | Assistant text and reasoning stream token by token; thinking renders in its own collapsible box that stays collapsed while streaming. |
44
- | **Tool activity** | Tool cards with a one-line summary (`read src/app.ts`, `run npm test`), flowing spinners while running, and markdown-rendered results. |
45
- | **Interactive questions** | The model can pause and ask you — option lists, multi-select, free text, and a scrollable plan review — all inline in the terminal. See [Interactive prompts](#interactive-prompts). |
46
- | **Inline approvals** | `approval/request` prompts are answered with `y` / `n` without leaving the UI; the box shows the action and the reason, with room for origin, risk and details when a `@dsh-std` request carries them. |
47
- | **Session stats** | One stats strip above the composer — turns/steps, LLM and tool wall time, average TTFT, decode throughput, cache-hit rate, and billed input/output tokens — folded from durable events. See [Session stats and the context meter](#session-stats-and-the-context-meter). |
48
- | **Stats window** | Click the strip or the context meter, or type `/stats`, for the full session-statistics and token-usage breakdown. |
49
- | **Context meter** | Live context occupancy (`ctx ▓▓░░ 32K/128K 25%`), with the system/tools/messages composition in the same window. |
50
- | **Thinking intensity** | `Tab` cycles the current model's real reasoning levels; `Ctrl+E` opens a slider. The level is applied per request and persisted. |
51
- | **Shared settings** | The same host settings namespaces the Web UI uses — general, sessions, per-provider model configuration, credentials — persisted to `$DSH_HOME/settings.yaml`. |
52
- | **In-app updates** | Detect and switch versions of `@deepseek-ai/dsh` and `dsh-oc-tui` from inside the TUI, with Windows-safe deferred installs. |
53
- | **Zero-dependency terminal engine** | Raw mode, alternate screen, a diffing cell buffer, truecolor ANSI, CJK-aware widths, SGR + legacy X10 mouse decoding, and IME caret anchoring. |
54
-
55
- ## Requirements
56
-
57
- | | |
58
- | --- | --- |
59
- | Node.js | >= 22 |
60
- | dsh CLI | `@deepseek-ai/dsh` — e.g. `npm install -g @deepseek-ai/dsh` |
61
- | pnpm | on `PATH`; `dsh plugin` forwards to it |
62
- | Terminal | an interactive terminal (Windows Terminal / ConPTY, iTerm2, GNOME Terminal, …) |
63
- | Model route | a usable route in `$DSH_HOME/settings.yaml` + `$DSH_HOME/.credentials.yaml` (the same setup the Web GUI uses) |
64
-
65
- ```sh
66
- dsh --version
67
- pnpm --version
68
- ```
69
-
70
- **Compatibility.** Verified against dsh `0.1.2-rc.1` (and `0.1.1-rc.2`). DSH renamed parts of the session API in 0.1.2 — `Session.events` became `snapshotEvents()` — and this plugin reads whichever accessor the host provides, so one build serves both lines.
71
-
72
- ## Install
73
-
74
- ### From npm
75
-
76
- The package is published on npm as [`dsh-oc-tui`](https://www.npmjs.com/package/dsh-oc-tui). Install it into the `tui` profile:
77
-
78
- ```sh
79
- dsh plugin --profile tui add -w dsh-oc-tui
80
- ```
81
-
82
- Or install the launcher globally — that puts the `dsh-oc-tui` command on `PATH`, which then boots `dsh --profile tui`:
83
-
84
- ```sh
85
- npm install -g dsh-oc-tui
86
- ```
87
-
88
- ### Version channels
89
-
90
- The **npm package** and the **[awesome-dsh-plugin](https://awesome-dsh-plugin.com/zh/p/rayafriandion/dsh-oc-tui/) marketplace entry** both ship **stable releases only** — pre-releases are never published to either. `npm install` therefore gives you the latest stable version, not a release candidate.
91
-
92
- This README describes the current source tree, which can be ahead of the published release — a feature documented here is only guaranteed to exist in a stable build once that version is on npm.
93
-
94
- To run a pre-release, or unreleased work from this repository, install it explicitly from source:
95
-
96
- ```sh
97
- npm pack # -> dsh-oc-tui-<version>.tgz
98
- dsh plugin --profile tui add -w ./dsh-oc-tui-<version>.tgz
99
- ```
100
-
101
- ### One-command installers
102
-
103
- The repository ships installers that check Node >= 22, make sure `pnpm` exists, install the plugin into the `tui` profile, and can also add the `dsh-oc-tui` launcher globally.
104
-
105
- ```sh
106
- # Linux / macOS
107
- curl -fsSL https://raw.githubusercontent.com/rayafriandion/dsh-oc-tui/main/install.sh | bash
108
- ```
109
-
110
- ```powershell
111
- # Windows (PowerShell)
112
- powershell -ExecutionPolicy Bypass -Command "iwr https://raw.githubusercontent.com/rayafriandion/dsh-oc-tui/main/install.ps1 -OutFile install.ps1; & .\install.ps1"
113
- ```
114
-
115
- Run `./install.sh` / `.\install.ps1` from a checkout instead, and add `--launcher` / `-Launcher` to also put the `dsh-oc-tui` command on `PATH`. Other flags: `--local` (`-Local`) installs the current checkout, `--source <spec>` (`-Source <spec>`) uses a custom source, `--profile <name>` (`-Profile <name>`) targets another profile.
116
-
117
- ### From a checkout or tarball
118
-
119
- ```sh
120
- npm pack # -> dsh-oc-tui-<version>.tgz
121
- dsh plugin --profile tui add -w ./dsh-oc-tui-<version>.tgz
122
- ```
123
-
124
- `dsh plugin` anchors relative paths to the directory you invoke it from before forwarding to pnpm.
125
-
126
- ### Why `-w`
127
-
128
- The profile directory declares itself a pnpm workspace root (`pnpm-workspace.yaml` → `packages: [.]`), so pnpm refuses a bare `add` with `ERR_PNPM_ADDING_TO_ROOT`. `-w` makes the dependency land in the profile's own manifest — which is exactly what it is. `dsh plugin` then reconciles `dsh.profile.bundles` against what is installed.
129
-
130
- ### What the install does
131
-
132
- 1. `dsh plugin` initializes `$DSH_HOME/profiles/tui` on first use (`@deepseek-ai/dsh-base` plus an empty user patch layer).
133
- 2. pnpm installs `dsh-oc-tui` into the profile's `node_modules`.
134
- 3. Because the package declares `dsh.bundle.patch`, dsh appends `dsh-oc-tui` to `dsh.profile.bundles`.
135
- 4. `dsh --profile tui` composes the base layer, this bundle's rows, and your own patch — no manual editing required.
136
-
137
- Verify without booting:
138
-
139
- ```sh
140
- dsh --profile tui --dump-config
141
- ```
142
-
143
- The dump shows a `# == dsh-oc-tui` layer containing `tui-startup`, `tui-app`, the `agent-presets` roster row, and the `tool-ask-user` row.
144
-
145
- ## Quick start
146
-
147
- ```sh
148
- dsh --profile tui # title screen; your first message creates a session
149
- dsh --profile tui --resume <sessionId> # resume a persisted session
150
- dsh --profile tui --model <modelId> # default model for new sessions
151
- dsh --profile tui --provider <route> # default provider route
152
- dsh --profile tui --no-sidebar # start without the session rail
153
- dsh --profile tui --help # the TUI's own flags
154
- ```
155
-
156
- The stock launcher hardcodes only `web` and `plugin` as bare subcommands, so `--profile tui` is the intended shape. Want the literal `dsh tui`? Add a shell alias:
157
-
158
- ```powershell
159
- function tui { dsh --profile tui @args } # PowerShell $PROFILE
160
- ```
161
-
162
- ```bat
163
- doskey tui=dsh --profile tui $* :: CMD
164
- ```
165
-
166
- ### Convenience launcher
167
-
168
- The package also ships a `dsh-oc-tui` binary that is equivalent to `dsh --profile tui`, but checks first that the profile actually has the plugin installed and prints the one-time install command when it does not.
169
-
170
- ```sh
171
- dsh-oc-tui # boot the tui profile
172
- dsh-oc-tui --profile mytui # boot a different profile
173
- dsh-oc-tui --help # launcher help
174
- dsh-oc-tui --version # launcher version
175
- ```
176
-
177
- It prefers the `dsh` on `PATH` and falls back to `npx --yes @deepseek-ai/dsh`. Install it with `npm install -g dsh-oc-tui`.
178
-
179
- | Environment variable | Effect |
180
- | --- | --- |
181
- | `DSH_TUI_PROFILE` | Default profile when `--profile` is absent (default `tui`). |
182
- | `DSH_TUI_SKIP_CHECK` | Set to `1` to skip the profile preflight (advanced installs). |
183
-
184
- ## Usage
185
-
186
- ### Keybindings
187
-
188
- | Key | Action |
189
- | --- | --- |
190
- | `Enter` | Send the message. |
191
- | `Ctrl+Enter` / `Shift+Enter` / `Alt+Enter` | Insert a newline. |
192
- | `Ctrl+C` | Clear a non-empty prompt, cancel the running turn, or press twice while idle to exit. |
193
- | `Ctrl+P` | Open Settings. |
194
- | `Ctrl+E` | Toggle the thinking-intensity slider below the composer. |
195
- | `Tab` | Session page: cycle the thinking intensity. Settings page: switch the left menu. |
196
- | `Ctrl+N` | New session. |
197
- | `Ctrl+D` | In Settings → Manage sessions: delete the focused session (press twice to confirm). |
198
- | `Ctrl+L` | Clear the transcript view. |
199
- | `Up` / `Down` | Move the caret across a multi-line prompt; on the first/last row, step through input history. |
200
- | `Left` / `Right` | Move the caret within the input box. |
201
- | `PgUp` / `PgDn` | Scroll the transcript. |
202
- | `Esc` | Close the session stats window, the thinking slider, or help; cancel an approval; cancel a running turn; clear the prompt you are typing. |
203
- | `Esc Esc` | Idle with an empty prompt: open the rewind picker. |
204
- | `y` / `n` | Answer an inline approval prompt. |
205
-
206
- **Mouse.** The wheel scrolls the transcript (or the Settings window while it is open). Hold the left button and drag across the transcript to select text, then press the right button to copy the selection.
207
-
208
- ### Slash commands
209
-
210
- Built in: `/help` `/settings` `/new` `/resume <id>` `/model <id>` `/provider <route>` `/rewind` `/stats` `/clear` `/cancel` `/quit` (`/exit` also works).
211
-
212
- **Rewind.** `Esc Esc` (or `/rewind`) lists the prompts of the live session. Restoring the **conversation** forks a new session from the events before the chosen prompt — the parent session is left untouched on disk, exactly as the harness's own `session/fork` does — and the picker lands on the most recent prompt, so `Enter` twice rewinds the last turn. `/rewind <n|last> [conversation|code|both]` runs it without the picker. The fork starts with an **empty inbox**: a cut before a turn also cuts the inbox claim that turn performed, so anything the parent had queued — including the prompt you rewound away from — is not delivered again; it stays in the parent's log, and the result line says `dropped N inherited pending input` when there was any. Restoring **files** is best-effort and fenced: it needs a git worktree (anywhere else the rewind reports `files not restored (not a git worktree)` and changes nothing), it rewrites tracked files from `HEAD` without touching the index, and it removes an untracked file only when the transcript's first recorded write to that path is at or after the rewind point. Everything it overwrites or deletes is copied to `$DSH_HOME/rewind-backups/<sessionId>/<timestamp>/` first, and the result line names that directory. Because the log stores no file contents, a tracked file returns to its last commit, not to its exact state at the rewind point.
213
-
214
- Harness commands — `/compact`, `/goal`, `/plan`, … — are forwarded to `ctx.commands` and run without a model turn. They need a live session: on the title screen the TUI answers `/<name>: start a session first` instead of dropping the command silently.
215
-
216
- `/stats` is the TUI's own command: it toggles the [session stats window](#session-stats-and-the-context-meter) and, like the harness commands, needs a live session.
217
-
218
- ### Interactive prompts
219
-
220
- **Approvals.** When a tool needs permission, the composer area is replaced by an approval box. The first row carries the action (`Approval · <tool>`) with the key hints riding along when they fit; the explanation the request supplies follows as a `Summary ·` row. `y` allows once, `n` rejects, `Esc` cancels. A request from a `@dsh-std` component can additionally carry `Origin`, `Risk` and `Details` rows — they are drawn when present and omitted when not, a detail marked private shows its label but not its value, and on a narrow terminal the middle yields with a `… N more` marker while the action and the hints always survive. The plugin also honours the effective permission preset, so an auto-approving preset does not prompt at all.
221
-
222
- An approval that outlives the deadline its caller set disappears on its own. That is an **expiry**, not a cancellation: the caller is told `expired` rather than `cancelled`, so a timeout can never be read as a human decision.
223
-
224
- **Questions.** The model can ask you directly through the `ask_user_question` tool. The tool is declared by this bundle's `tool-ask-user` row — `dsh-base` mounts the `user-questions` service but not the tool, and a TUI session composes from the base rather than from an agent preset — and it is answered by a modal:
225
-
226
- | Key | Action |
227
- | --- | --- |
228
- | `Up` / `Down` | Move between options (wrapping). |
229
- | `Space` | Toggle the highlighted option (multi-select) or select it (single choice). |
230
- | `Enter` | Continue: a single choice is selected and advances; on the free-text row it starts editing; in a multi-select it confirms the toggled set. |
231
- | any printable key | Jump into the free-text row and start typing. |
232
- | `PgUp` / `PgDn`, wheel | Scroll a long plan or detail pane. |
233
- | `Esc` | Defer: decline to answer here (`Esc` while editing returns to the options). |
234
- | `Ctrl+C` | Still cancels the running turn; the pending question is withdrawn. |
235
-
236
- Questions are staged one at a time, exactly as the Web UI composer stages them, and the answer encoding is identical: a free-text answer replaces the selection for a single-select question and accompanies it for a multi-select one.
237
-
238
- A question carrying the `plan-review` intent — what `exit_plan_mode` sends — renders the plan markdown in a scrollable pane above `Approve` / `Keep planning`. Answering `Approve` exits plan mode and the model continues; anything else keeps planning.
239
-
240
- Deferring is deliberate, not a cancel: with no other answerer the tool reports `no user-questions answerer accepted the request`, which cannot be mistaken for a human choice.
241
-
242
- ### Thinking intensity
243
-
244
- The effective level sits on the composer's top-right border as the bare level name, diagonally opposite the `provider · model` label.
245
-
246
- - `Tab` on the session page cycles the levels of the **current model** (wrapping strongest → weakest); `Shift+Tab` steps backwards.
247
- - `Ctrl+E` opens a slider below the composer: `Tab` or `←`/`→` adjust and persist, `Esc` or `Ctrl+E` close it.
248
- - Levels come from the provider adapter (`ctx.llm.resolveModelInfo`), so a boolean-thinking model shows exactly its two ends, DeepSeek's `Off`/`High`/`Max` shows those three, and a full-range model shows every advertised level — never a blanket `none → max` scale.
249
-
250
- The choice is applied to the session's requests through the `agent/request` waterfall and stored in `agent-default-model.reasoningEffort`.
251
-
252
- ### Session stats and the context meter
253
-
254
- The row above the composer is the **session stats strip**, the TUI's counterpart of the web chat's stats line: the same figures in the same order, separated by `│`:
255
-
256
- ```
257
- ▤ 1 turn · 2 steps│LLM 1.3s · tools 1.2s│TTFT avg 400ms · 20.0 tok/s│cache 55%│in 110 · out 30
258
- ```
259
-
260
- - `turns` / `steps` count **closed steps** (`step/end`), so failed, cancelled, and max-tokens steps count too. `LLM` is `step/start` → assembled reply; `tools` pairs `tool/call` → `tool/result`.
261
- - `TTFT avg` is the per-step average time to first token; `tok/s` is decode throughput (first token → assembled reply over the reported output tokens).
262
- - `cache` is the prompt-side cache-hit share (cache reads over all billed input); `in` / `out` are the session's billed input and output tokens.
263
- - A narrow terminal drops trailing groups **whole** and marks the elision with `│…` rather than cutting a figure in half; the window always carries the complete set.
264
- - A session with no closed step and no billed tokens hides the strip entirely and gives the row back to the transcript.
265
-
266
- The whole strip is a click target. Clicking it — or the context meter at the right end of the status row (`ctx ▓▓░░ 32K/128K 25%`), or typing `/stats` — opens the **session stats window**; click again, click elsewhere, or press `Esc` to close. The window breaks the same line into labelled rows (`usage` / `duration` / `speed` / `tokens` / `cache`) and adds the context-occupancy reading with its heuristic composition — system prompt, tools, and messages — matching the Web UI's ContextMeter dialog.
267
-
268
- Figures come from the same sources as the Web UI, projection-first with the plugin's own fold as the fallback: `tokenUsage`, `contextPressure`, and `contextBreakdown` are mounted by `dsh-base`'s token-meter row, while `sessionStats` is mounted only by the web app bundle — so the TUI folds the durable `step` / `chunk` / `message` / `tool` events by the same rules. A missing projection falls back for that figure alone, and a figure nobody can supply stays hidden instead of printing a zero.
269
-
270
- ### Settings
271
-
272
- `Ctrl+P` opens a settings menu over the same host settings namespaces as the Web UI, persisted through `ctx.settings` to `$DSH_HOME/settings.yaml`. A left menu splits it into three tabs (`Tab` or click to switch):
273
-
274
- - **Main** — General (busy-Enter behaviour, default agent preset, permission preset), Sessions (new session, manage sessions), System (provider API hints, update-manager shortcut, settings file path).
275
- - **Model** — the default provider/model/reasoning choice, then one group per provider holding its URL, API key, and model list. Pressing `Enter` on **Models** fetches the provider's advertised catalog (`ctx.llm.discoverModels`) and opens a checkbox window; pressing `Enter` on a listed model makes it the default route.
276
- - **Update** — see [In-app updates](#in-app-updates).
277
-
278
- Only providers you actually added (present in your user settings layer) are listed; a provider that was never added stays hidden. The default agent preset comes from the roster the profile mounts (the shipped presets plus any you authored under `$DSH_HOME/.agent-presets`) — note that a TUI session composes process-wide from the base, so the stored default applies where a session is created from a preset. Web-UI-only options (`ui-theme` appearance, `locale`) are not shown because they have no effect in the TUI.
279
-
280
- ### In-app updates
281
-
282
- `Ctrl+P → Update` shows the installed versions of `@deepseek-ai/dsh` and `dsh-oc-tui`, the latest npm dist-tag, and a status line that only ever targets **stable** releases:
283
-
284
- - `Update available → x.y.z` — a newer stable release exists.
285
- - `Up to date` — nothing to do.
286
- - `No stable release — pick from Versions` — the registry has no stable release yet; pick one manually.
287
- - `Install damaged — reinstall below` — the global dsh tree is in a mixed old/new state; reinstall it.
288
-
289
- `Enter` on a package's **Versions** row opens the full registry list (newest first, `[latest]`/`[next]`/other tags and `(installed)` colour-coded) where you can pick any version — including pre-releases — for a y/n-confirmed install through `npm`/`dsh plugin`. `Check now` re-reads the registry; `Startup check` toggles the silent boot-time stable-release check. Installs run in the background, never block the UI, and need a restart to apply.
290
-
291
- <details>
292
- <summary><strong>Windows: why dsh installs are deferred to exit</strong></summary>
293
-
294
- On Windows, updating dsh while any dsh process runs can *silently corrupt* the global install: npm replaces the directory while the running process holds memory-mapped native DLLs, still exits 0, and the resulting old/new hybrid tree fails to boot. The updater guards this in three layers:
295
-
296
- 1. **dsh installs are deferred to TUI exit** — a detached helper waits for the TUI to close, runs the install, and records the outcome in `$DSH_HOME/tui-dsh-install.json`, which the Update page verifies on the next visit.
297
- 2. **The on-disk version is compared** against the requested target after every direct install, so a silent corruption surfaces as an `install corrupt` toast with repair instructions.
298
- 3. **An already-damaged install is flagged** in the Status row rather than reported as a bogus success.
299
-
300
- macOS/Linux have no DLL lock, but an install is refused while other dsh processes are running.
301
- </details>
302
-
303
- ## How it works
304
-
305
- - The plugin is a Cordis function plugin loaded by the `tui` profile. `lib/startup.js` parses the app's flags and provides the `tuiStartup` service; `lib/index.js` owns the UI loop.
306
- - `lib/term.js` is a zero-dependency terminal engine: raw mode, alternate screen, a diffing cell buffer, and a key decoder (truecolor ANSI, CJK-aware widths). It parks the hidden terminal cursor at the input caret so the OS IME anchors its composition window inside the composer, and it understands both SGR and legacy X10 mouse encodings so wheel and click bytes can never leak into the input text. The cell buffer is also the single chokepoint where every painted string is sanitized: C0, DEL and C1 characters are replaced with a visible `\uFFFD`, so a tool name, a path, a title or a `@dsh-std` request's action can never smuggle an escape sequence into the output stream. The width-preserving placeholder is deliberate — dropping the character instead would silently change layout, whereas a visible one makes the injection obvious.
307
- - `lib/ui.js` is the responsive view model and renderer (DeepSeek blue-white theme, session rail, transcript, multiline composer, command suggestions, telemetry footer). Transcript lines are cached per block, only the visible window is materialised each frame, streaming paints are coalesced, and the live block re-renders on a short throttle — so render cost stays bounded as history grows. Thinking collapses to keep the transcript readable, and running tools and thinking blocks animate with flowing spinners.
308
- - `lib/metrics.js` folds durable step/chunk/message events into token, TTFT, throughput, and cache-hit metrics.
309
- - `lib/interrupt.js` owns the clear/cancel/double-exit state machine used by stdin and `SIGINT`.
310
- - `lib/markdown.js` renders model output (headings, lists, quotes, code, inline spans) to styled lines.
311
- - `lib/updates.js` isolates every npm/pnpm interaction for the Update tab — registry queries, dependency-free semver comparison, dsh install detection, and installs — all through `child_process.spawn`, never `spawnSync`.
312
- - Agents are created and resumed through `ctx.agents`, the transcript is rebuilt from the session's durable log and fed live by `session/event` (including `assistant/chunk`), model defaults come from `ctx.agentDefaultModel`, and approvals answer the `approval/request` waterfall inline.
313
- - `ask_user_question` is answered over the `user-questions/request` waterfall: a scoped Cordis waterfall where the modal either returns an answer or delegates with `next()`. An aborted request rejects so the service reports its own `ASK_ABORTED`; requests addressed to another agent are delegated untouched.
314
-
315
- ## @dsh-std interop
316
-
317
- The package carries a [`dsh-plugin.json`](dsh-plugin.json) manifest and a facet at `lib/facet.js`, so a `@dsh-std` host can discover it without running any code. **What ships today is that static manifest and the preflight surface; the runtime protocol support is currently gated off by an upstream limitation.** The plugin is designed as a **host and Presentation provider** in that ecosystem, not a protocol consumer — its facet is wired to publish `presentation.dsh/v1alpha1` `UserInteraction` / `Notification` / `CopyText` and `commands.dsh/v1alpha1` `CommandRuntime` for other components to drive. Its one `requires.contracts` entry is the `commands.dsh/v1alpha1` `Command` resource, declared **`optional: true`**. The entry is retained for a future consumer and is inert today — nothing calls `protocols.client(...)`, and its own command line is parsed from the cordis `ctx.commands` service, so the TUI behaves identically whether or not a Command provider exists. An unsatisfiable non-optional requirement would be a hard activation failure rather than a warning, and the `optional` flag is what keeps the declaration honest about that. But a Community v0.15 manifest **cannot declare protocol supports**, and `@dsh-std/lifecycle`'s `implement` — which *is* `stageProtocol` — refuses to stage a support the facet has not declared, so the facet stages **nothing**. The protocol shims are written and test-covered; they go live unchanged once upstream closes the gap. See [docs/dsh-std-接入说明.md](docs/dsh-std-接入说明.md#阻断性发现本插件当前不发布任何协议-support) for the layer-by-layer evidence.
318
-
319
- **The facet does not own the TUI's lifecycle, and the double activation is avoided by design.** `@dsh-std/adapter-dsh`'s `mountProfileComponents` mounts the facet of every `dsh-plugin.json` it finds in a profile's `dependencies`, without checking whether the cordis bundle rows already loaded that component — and this plugin *is* a profile dependency that `cordis.patch.yml` also activates. Since the TUI seizes the terminal (raw mode, alternate screen, mouse tracking), a manifest alone would start it twice. The facet is therefore an **interop shell**: when it does stage protocol support, it forwards protocol calls through `lib/bridge.js` to whichever TUI instance is live, and it reports `{ state: 'degraded' }` when none is — or when it staged nothing at all. A `dsh-plugin.json` whose facet never starts the TUI is working as intended, not broken.
320
-
321
- > 中文说明:[docs/dsh-std-接入说明.md](docs/dsh-std-接入说明.md) — 本插件在 `@dsh-std` 生态里扮演 **Host 与 Presentation 提供方**;facet 只是协议外壳,**不拥有 TUI 的生命周期**,双激活已由设计规避。**但当前上游下它一条协议都不发布**:Community v0.15 清单无法声明 supports,而 lifecycle 要求先声明才能暂存,因此 Phase B(运行时互操作)休眠、Phase A(静态清单与预检)可交付——见该文档开头的「阻断性发现」。该文档还记录了清单字段、pin 的 `rc` 版本、`lib/bridge.js` 的契约、adapter 的 staging 契约、明确不实现的协议,以及评审确认的已知行为。
322
-
323
- ## Development
324
-
325
- ```sh
326
- npm run check # node --check over lib/, bin/
327
- npm test # standalone smoke tests (no dsh needed)
328
- ```
329
-
330
- **The install is a build, so edit → build → install.** The profile contains a *tarball* copy of the plugin, and the profile's HMR root is the profile directory, not the plugin directory — editing this checkout changes nothing until you repack and reinstall:
331
-
332
- ```sh
333
- npm pack # -> dsh-oc-tui-<version>.tgz
334
- dsh plugin --profile tui remove -w dsh-oc-tui # detach the old copy FIRST
335
- Remove-Item .\*.tgz # then drop the stale tarball
336
- npm pack
337
- dsh plugin --profile tui add -w .\dsh-oc-tui-<version>.tgz
338
- ```
339
-
340
- Detach before deleting: pnpm resolves the profile's existing `file:` dependency while adding, so a dependency pointing at a deleted tarball aborts the whole install with `ENOENT`.
341
-
342
- Verify the swap actually landed — the version string proves nothing:
343
-
344
- ```powershell
345
- foreach ($rel in @('lib\index.js','lib\ui.js','lib\util.js','lib\term.js','lib\metrics.js',
346
- 'lib\interrupt.js','lib\web-settings.js','lib\updates.js','lib\markdown.js',
347
- 'lib\startup.js','bin\dsh-oc-tui.js','cordis.patch.yml')) {
348
- $a = (Get-FileHash ".\$rel").Hash
349
- $b = (Get-FileHash "$env:USERPROFILE\.dsh\profiles\tui\node_modules\dsh-oc-tui\$rel").Hash
350
- if ($a -ne $b) { "DIFFERS: $rel" }
351
- }
352
- ```
353
-
354
- Then boot it for real. Reaching the title screen is not enough — the session-open path is where host API breaks surface, so send a message. Test `--resume` separately, because it is an apply-time path that can lose a startup race the post-boot paths win.
355
-
356
- For a zero-install bootstrap that skips packaging entirely, create the profile once and point its patch at this checkout:
357
-
358
- ```sh
359
- dsh --profile tui --dump-config # initializes the base profile once
360
- ```
361
-
362
- ```yaml
363
- # $DSH_HOME/profiles/tui/cordis.patch.yml
364
- - insert:
365
- - id: tui-startup
366
- name: 'file:///path/to/dsh-oc-tui/lib/startup.js'
367
- - id: tui-app
368
- name: 'file:///path/to/dsh-oc-tui/lib/index.js'
369
- config:
370
- sidebar: true
371
- showReasoning: true
372
- ```
373
-
374
- The plugin's dsh imports resolve through the shared `$DSH_HOME/profiles/node_modules` fallback that dsh maintains, so nothing has to be installed into the plugin directory.
375
-
376
- ## Troubleshooting
377
-
378
- | Symptom | Cause and fix |
379
- | --- | --- |
380
- | `pnpm failed in profile directory` / `ERR_PNPM_ADDING_TO_ROOT` | The profile is a pnpm workspace root; add `-w` to the `add`/`remove` command. |
381
- | `ENOENT: … dsh-oc-tui-<v>.tgz` during install | The profile still references a tarball you deleted. `dsh plugin --profile tui remove -w dsh-oc-tui`, then add again. |
382
- | `pnpm not found on PATH` | Install pnpm (`npm install -g pnpm`) and retry. |
383
- | `--dump-config` has no TUI layer | The install did not complete, or the package name is misspelled. Re-run the `add` and check `dsh.profile.bundles`. |
384
- | Exits immediately / no UI | stdin and stdout must both be a TTY — do not pipe or redirect. Then verify the model route and credentials exist. |
385
- | `--resume` or Manage sessions unavailable | Both need the shared `sessionQuery` service; keep `@deepseek-ai/dsh-base` first in `dsh.profile.bundles`. |
386
- | `no agent factory registered` on `--resume` | A startup race with the agent-loop row; current builds retry it. On an older build, run `/resume <id>` after boot instead. |
387
- | Loader errors after updating dsh (`State`, `./internal`) | The global dsh install is a mixed old/new tree. Close every dsh process and reinstall: `npm install -g @deepseek-ai/dsh@<version>`. |
388
- | Source edits have no effect | The installed copy is a tarball; repack and reinstall (see [Development](#development)). |
389
-
390
- More detail, in Chinese: [docs/用户手册.md](docs/用户手册.md).
391
-
392
- ## Known limitations
393
-
394
- - IME composition is not exposed by the zero-dependency terminal engine yet. Pasted images are: a bracketed paste of raw image bytes, a `data:image/...;base64,...` URL, a local image path, or an image URL becomes a `[Image N]` attachment, and pasting text nothing recognizes asks the terminal for its clipboard (OSC 52).
395
- - The plugin does not hot-reload: the profile's HMR root is the profile directory, so a running TUI keeps the copy it booted with.
396
- - `dsh tui` as a bare subcommand needs a shell alias — the stock launcher hardcodes only `web` and `plugin`.
397
- - Harness slash commands need a live session; on the title screen the TUI tells you to start one first.
398
- - Deferring a question with `Esc` does not cancel the tool call — it delegates, and with no other answerer the tool call fails. Per-question skip (as the Web UI composer offers) is not implemented.
399
- - `--resume`, Settings → Manage sessions, the context meter, and the stats strip depend on services mounted by `@deepseek-ai/dsh-base` (`sessionQuery`, `sessionProjections`); a hand-built profile must provide them. The `sessionStats` projection is a web-app-layer row, so the TUI folds those figures from the session log itself when no profile mounts it.
400
- - The deferred dsh install on Windows waits for the TUI that scheduled it, not for every dsh process on the machine — close other TUI windows (and `dsh web`) before it runs.
401
- - Two `@dsh-std` presentation surfaces have no caller today, because the runtime protocol support is gated off (see [@dsh-std interop](#dsh-std-interop)). A standard `secret-input` request would open a masked prompt with `minLength`/`maxLength` bounds, and a standard `ApprovalRequest` is the only thing that populates the approval box's `Origin` / `Risk` / `Details` rows. Both paths are written and integration-tested; neither can be reached from a normal `dsh --profile tui` session, which is why the harness-shaped approval box shows only the action and the summary.
402
-
403
- ## Layout
404
-
405
- ```
406
- lib/index.js plugin entry: agents, events, input, commands, approvals, user questions
407
- lib/startup.js command-line provider (tuiStartup service)
408
- lib/term.js terminal engine (raw mode, screen, key decoding)
409
- lib/ui.js responsive view model + renderer (includes the question modal)
410
- lib/metrics.js whole-session stats + token usage fold (web stats strip / tokenUsage port)
411
- lib/interrupt.js Ctrl+C lifecycle state
412
- lib/web-settings.js shared WebUI settings projection
413
- lib/updates.js in-app update manager (npm registry + installs)
414
- lib/markdown.js markdown -> styled lines
415
- lib/util.js text/display helpers
416
- lib/bridge.js live-TUI registry the @dsh-std facet forwards through
417
- lib/facet.js @dsh-std facet entry (interop shell; never starts the TUI)
418
- lib/std/ @dsh-std protocol adapters (adapt, presentation, commands, command-list)
419
- bin/dsh-oc-tui.js convenience launcher for `dsh --profile tui`
420
- install.sh one-command installer (Linux/macOS)
421
- install.ps1 one-command installer (Windows)
422
- dsh-plugin.json @dsh-std component manifest (discovery + preflight only)
423
- cordis.patch.yml bundle patch layer (TUI rows, agent-presets roster, ask-user tool)
424
- docs/用户手册.md Chinese user manual
425
- docs/dsh-std-接入说明.md @dsh-std interop scope and constraints (Chinese)
426
- tests/smoke.test.mjs standalone smoke tests
427
- ```
428
-
429
- ## License
430
-
431
- [LGPL-3.0-or-later](LICENSE).
1
+ # dsh-oc-tui
2
+
3
+ **As the custom TUI engine cannot support advanced visual effects or full Markdown and Mermaid integration**, future versions will incorporate dependencies such as `chafa`, `mmdc`, and `mdcat` to enhance the visual experience. This update is tentatively scheduled for version `0.2.0` ; please note that this may result in incompatibility between the `0.1.x` and `0.2.x` series. We plan to release pre-release versions first to gather feedback. If you have suggestions for specific visual features, please share them in this issue thread or open a new issue with a title containing `Visual Experience Suggestion`
4
+
5
+ **_Thank you for your support of the `dsh-oc-tui` plugin!_**
6
+
7
+ ---
8
+
9
+ **A terminal UI for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)** — an opencode-inspired chat client that boots inside the `dsh` process as a profile app plugin.
10
+
11
+ [![npm latest](https://img.shields.io/npm/v/dsh-oc-tui?label=npm&color=BF392B)](https://www.npmjs.com/package/dsh-oc-tui)
12
+ [![awesome-dsh-plugin](https://img.shields.io/badge/awesome--dsh--plugin-marketplace-BF392B)](https://awesome-dsh-plugin.com/zh/p/rayafriandion/dsh-oc-tui/)
13
+ [![License: LGPL-3.0-or-later](https://img.shields.io/badge/license-LGPL--3.0--or--later-blue.svg)](LICENSE)
14
+ [![Node](https://img.shields.io/badge/node-%3E%3D22-339933.svg)](https://nodejs.org)
15
+
16
+ `dsh-oc-tui` renders the harness's durable event stream in your terminal — streaming replies, tool activity, todo lists, thinking blocks — and routes what you type back into the agent. Model routing, tool execution, approvals, commands, durable sessions, and credentials stay owned by DSH; this package owns terminal input and presentation.
17
+
18
+ Published on **npm** as [`dsh-oc-tui`](https://www.npmjs.com/package/dsh-oc-tui) and listed in the [**awesome-dsh-plugin**](https://awesome-dsh-plugin.com/zh/p/rayafriandion/dsh-oc-tui/) marketplace.
19
+
20
+ > 中文文档:[docs/用户手册.md](docs/用户手册.md)
21
+
22
+ ## Table of contents
23
+
24
+ - [Features](#features)
25
+ - [Requirements](#requirements)
26
+ - [Install](#install)
27
+ - [Quick start](#quick-start)
28
+ - [Usage](#usage)
29
+ - [Keybindings](#keybindings)
30
+ - [Slash commands](#slash-commands)
31
+ - [Interactive prompts](#interactive-prompts)
32
+ - [Thinking intensity](#thinking-intensity)
33
+ - [Session stats and the context meter](#session-stats-and-the-context-meter)
34
+ - [The status rail](#the-status-rail)
35
+ - [Settings](#settings)
36
+ - [In-app updates](#in-app-updates)
37
+ - [How it works](#how-it-works)
38
+ - [@dsh-std interop](#dsh-std-interop)
39
+ - [Development](#development)
40
+ - [Troubleshooting](#troubleshooting)
41
+ - [Known limitations](#known-limitations)
42
+ - [Layout](#layout)
43
+ - [License](#license)
44
+
45
+ ## Features
46
+
47
+ | | |
48
+ | --- | --- |
49
+ | **Durable sessions** | Create, resume, list, and delete sessions; the transcript is rebuilt from the persisted event log, so a resumed session looks exactly like the one you left. |
50
+ | **Live streaming** | Assistant text and reasoning stream token by token; thinking renders in its own collapsible box that stays collapsed while streaming. |
51
+ | **Real image rendering** | Images in the transcript render as real pixels — kitty graphics, iTerm2 inline images, or sixel, probed from the terminal itself (works over SSH), with a truecolor half-block preview on terminals that support no protocol. Pasted images, stored attachments, and markdown images all flow through one pipeline. See [Graphics rendering](#graphics-rendering). |
52
+ | **Mermaid diagrams** | A ` ```mermaid ` fence renders as a real diagram through a provider chain — local `mmdc` if installed, otherwise mermaid.ink (gated by a setting) — into the same image pipeline; offline it degrades to highlighted source. |
53
+ | **Full markdown** | Tables in rounded boxes, nested and task lists, syntax-highlighted code (190+ languages), and clickable OSC 8 hyperlinks, rendered from markdown-it's CommonMark + GFM token stream. |
54
+ | **Flat transcript** | No frames around the conversation: your turns open with a brand-blue `❯` pointer and hang under it, the assistant answers as a full-width column of text under its `dsh · time` label, and every block measures itself in text — so wide runes never push a border out of alignment. The header is one row: brand and session title on the left, the workspace path with its git branch on the right; the model is not repeated there (the status row / rail names it). |
55
+ | **Tool activity** | One row per tool: a status mark, the action in the tool's own accent (`read` cyan, `run` gold, `edit` light blue, `todo` mint), and its target. Click the row to expand the full result under a `⎿` gutter. |
56
+ | **Interactive questions** | The model can pause and ask you — option lists, multi-select, free text, and a scrollable plan review — all inline in the terminal. See [Interactive prompts](#interactive-prompts). |
57
+ | **Inline approvals** | `approval/request` prompts are answered with `y` / `n` without leaving the UI; the box shows the action and the reason, with room for origin, risk and details when a `@dsh-std` request carries them. |
58
+ | **Session stats** | One stats strip above the composer — turns/steps, LLM and tool wall time, average TTFT, decode throughput, cache-hit rate, and billed input/output tokens — folded from durable events. See [Session stats and the context meter](#session-stats-and-the-context-meter). |
59
+ | **Stats window** | Click the strip or the context meter, or type `/stats`, for the full session-statistics and token-usage breakdown. |
60
+ | **Context meter** | Live context occupancy (`ctx ▓▓░░ 32K/128K 25%`), with the system/tools/messages composition in the same window. |
61
+ | **Status rail (wide windows)** | From 140 columns a right-hand rail carries the expanded status — model, thinking intensity, context, session figures — plus the session's current goal (`/goal`) and its subagent activity, scrollable with `Shift+↑/↓` or the wheel. Narrow windows keep the bottom status row. See [The status rail](#the-status-rail). |
62
+ | **Thinking intensity** | `Tab` cycles the current model's real reasoning levels; `Ctrl+E` opens a slider. The level is applied per request and persisted. |
63
+ | **Shared settings** | The same host settings namespaces the Web UI uses — general, sessions, per-provider model configuration, credentials — persisted to `$DSH_HOME/settings.yaml`. |
64
+ | **In-app updates** | Detect and switch versions of `@deepseek-ai/dsh` and `dsh-oc-tui` from inside the TUI, with Windows-safe deferred installs. |
65
+ | **Zero-dependency terminal engine** | Raw mode, alternate screen, a diffing cell buffer, truecolor ANSI, CJK-aware widths, SGR + legacy X10 mouse decoding, and IME caret anchoring. |
66
+
67
+ ## Requirements
68
+
69
+ | | |
70
+ | --- | --- |
71
+ | Node.js | >= 22 |
72
+ | dsh CLI | `@deepseek-ai/dsh` — e.g. `npm install -g @deepseek-ai/dsh` |
73
+ | pnpm | on `PATH`; `dsh plugin` forwards to it |
74
+ | Terminal | an interactive terminal (Windows Terminal / ConPTY, iTerm2, GNOME Terminal, …) |
75
+ | Model route | a usable route in `$DSH_HOME/settings.yaml` + `$DSH_HOME/.credentials.yaml` (the same setup the Web GUI uses) |
76
+
77
+ ```sh
78
+ dsh --version
79
+ pnpm --version
80
+ ```
81
+
82
+ **Compatibility.** Verified against dsh `0.2.0-rc.2` and `0.1.2-rc.1` (`0.1.1-rc.2` before that). DSH renamed parts of the session API in 0.1.2 — `Session.events` became `snapshotEvents()` — and this plugin reads whichever accessor the host provides, so one build serves both lines.
83
+
84
+ **dsh (the harness) 0.2.0 is a breaking update — upgrade this plugin first.** From dsh `0.2.0-rc.1` the agent-preset packages were split: the roster provider `@deepseek-ai/dsh-agent-presets` (which shipped its own presets) became `@deepseek-ai/dsh-agent-preset-registry`, and a new `@deepseek-ai/dsh-agent-preset` package is only a *declarative* preset row that injects the service the registry provides. A TUI older than `0.1.5` names the old provider, and dsh reports it at every boot:
85
+
86
+ ```
87
+ dsh: disabling profile plugin row "agent-presets": Plugin @deepseek-ai/dsh-agent-presets@0.1.1-rc.2 is
88
+ incompatible with dsh 0.2.0-rc.2: peerDependencies {…}. Running it may cause crashes or data loss.
89
+ ```
90
+
91
+ The TUI still starts, but Settings → Default preset loses its roster (the row is hidden once no provider mounts it). Updating `dsh-oc-tui` to `0.1.5` or later removes the report: this bundle mounts no roster row at all, because no single package name works on both lines — the old one warns on dsh 0.2.0, the singular never activates, and the registry activates empty unless a host declares presets. A dsh 0.2.0 profile that wants a roster mounts the registry plus its own preset declarations, exactly as `dsh-web-app` does. Nothing about your sessions changes in either direction; `cordis.patch.yml` documents the same decision next to the rows.
92
+
93
+ ## Install
94
+
95
+ ### From npm
96
+
97
+ The package is published on npm as [`dsh-oc-tui`](https://www.npmjs.com/package/dsh-oc-tui). Install it into the `tui` profile:
98
+
99
+ ```sh
100
+ dsh plugin --profile tui add -w dsh-oc-tui
101
+ ```
102
+
103
+ Or install the launcher globally — that puts the `dsh-oc-tui` command on `PATH`, which then boots `dsh --profile tui`:
104
+
105
+ ```sh
106
+ npm install -g dsh-oc-tui
107
+ ```
108
+
109
+ ### Version channels
110
+
111
+ The **npm package** and the **[awesome-dsh-plugin](https://awesome-dsh-plugin.com/zh/p/rayafriandion/dsh-oc-tui/) marketplace entry** both ship **stable releases only** — pre-releases are never published to either. `npm install` therefore gives you the latest stable version, not a release candidate.
112
+
113
+ This README describes the current source tree, which can be ahead of the published release — a feature documented here is only guaranteed to exist in a stable build once that version is on npm.
114
+
115
+ To run a pre-release, or unreleased work from this repository, install it explicitly from source:
116
+
117
+ ```sh
118
+ npm pack # -> dsh-oc-tui-<version>.tgz
119
+ dsh plugin --profile tui add -w ./dsh-oc-tui-<version>.tgz
120
+ ```
121
+
122
+ ### One-command installers
123
+
124
+ The repository ships installers that check Node >= 22, make sure `pnpm` exists, install the plugin into the `tui` profile, and can also add the `dsh-oc-tui` launcher globally.
125
+
126
+ ```sh
127
+ # Linux / macOS
128
+ curl -fsSL https://raw.githubusercontent.com/rayafriandion/dsh-oc-tui/main/install.sh | bash
129
+ ```
130
+
131
+ ```powershell
132
+ # Windows (PowerShell)
133
+ powershell -ExecutionPolicy Bypass -Command "iwr https://raw.githubusercontent.com/rayafriandion/dsh-oc-tui/main/install.ps1 -OutFile install.ps1; & .\install.ps1"
134
+ ```
135
+
136
+ Run `./install.sh` / `.\install.ps1` from a checkout instead, and add `--launcher` / `-Launcher` to also put the `dsh-oc-tui` command on `PATH`. Other flags: `--local` (`-Local`) installs the current checkout, `--source <spec>` (`-Source <spec>`) uses a custom source, `--profile <name>` (`-Profile <name>`) targets another profile.
137
+
138
+ ### From a checkout or tarball
139
+
140
+ ```sh
141
+ npm pack # -> dsh-oc-tui-<version>.tgz
142
+ dsh plugin --profile tui add -w ./dsh-oc-tui-<version>.tgz
143
+ ```
144
+
145
+ `dsh plugin` anchors relative paths to the directory you invoke it from before forwarding to pnpm.
146
+
147
+ ### Why `-w`
148
+
149
+ The profile directory declares itself a pnpm workspace root (`pnpm-workspace.yaml` → `packages: [.]`), so pnpm refuses a bare `add` with `ERR_PNPM_ADDING_TO_ROOT`. `-w` makes the dependency land in the profile's own manifest — which is exactly what it is. `dsh plugin` then reconciles `dsh.profile.bundles` against what is installed.
150
+
151
+ ### What the install does
152
+
153
+ 1. `dsh plugin` initializes `$DSH_HOME/profiles/tui` on first use (`@deepseek-ai/dsh-base` plus an empty user patch layer).
154
+ 2. pnpm installs `dsh-oc-tui` into the profile's `node_modules`.
155
+ 3. Because the package declares `dsh.bundle.patch`, dsh appends `dsh-oc-tui` to `dsh.profile.bundles`.
156
+ 4. `dsh --profile tui` composes the base layer, this bundle's rows, and your own patch — no manual editing required.
157
+
158
+ Verify without booting:
159
+
160
+ ```sh
161
+ dsh --profile tui --dump-config
162
+ ```
163
+
164
+ The dump shows a `# == dsh-oc-tui` layer containing `tui-startup`, `tui-app`, and the `tool-ask-user` row — three rows, no agent-preset one (see the compatibility note above).
165
+
166
+ ## Quick start
167
+
168
+ ```sh
169
+ dsh --profile tui # title screen; your first message creates a session
170
+ dsh --profile tui --resume <sessionId> # resume a persisted session
171
+ dsh --profile tui --model <modelId> # default model for new sessions
172
+ dsh --profile tui --provider <route> # default provider route
173
+ dsh --profile tui --no-sidebar # start without the session rail
174
+ dsh --profile tui --help # the TUI's own flags
175
+ ```
176
+
177
+ The stock launcher hardcodes only `web` and `plugin` as bare subcommands, so `--profile tui` is the intended shape. Want the literal `dsh tui`? Add a shell alias:
178
+
179
+ ```powershell
180
+ function tui { dsh --profile tui @args } # PowerShell $PROFILE
181
+ ```
182
+
183
+ ```bat
184
+ doskey tui=dsh --profile tui $* :: CMD
185
+ ```
186
+
187
+ ### Convenience launcher
188
+
189
+ The package also ships a `dsh-oc-tui` binary that is equivalent to `dsh --profile tui`, but checks first that the profile actually has the plugin installed and prints the one-time install command when it does not.
190
+
191
+ ```sh
192
+ dsh-oc-tui # boot the tui profile
193
+ dsh-oc-tui --profile mytui # boot a different profile
194
+ dsh-oc-tui --help # launcher help
195
+ dsh-oc-tui --version # launcher version
196
+ ```
197
+
198
+ It prefers the `dsh` on `PATH` and falls back to `npx --yes @deepseek-ai/dsh`. Install it with `npm install -g dsh-oc-tui`.
199
+
200
+ | Environment variable | Effect |
201
+ | --- | --- |
202
+ | `DSH_TUI_PROFILE` | Default profile when `--profile` is absent (default `tui`). |
203
+ | `DSH_TUI_SKIP_CHECK` | Set to `1` to skip the profile preflight (advanced installs). |
204
+
205
+ ## Usage
206
+
207
+ ### Keybindings
208
+
209
+ | Key | Action |
210
+ | --- | --- |
211
+ | `Enter` | Send the message. |
212
+ | `Ctrl+Enter` / `Shift+Enter` / `Alt+Enter` | Insert a newline. |
213
+ | `Ctrl+C` | Clear a non-empty prompt, cancel the running turn, or press twice while idle to exit. |
214
+ | `Ctrl+P` | Open Settings. |
215
+ | `Ctrl+E` | Toggle the thinking-intensity slider below the composer. |
216
+ | `Tab` | Session page: cycle the thinking intensity. Settings page: switch the left menu. |
217
+ | `Ctrl+N` | New session. |
218
+ | `Ctrl+D` | In Settings → Manage sessions: delete the focused session (press twice to confirm). |
219
+ | `Ctrl+L` | Clear the transcript view. |
220
+ | `Up` / `Down` | Move the caret across a multi-line prompt; on the first/last row, step through input history. |
221
+ | `Shift+Up` / `Shift+Down` | Scroll the status rail (wide windows); without the rail they act as `Up` / `Down`. |
222
+ | `Left` / `Right` | Move the caret within the input box. |
223
+ | `PgUp` / `PgDn` | Scroll the transcript. |
224
+ | `Esc` | Close the session stats window, the thinking slider, or help; cancel an approval; cancel a running turn; clear the prompt you are typing. |
225
+ | `Esc Esc` | Idle with an empty prompt: open the rewind picker. |
226
+ | `y` / `n` | Answer an inline approval prompt. |
227
+
228
+ **Mouse.** The wheel scrolls the transcript — or the status rail when the pointer rests over it, or the Settings window while it is open. Hold the left button and drag across the transcript to select text, then press the right button to copy the selection.
229
+
230
+ ### Slash commands
231
+
232
+ Built in: `/help` `/settings` `/new` `/resume <id>` `/model <id>` `/provider <route>` `/rewind` `/stats` `/clear` `/cancel` `/quit` (`/exit` also works).
233
+
234
+ **Rewind.** `Esc Esc` (or `/rewind`) lists the prompts of the live session. Restoring the **conversation** forks a new session from the events before the chosen prompt — the parent session is left untouched on disk, exactly as the harness's own `session/fork` does — and the picker lands on the most recent prompt, so `Enter` twice rewinds the last turn. `/rewind <n|last> [conversation|code|both]` runs it without the picker. The fork starts with an **empty inbox**: a cut before a turn also cuts the inbox claim that turn performed, so anything the parent had queued — including the prompt you rewound away from — is not delivered again; it stays in the parent's log, and the result line says `dropped N inherited pending input` when there was any. Restoring **files** is best-effort and fenced: it needs a git worktree (anywhere else the rewind reports `files not restored (not a git worktree)` and changes nothing), it rewrites tracked files from `HEAD` without touching the index, and it removes an untracked file only when the transcript's first recorded write to that path is at or after the rewind point. Everything it overwrites or deletes is copied to `$DSH_HOME/rewind-backups/<sessionId>/<timestamp>/` first, and the result line names that directory. Because the log stores no file contents, a tracked file returns to its last commit, not to its exact state at the rewind point.
235
+
236
+ Harness commands — `/compact`, `/goal`, `/plan`, … — are forwarded to `ctx.commands` and run without a model turn. They need a live session: on the title screen the TUI answers `/<name>: start a session first` instead of dropping the command silently.
237
+
238
+ `/stats` is the TUI's own command: it toggles the [session stats window](#session-stats-and-the-context-meter) and, like the harness commands, needs a live session.
239
+
240
+ ### Interactive prompts
241
+
242
+ **Approvals.** When a tool needs permission, the composer area is replaced by an approval box. The first row carries the action (`Approval · <tool>`) with the key hints riding along when they fit; the explanation the request supplies follows as a `Summary ·` row. `y` allows once, `n` rejects, `Esc` cancels. A request from a `@dsh-std` component can additionally carry `Origin`, `Risk` and `Details` rows — they are drawn when present and omitted when not, a detail marked private shows its label but not its value, and on a narrow terminal the middle yields with a `… N more` marker while the action and the hints always survive. The plugin also honours the effective permission preset, so an auto-approving preset does not prompt at all.
243
+
244
+ An approval that outlives the deadline its caller set disappears on its own. That is an **expiry**, not a cancellation: the caller is told `expired` rather than `cancelled`, so a timeout can never be read as a human decision.
245
+
246
+ **Questions.** The model can ask you directly through the `ask_user_question` tool. The tool is declared by this bundle's `tool-ask-user` row — `dsh-base` mounts the `user-questions` service but not the tool, and a TUI session composes from the base rather than from an agent preset — and it is answered by a modal:
247
+
248
+ | Key | Action |
249
+ | --- | --- |
250
+ | `Up` / `Down` | Move between options (wrapping). |
251
+ | `Space` | Toggle the highlighted option (multi-select) or select it (single choice). |
252
+ | `Enter` | Continue: a single choice is selected and advances; on the free-text row it starts editing; in a multi-select it confirms the toggled set. |
253
+ | any printable key | Jump into the free-text row and start typing. |
254
+ | `PgUp` / `PgDn`, wheel | Scroll a long plan or detail pane. |
255
+ | `Esc` | Defer: decline to answer here (`Esc` while editing returns to the options). |
256
+ | `Ctrl+C` | Still cancels the running turn; the pending question is withdrawn. |
257
+
258
+ Questions are staged one at a time, exactly as the Web UI composer stages them, and the answer encoding is identical: a free-text answer replaces the selection for a single-select question and accompanies it for a multi-select one.
259
+
260
+ A question carrying the `plan-review` intent — what `exit_plan_mode` sends — renders the plan markdown in a scrollable pane above `Approve` / `Keep planning`. Answering `Approve` exits plan mode and the model continues; anything else keeps planning.
261
+
262
+ Deferring is deliberate, not a cancel: with no other answerer the tool reports `no user-questions answerer accepted the request`, which cannot be mistaken for a human choice.
263
+
264
+ ### Thinking intensity
265
+
266
+ The effective level sits on the composer's top-right border as the bare level name, diagonally opposite the `provider · model` label.
267
+
268
+ - `Tab` on the session page cycles the levels of the **current model** (wrapping strongest → weakest); `Shift+Tab` steps backwards.
269
+ - `Ctrl+E` opens a slider below the composer: `Tab` or `←`/`→` adjust and persist, `Esc` or `Ctrl+E` close it.
270
+ - Levels come from the provider adapter (`ctx.llm.resolveModelInfo`), so a boolean-thinking model shows exactly its two ends, DeepSeek's `Off`/`High`/`Max` shows those three, and a full-range model shows every advertised level — never a blanket `none → max` scale.
271
+
272
+ The choice is applied to the session's requests through the `agent/request` waterfall and stored in `agent-default-model.reasoningEffort`.
273
+
274
+ ### Session stats and the context meter
275
+
276
+ The row above the composer is the **session stats strip**, the TUI's counterpart of the web chat's stats line: the same figures in the same order, separated by `│`:
277
+
278
+ ```
279
+ ▤ 1 turn · 2 steps│LLM 1.3s · tools 1.2s│TTFT avg 400ms · 20.0 tok/s│cache 55%│in 110 · out 30
280
+ ```
281
+
282
+ - `turns` / `steps` count **closed steps** (`step/end`), so failed, cancelled, and max-tokens steps count too. `LLM` is `step/start` → assembled reply; `tools` pairs `tool/call` → `tool/result`.
283
+ - `TTFT avg` is the per-step average time to first token; `tok/s` is decode throughput (first token → assembled reply over the reported output tokens).
284
+ - `cache` is the prompt-side cache-hit share (cache reads over all billed input); `in` / `out` are the session's billed input and output tokens.
285
+ - A narrow terminal drops trailing groups **whole** and marks the elision with `│…` rather than cutting a figure in half; the window always carries the complete set.
286
+ - A session with no closed step and no billed tokens hides the strip entirely and gives the row back to the transcript.
287
+
288
+ The whole strip is a click target. Clicking it — or the context meter at the right end of the status row (`ctx ▓▓░░ 32K/128K 25%`), or typing `/stats` — opens the **session stats window**; click again, click elsewhere, or press `Esc` to close. The window breaks the same line into labelled rows (`usage` / `duration` / `speed` / `tokens` / `cache`) and adds the context-occupancy reading with its heuristic composition — system prompt, tools, and messages — matching the Web UI's ContextMeter dialog.
289
+
290
+ Figures come from the same sources as the Web UI, projection-first with the plugin's own fold as the fallback: `tokenUsage`, `contextPressure`, and `contextBreakdown` are mounted by `dsh-base`'s token-meter row, while `sessionStats` is mounted only by the web app bundle — so the TUI folds the durable `step` / `chunk` / `message` / `tool` events by the same rules. A missing projection falls back for that figure alone, and a figure nobody can supply stays hidden instead of printing a zero.
291
+
292
+ ### The status rail
293
+
294
+ From **140 columns** a right-hand rail takes over the status figures, OpenCode-style: the session title pins to the top, scrollable sections fill the middle — `MODEL` (model, provider), `THINKING` (current level and its real range), `CONTEXT` (occupancy bar, tokens, billing, composition), `SESSION` (the stats strip's figures, expanded), `GOAL`, `SUBAGENTS` — and the running builds (`dsh x.y.z · tui a.b.c`) pin to the bottom. Sections with no data drop out whole.
295
+
296
+ `GOAL` and `SUBAGENTS` read the harness's native surfaces: the `goal` projection registered by `dsh-goal` (objective, phase, rounds, blocker reason), and `ctx.subagents.listDescendants()` for the delegation tree rooted at the open session, refreshed on the scoped `subagent/start` / `subagent/end` edges. Both services are optional — a composition without them leaves those sections empty.
297
+
298
+ When the sections outgrow the window a scrollbar appears in the rail's right edge; `Shift+↑` / `Shift+↓` or the wheel over the rail scrolls it. Clicking the `CONTEXT` section opens the session stats window (the seat the bottom-row meter vacates). Below 140 columns the rail disappears and the pre-rail layout — model on the status row, the stats strip above the composer — returns unchanged.
299
+
300
+ ### Settings
301
+
302
+ `Ctrl+P` opens a settings menu over the same host settings namespaces as the Web UI, persisted through `ctx.settings` to `$DSH_HOME/settings.yaml`. A left menu splits it into three tabs (`Tab` or click to switch):
303
+
304
+ - **Main** — General (busy-Enter behaviour, permission preset), Sessions (new session, manage sessions), System (provider API hints, update-manager shortcut, settings file path).
305
+ - **Model** — the default provider/model/reasoning choice, then one group per provider holding its URL, API key, and model list. Pressing `Enter` on **Models** fetches the provider's advertised catalog (`ctx.llm.discoverModels`) and opens a checkbox window; pressing `Enter` on a listed model makes it the default route.
306
+ - **Update** — see [In-app updates](#in-app-updates).
307
+
308
+ Only providers you actually added (present in your user settings layer) are listed; a provider that was never added stays hidden. **Default preset** is shown only when the profile mounts an agent-preset roster, because the choice list is that roster — and this plugin deliberately mounts none (see the compatibility note above), so on a stock `tui` profile the row is absent rather than empty. Where a roster *is* mounted, note that a TUI session composes process-wide from the base, so the stored default applies where a session is created from a preset. Web-UI-only options (`ui-theme` appearance, `locale`) are not shown because they have no effect in the TUI.
309
+
310
+ ### In-app updates
311
+
312
+ `Ctrl+P → Update` shows the installed versions of `@deepseek-ai/dsh` and `dsh-oc-tui`, the latest npm dist-tag, and a status line that only ever targets **stable** releases:
313
+
314
+ - `Update available → x.y.z` — a newer stable release exists.
315
+ - `Up to date` — nothing to do.
316
+ - `No stable release — pick from Versions` — the registry has no stable release yet; pick one manually.
317
+ - `Install damaged — reinstall below` — the global dsh tree is in a mixed old/new state; reinstall it.
318
+
319
+ `Enter` on a package's **Versions** row opens the full registry list (newest first, `[latest]`/`[next]`/other tags and `(installed)` colour-coded) where you can pick any version — including pre-releases. A picked version is **downloaded first**: the tarball streams into `$DSH_HOME/updates` with live byte progress, `Esc` cancels mid-flight, and the sha512 registry hash is checked before the file is kept. Applying it is a **restart**: after the y/n confirm the TUI spawns a detached applier, exits, and that child installs from the staged tarball, verifies the on-disk version, and relaunches dsh in the same terminal with the same arguments (`--profile tui --resume …` included) and working directory.
320
+
321
+ Version rows are tagged `[cached]` when that tarball is already on disk — a version you downloaded before (or rolled back from) needs no new download — and `[staged]` for the version waiting to be applied. A staged install also shows up on the Update tab as **Restart to apply x.y.z** (one `Enter`) and **Discard downloaded x.y.z**, and its state survives a crash: the row is only offered while the staged tarball actually exists. `Check now` re-reads the registry; `Startup check` toggles the silent boot-time stable-release check.
322
+
323
+ <details>
324
+ <summary><strong>Why an update never installs while the TUI runs</strong></summary>
325
+
326
+ Updating a package that a live `dsh` process has loaded can *silently corrupt* it: on Windows the process holds memory-mapped native DLLs, npm's directory replace fails, and the result is an old/new hybrid tree — while npm still exits 0. The crash of 2026-09-04 happened exactly that way. Migrating the whole flow to "download, then restart to apply" removed the window instead of narrowing it, on every platform:
327
+
328
+ 1. **Nothing is installed until this process is gone.** The download is only a file; the applier polls this pid, and the TUI refuses to start the restart while any *other* dsh process is alive.
329
+ 2. **The on-disk version decides success**, not npm's exit code. A mismatch is reported as `Install damaged — reinstall below` with repair instructions, never as a bogus success.
330
+ 3. **The staged tarball is kept**, so a rollback to any downloaded version is a verified cache hit through the same code path as an update.
331
+
332
+ The applier writes the outcome to `$DSH_HOME/tui-dsh-install.json`, which the Update page reads and drains on the next visit — so the result of an interrupted install is still reported.
333
+ </details>
334
+
335
+ ## How it works
336
+
337
+ - The plugin is a Cordis function plugin loaded by the `tui` profile. `lib/startup.js` parses the app's flags and provides the `tuiStartup` service; `lib/index.js` owns the UI loop.
338
+ - `lib/term.js` is a zero-dependency terminal engine: raw mode, alternate screen, a diffing cell buffer, and a key decoder (truecolor ANSI, CJK-aware widths). It parks the hidden terminal cursor at the input caret so the OS IME anchors its composition window inside the composer, and it understands both SGR and legacy X10 mouse encodings so wheel and click bytes can never leak into the input text. The cell buffer is also the single chokepoint where every painted string is sanitized: C0, DEL and C1 characters are replaced with a visible `\uFFFD`, so a tool name, a path, a title or a `@dsh-std` request's action can never smuggle an escape sequence into the output stream. The width-preserving placeholder is deliberate — dropping the character instead would silently change layout, whereas a visible one makes the injection obvious.
339
+ - `lib/ui.js` is the responsive view model and renderer (DeepSeek blue-white theme, session rail, transcript, multiline composer, command suggestions, telemetry footer). Transcript lines are cached per block, only the visible window is materialised each frame, streaming paints are coalesced, and the live block re-renders on a short throttle — so render cost stays bounded as history grows. Thinking collapses to keep the transcript readable, and running tools and thinking blocks animate with flowing spinners.
340
+ - `lib/metrics.js` folds durable step/chunk/message events into token, TTFT, throughput, and cache-hit metrics.
341
+ - `lib/interrupt.js` owns the clear/cancel/double-exit state machine used by stdin and `SIGINT`.
342
+ - `lib/markdown.js` renders model output (headings, lists, quotes, code, inline spans) to styled lines.
343
+ - `lib/updates.js` isolates every npm/pnpm interaction for the Update tab — registry queries, dependency-free semver comparison, dsh install detection, and installs — all through `child_process.spawn`, never `spawnSync`.
344
+ - Agents are created and resumed through `ctx.agents`, the transcript is rebuilt from the session's durable log and fed live by `session/event` (including `assistant/chunk`), model defaults come from `ctx.agentDefaultModel`, and approvals answer the `approval/request` waterfall inline.
345
+ - `ask_user_question` is answered over the `user-questions/request` waterfall: a scoped Cordis waterfall where the modal either returns an answer or delegates with `next()`. An aborted request rejects so the service reports its own `ASK_ABORTED`; requests addressed to another agent are delegated untouched.
346
+
347
+ ## Graphics rendering
348
+
349
+ Images and mermaid diagrams render as real pixels. On boot the TUI probes the terminal itself — DA1 attributes, an XTVERSION name query, a kitty graphics query, and a cell-size query — and every probe is an escape sequence the *client* terminal answers, so detection works unchanged over SSH where environment variables do not propagate.
350
+
351
+ | Terminal | Protocol used |
352
+ | --- | --- |
353
+ | kitty, Ghostty | kitty graphics (transmitted once per image, moved by reference — free on scroll) |
354
+ | WezTerm | kitty graphics, or iTerm2 inline images |
355
+ | iTerm2, Konsole, mintty | iTerm2 inline images / sixel |
356
+ | Windows Terminal ≥ 1.22 | sixel |
357
+ | GNOME Terminal, older xterm, anything else | truecolor half-block preview (▀, every font has the glyphs) |
358
+
359
+ - **Sources.** A pasted image (bytes are already in hand), a stored attachment on resume/replay (read back through the attachment store), a `data:` URL, and a standalone markdown image (`![alt](https://…)`), plus tool-result images such as screenshots.
360
+ - **Budgets.** Images scale to fit the transcript width and a row cap (Settings → Graphics → Image height cap, default 20 rows); GIFs render their first frame.
361
+ - **Mermaid.** A ` ```mermaid ` fence draws as **native box-drawing art** whenever the grammar is one the built-in engine knows (flowchart, sequence, class, state, ER, pie, mindmap, timeline, gitGraph) — text in the transcript, sharp at any font size, selectable and searchable, with emoji and CJK labels measured the way the terminal draws them. Only a grammar the engine does not draw falls back to the provider chain: local `mmdc` (mermaid-cli) if it is on `PATH`, otherwise the mermaid.ink service (10 s timeout, disk-cached) — which sends the diagram text to a third party, hence the setting. A diagram wider than the pane reports the width it needs and shows its source instead of a raster. `Settings → Graphics → Mermaid rendering` offers `auto` / `local` / `off`; `off` keeps every diagram as its highlighted source, and every failure degrades to source, never a broken frame.
362
+ - **Settings.** `Graphics rendering` (`auto`/`off`), `No-protocol fallback` (`halfblock`/`chip`), `Image height cap (rows)`, `Mermaid rendering` (`auto`/`local`/`off`). Changes apply the moment they are saved.
363
+ - **Dependencies.** Rendering is pure npm: `sharp` decodes and scales (prebuilt binaries on all three platforms), [`lovely-mermaid`](https://www.npmjs.com/package/lovely-mermaid) lays out mermaid diagrams as terminal text, and the kitty/iTerm2/sixel encoders are part of the plugin. No external binaries are required; `mmdc` is optional.
364
+
365
+ ## @dsh-std interop
366
+
367
+ The package carries a [`dsh-plugin.json`](dsh-plugin.json) manifest and a facet at `lib/facet.js`, so a `@dsh-std` host can discover it without running any code. **What ships today is that static manifest and the preflight surface; the runtime protocol support is currently gated off by an upstream limitation.** The plugin is designed as a **host and Presentation provider** in that ecosystem, not a protocol consumer — its facet is wired to publish `presentation.dsh/v1alpha1` `UserInteraction` / `Notification` / `CopyText` and `commands.dsh/v1alpha1` `CommandRuntime` for other components to drive. Its one `requires.contracts` entry is the `commands.dsh/v1alpha1` `Command` resource, declared **`optional: true`**. The entry is retained for a future consumer and is inert today — nothing calls `protocols.client(...)`, and its own command line is parsed from the cordis `ctx.commands` service, so the TUI behaves identically whether or not a Command provider exists. An unsatisfiable non-optional requirement would be a hard activation failure rather than a warning, and the `optional` flag is what keeps the declaration honest about that. But a Community v0.15 manifest **cannot declare protocol supports**, and `@dsh-std/lifecycle`'s `implement` — which *is* `stageProtocol` — refuses to stage a support the facet has not declared, so the facet stages **nothing**. The protocol shims are written and test-covered; they go live unchanged once upstream closes the gap. See [docs/dsh-std-接入说明.md](docs/dsh-std-接入说明.md#阻断性发现本插件当前不发布任何协议-support) for the layer-by-layer evidence.
368
+
369
+ **The facet does not own the TUI's lifecycle, and the double activation is avoided by design.** `@dsh-std/adapter-dsh`'s `mountProfileComponents` mounts the facet of every `dsh-plugin.json` it finds in a profile's `dependencies`, without checking whether the cordis bundle rows already loaded that component — and this plugin *is* a profile dependency that `cordis.patch.yml` also activates. Since the TUI seizes the terminal (raw mode, alternate screen, mouse tracking), a manifest alone would start it twice. The facet is therefore an **interop shell**: when it does stage protocol support, it forwards protocol calls through `lib/bridge.js` to whichever TUI instance is live, and it reports `{ state: 'degraded' }` when none is — or when it staged nothing at all. A `dsh-plugin.json` whose facet never starts the TUI is working as intended, not broken.
370
+
371
+ > 中文说明:[docs/dsh-std-接入说明.md](docs/dsh-std-接入说明.md) — 本插件在 `@dsh-std` 生态里扮演 **Host 与 Presentation 提供方**;facet 只是协议外壳,**不拥有 TUI 的生命周期**,双激活已由设计规避。**但当前上游下它一条协议都不发布**:Community v0.15 清单无法声明 supports,而 lifecycle 要求先声明才能暂存,因此 Phase B(运行时互操作)休眠、Phase A(静态清单与预检)可交付——见该文档开头的「阻断性发现」。该文档还记录了清单字段、pin 的 `rc` 版本、`lib/bridge.js` 的契约、adapter 的 staging 契约、明确不实现的协议,以及评审确认的已知行为。
372
+
373
+ ## Development
374
+
375
+ AI agents working in this checkout should read [AGENTS.md](AGENTS.md) first — it
376
+ records the build/install loop as a rule rather than a suggestion.
377
+
378
+ ```sh
379
+ npm run check # node --check over lib/, bin/
380
+ npm test # standalone smoke tests (no dsh needed)
381
+ ```
382
+
383
+ **The install is a build, so edit → build → install.** The profile contains a *tarball* copy of the plugin, and the profile's HMR root is the profile directory, not the plugin directory — editing this checkout changes nothing until you repack and reinstall:
384
+
385
+ ```sh
386
+ npm pack # -> dsh-oc-tui-<version>.tgz
387
+ dsh plugin --profile tui remove -w dsh-oc-tui # detach the old copy FIRST
388
+ Remove-Item .\*.tgz # then drop the stale tarball
389
+ npm pack # repack the current tree
390
+ Copy-Item .\dsh-oc-tui-<version>.tgz $env:USERPROFILE\.dsh\profiles\tui\
391
+ dsh plugin --profile tui add -w .\dsh-oc-tui-<version>.tgz
392
+ Remove-Item $env:USERPROFILE\.dsh\profiles\tui\dsh-oc-tui-<version>.tgz
393
+ ```
394
+
395
+ Three things that will bite you:
396
+
397
+ - **Detach before deleting.** pnpm resolves the profile's existing `file:` dependency while adding, so a dependency pointing at a deleted tarball aborts the whole install with `ENOENT`.
398
+ - **The tarball has to be reachable from the profile directory while you add it.** dsh hands package specs to pnpm with `cwd` pinned to the profile directory, and pnpm 9.15.9 then looks for the tarball *by name inside the profile* rather than at the absolute path it was given: the `add` fails with `ENOENT: no such file or directory, open '…\.dsh\profiles\tui\dsh-oc-tui-<version>.tgz'`. Copy it in first; dsh writes the absolute path into the profile manifest, so the copy comes straight back out afterwards.
399
+ - **Remove before adding, even at the same version.** dsh appends a bundle to `dsh.profile.bundles` only for dependencies that were *not* already installed, so adding over an existing copy installs the files and silently skips the bundle row — the profile then boots dsh-base with no TUI.
400
+
401
+ Verify the swap actually landed — the version string proves nothing:
402
+
403
+ ```powershell
404
+ foreach ($rel in @('lib\index.js','lib\ui.js','lib\util.js','lib\term.js','lib\metrics.js',
405
+ 'lib\interrupt.js','lib\web-settings.js','lib\updates.js','lib\markdown.js',
406
+ 'lib\startup.js','bin\dsh-oc-tui.js','cordis.patch.yml')) {
407
+ $a = (Get-FileHash ".\$rel").Hash
408
+ $b = (Get-FileHash "$env:USERPROFILE\.dsh\profiles\tui\node_modules\dsh-oc-tui\$rel").Hash
409
+ if ($a -ne $b) { "DIFFERS: $rel" }
410
+ }
411
+ ```
412
+
413
+ The reverse failure is just as silent and worse: current files with no `dsh-oc-tui` row in `dsh.profile.bundles` means dsh-base boots with no TUI. Confirm the layer composes:
414
+
415
+ ```sh
416
+ dsh --profile tui --dump-config | grep -A2 tui-app
417
+ ```
418
+
419
+ Then boot it for real. Reaching the title screen is not enough — the session-open path is where host API breaks surface, so send a message. Test `--resume` separately, because it is an apply-time path that can lose a startup race the post-boot paths win.
420
+
421
+ For a zero-install bootstrap that skips packaging entirely, create the profile once and point its patch at this checkout:
422
+
423
+ ```sh
424
+ dsh --profile tui --dump-config # initializes the base profile once
425
+ ```
426
+
427
+ ```yaml
428
+ # $DSH_HOME/profiles/tui/cordis.patch.yml
429
+ - insert:
430
+ - id: tui-startup
431
+ name: 'file:///path/to/dsh-oc-tui/lib/startup.js'
432
+ - id: tui-app
433
+ name: 'file:///path/to/dsh-oc-tui/lib/index.js'
434
+ config:
435
+ sidebar: true
436
+ showReasoning: true
437
+ ```
438
+
439
+ The plugin's dsh imports resolve through the shared `$DSH_HOME/profiles/node_modules` fallback that dsh maintains, so nothing has to be installed into the plugin directory.
440
+
441
+ ## Troubleshooting
442
+
443
+ | Symptom | Cause and fix |
444
+ | --- | --- |
445
+ | `pnpm failed in profile directory` / `ERR_PNPM_ADDING_TO_ROOT` | The profile is a pnpm workspace root; add `-w` to the `add`/`remove` command. |
446
+ | `ENOENT: … dsh-oc-tui-<v>.tgz` during install | The profile still references a tarball you deleted. `dsh plugin --profile tui remove -w dsh-oc-tui`, then add again. |
447
+ | `pnpm not found on PATH` | Install pnpm (`npm install -g pnpm`) and retry. |
448
+ | `--dump-config` has no TUI layer | The install did not complete, or the package name is misspelled. Re-run the `add` and check `dsh.profile.bundles`. |
449
+ | Exits immediately / no UI | stdin and stdout must both be a TTY — do not pipe or redirect. Then verify the model route and credentials exist. |
450
+ | `--resume` or Manage sessions unavailable | Both need the shared `sessionQuery` service; keep `@deepseek-ai/dsh-base` first in `dsh.profile.bundles`. |
451
+ | `no agent factory registered` on `--resume` | A startup race with the agent-loop row; current builds retry it. On an older build, run `/resume <id>` after boot instead. |
452
+ | Loader errors after updating dsh (`State`, `./internal`) | The global dsh install is a mixed old/new tree. Close every dsh process and reinstall: `npm install -g @deepseek-ai/dsh@<version>`. |
453
+ | Source edits have no effect | The installed copy is a tarball; repack and reinstall (see [Development](#development)). |
454
+
455
+ More detail, in Chinese: [docs/用户手册.md](docs/用户手册.md).
456
+
457
+ ## Known limitations
458
+
459
+ - IME composition is not exposed by the zero-dependency terminal engine yet. Pasted images are: a bracketed paste of raw image bytes, a `data:image/...;base64,...` URL, a local image path, or an image URL — each becomes an attachment and renders in the transcript (see [Graphics rendering](#graphics-rendering)); pasting text nothing recognizes asks the terminal for its clipboard (OSC 52).
460
+ - The plugin does not hot-reload: the profile's HMR root is the profile directory, so a running TUI keeps the copy it booted with.
461
+ - `dsh tui` as a bare subcommand needs a shell alias — the stock launcher hardcodes only `web` and `plugin`.
462
+ - Harness slash commands need a live session; on the title screen the TUI tells you to start a session first.
463
+ - Deferring a question with `Esc` does not cancel the tool call — it delegates, and with no other answerer the tool call fails. Per-question skip (as the Web UI composer offers) is not implemented.
464
+ - `--resume`, Settings → Manage sessions, the context meter, and the stats strip depend on services mounted by `@deepseek-ai/dsh-base` (`sessionQuery`, `sessionProjections`); a hand-built profile must provide them. The `sessionStats` projection is a web-app-layer row, so the TUI folds those figures from the session log itself when no profile mounts it.
465
+ - An update restart only waits for the TUI that scheduled it: the "no other dsh process" check runs *before* you confirm, so a `dsh web` window opened afterwards can still overlap the install. Close other dsh windows first.
466
+ - Two `@dsh-std` presentation surfaces have no caller today, because the runtime protocol support is gated off (see [@dsh-std interop](#dsh-std-interop)). A standard `secret-input` request would open a masked prompt with `minLength`/`maxLength` bounds, and a standard `ApprovalRequest` is the only thing that populates the approval box's `Origin` / `Risk` / `Details` rows. Both paths are written and integration-tested; neither can be reached from a normal `dsh --profile tui` session, which is why the harness-shaped approval box shows only the action and the summary.
467
+ - Graphics: an image whose top rows are scrolled out of the transcript window shows as blank placeholder rows until it fits fully again (kitty placements cannot anchor off-screen); GIFs animate their first frame only; sixel/iTerm2 payloads are re-emitted when an image's row moves, which is fine locally and throttled on slow SSH links.
468
+
469
+ ## Layout
470
+
471
+ ```
472
+ lib/index.js plugin entry: agents, events, input, commands, approvals, user questions
473
+ lib/startup.js command-line provider (tuiStartup service)
474
+ lib/term.js terminal engine (raw mode, screen, key decoding, image slab emission)
475
+ lib/caps.js terminal capability probing (DA1, XTVERSION, kitty graphics, cell size)
476
+ lib/image.js image pipeline (decode/scale + halfblock, sixel, kitty, iTerm2 encoders)
477
+ lib/mermaid.js mermaid provider chain (mmdc → mermaid.ink → highlighted fallback)
478
+ lib/ui.js responsive view model + renderer (flat transcript, tool rows, question modal)
479
+ lib/metrics.js whole-session stats + token usage fold (web stats strip / tokenUsage port)
480
+ lib/interrupt.js Ctrl+C lifecycle state
481
+ lib/web-settings.js shared WebUI settings projection (incl. the tui-graphics namespace)
482
+ lib/updates.js in-app update manager (npm registry, download + staging, install verification)
483
+ lib/markdown.js markdown-it token stream -> styled lines (tables, hljs, links)
484
+ lib/util.js text/display helpers
485
+ lib/bridge.js live-TUI registry the @dsh-std facet forwards through
486
+ lib/facet.js @dsh-std facet entry (interop shell; never starts the TUI)
487
+ lib/std/ @dsh-std protocol adapters (adapt, presentation, commands, command-list)
488
+ bin/dsh-oc-tui.js convenience launcher for `dsh --profile tui`
489
+ bin/apply-update.cjs detached applier: waits for the TUI to exit, installs the staged tarball, relaunches dsh
490
+ install.sh one-command installer (Linux/macOS)
491
+ install.ps1 one-command installer (Windows)
492
+ dsh-plugin.json @dsh-std component manifest (discovery + preflight only)
493
+ cordis.patch.yml bundle patch layer (TUI rows, ask-user tool; no agent-preset row, with the reason in a comment)
494
+ docs/用户手册.md Chinese user manual
495
+ docs/dsh-std-接入说明.md @dsh-std interop scope and constraints (Chinese)
496
+ tests/smoke.test.mjs standalone smoke tests
497
+ ```
498
+
499
+ ## License
500
+
501
+ [LGPL-3.0-or-later](LICENSE).
502
+
503
+ **Third-party.** [`lovely-mermaid`](https://www.npmjs.com/package/lovely-mermaid) 0.3.3 (Apache-2.0, © Alexey Zaytsev / ccch1mneyyy) lays out mermaid diagrams as terminal text; its license ships in its package, and its source is at [github.com/ccch1mneyyy/lovely-mermaid](https://github.com/ccch1mneyyy/lovely-mermaid).