dsh-oc-tui 0.1.2 → 0.1.4

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,396 +1,431 @@
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
- - [Context meter and telemetry](#context-meter-and-telemetry)
28
- - [Settings](#settings)
29
- - [In-app updates](#in-app-updates)
30
- - [How it works](#how-it-works)
31
- - [Development](#development)
32
- - [Troubleshooting](#troubleshooting)
33
- - [Known limitations](#known-limitations)
34
- - [Layout](#layout)
35
- - [License](#license)
36
-
37
- ## Features
38
-
39
- | | |
40
- | --- | --- |
41
- | **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. |
42
- | **Live streaming** | Assistant text and reasoning stream token by token; thinking renders in its own collapsible box that stays collapsed while streaming. |
43
- | **Tool activity** | Tool cards with a one-line summary (`read src/app.ts`, `run npm test`), flowing spinners while running, and markdown-rendered results. |
44
- | **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). |
45
- | **Inline approvals** | `approval/request` prompts are answered with `y` / `n` without leaving the UI. |
46
- | **Telemetry footer** | Session tokens, average time to first token, decode throughput, and cache-hit rate, folded from durable events. |
47
- | **Context meter** | Live context occupancy (`ctx ▓▓░░ 32K/128K 25%`) with a click-through composition breakdown. |
48
- | **Thinking intensity** | `Tab` cycles the current model's real reasoning levels; `Ctrl+E` opens a slider. The level is applied per request and persisted. |
49
- | **Shared settings** | The same host settings namespaces the Web UI uses — general, sessions, per-provider model configuration, credentials persisted to `$DSH_HOME/settings.yaml`. |
50
- | **In-app updates** | Detect and switch versions of `@deepseek-ai/dsh` and `dsh-oc-tui` from inside the TUI, with Windows-safe deferred installs. |
51
- | **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. |
52
-
53
- ## Requirements
54
-
55
- | | |
56
- | --- | --- |
57
- | Node.js | >= 22 |
58
- | dsh CLI | `@deepseek-ai/dsh` — e.g. `npm install -g @deepseek-ai/dsh` |
59
- | pnpm | on `PATH`; `dsh plugin` forwards to it |
60
- | Terminal | an interactive terminal (Windows Terminal / ConPTY, iTerm2, GNOME Terminal, …) |
61
- | Model route | a usable route in `$DSH_HOME/settings.yaml` + `$DSH_HOME/.credentials.yaml` (the same setup the Web GUI uses) |
62
-
63
- ```sh
64
- dsh --version
65
- pnpm --version
66
- ```
67
-
68
- **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.
69
-
70
- ## Install
71
-
72
- ### From npm
73
-
74
- The package is published on npm as [`dsh-oc-tui`](https://www.npmjs.com/package/dsh-oc-tui). Install it into the `tui` profile:
75
-
76
- ```sh
77
- dsh plugin --profile tui add -w dsh-oc-tui
78
- ```
79
-
80
- Or install the launcher globally — that puts the `dsh-oc-tui` command on `PATH`, which then boots `dsh --profile tui`:
81
-
82
- ```sh
83
- npm install -g dsh-oc-tui
84
- ```
85
-
86
- ### Version channels
87
-
88
- 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.
89
-
90
- 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.
91
-
92
- To run a pre-release, or unreleased work from this repository, install it explicitly from source:
93
-
94
- ```sh
95
- npm pack # -> dsh-oc-tui-<version>.tgz
96
- dsh plugin --profile tui add -w ./dsh-oc-tui-<version>.tgz
97
- ```
98
-
99
- ### One-command installers
100
-
101
- 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.
102
-
103
- ```sh
104
- # Linux / macOS
105
- curl -fsSL https://raw.githubusercontent.com/rayafriandion/dsh-oc-tui/main/install.sh | bash
106
- ```
107
-
108
- ```powershell
109
- # Windows (PowerShell)
110
- powershell -ExecutionPolicy Bypass -Command "iwr https://raw.githubusercontent.com/rayafriandion/dsh-oc-tui/main/install.ps1 -OutFile install.ps1; & .\install.ps1"
111
- ```
112
-
113
- 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.
114
-
115
- ### From a checkout or tarball
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
- `dsh plugin` anchors relative paths to the directory you invoke it from before forwarding to pnpm.
123
-
124
- ### Why `-w`
125
-
126
- 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.
127
-
128
- ### What the install does
129
-
130
- 1. `dsh plugin` initializes `$DSH_HOME/profiles/tui` on first use (`@deepseek-ai/dsh-base` plus an empty user patch layer).
131
- 2. pnpm installs `dsh-oc-tui` into the profile's `node_modules`.
132
- 3. Because the package declares `dsh.bundle.patch`, dsh appends `dsh-oc-tui` to `dsh.profile.bundles`.
133
- 4. `dsh --profile tui` composes the base layer, this bundle's rows, and your own patch — no manual editing required.
134
-
135
- Verify without booting:
136
-
137
- ```sh
138
- dsh --profile tui --dump-config
139
- ```
140
-
141
- The dump shows a `# == dsh-oc-tui` layer containing `tui-startup`, `tui-app`, the `agent-presets` roster row, and the `tool-ask-user` row.
142
-
143
- ## Quick start
144
-
145
- ```sh
146
- dsh --profile tui # title screen; your first message creates a session
147
- dsh --profile tui --resume <sessionId> # resume a persisted session
148
- dsh --profile tui --model <modelId> # default model for new sessions
149
- dsh --profile tui --provider <route> # default provider route
150
- dsh --profile tui --no-sidebar # start without the session rail
151
- dsh --profile tui --help # the TUI's own flags
152
- ```
153
-
154
- 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:
155
-
156
- ```powershell
157
- function tui { dsh --profile tui @args } # PowerShell $PROFILE
158
- ```
159
-
160
- ```bat
161
- doskey tui=dsh --profile tui $* :: CMD
162
- ```
163
-
164
- ### Convenience launcher
165
-
166
- 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.
167
-
168
- ```sh
169
- dsh-oc-tui # boot the tui profile
170
- dsh-oc-tui --profile mytui # boot a different profile
171
- dsh-oc-tui --help # launcher help
172
- dsh-oc-tui --version # launcher version
173
- ```
174
-
175
- It prefers the `dsh` on `PATH` and falls back to `npx --yes @deepseek-ai/dsh`. Install it with `npm install -g dsh-oc-tui`.
176
-
177
- | Environment variable | Effect |
178
- | --- | --- |
179
- | `DSH_TUI_PROFILE` | Default profile when `--profile` is absent (default `tui`). |
180
- | `DSH_TUI_SKIP_CHECK` | Set to `1` to skip the profile preflight (advanced installs). |
181
-
182
- ## Usage
183
-
184
- ### Keybindings
185
-
186
- | Key | Action |
187
- | --- | --- |
188
- | `Enter` | Send the message. |
189
- | `Ctrl+Enter` / `Shift+Enter` / `Alt+Enter` | Insert a newline. |
190
- | `Ctrl+C` | Clear a non-empty prompt, cancel the running turn, or press twice while idle to exit. |
191
- | `Ctrl+P` | Open Settings. |
192
- | `Ctrl+E` | Toggle the thinking-intensity slider below the composer. |
193
- | `Tab` | Session page: cycle the thinking intensity. Settings page: switch the left menu. |
194
- | `Ctrl+N` | New session. |
195
- | `Ctrl+D` | In Settings Manage sessions: delete the focused session (press twice to confirm). |
196
- | `Ctrl+L` | Clear the transcript view. |
197
- | `Up` / `Down` | Move the caret across a multi-line prompt; on the first/last row, step through input history. |
198
- | `Left` / `Right` | Move the caret within the input box. |
199
- | `PgUp` / `PgDn` | Scroll the transcript. |
200
- | `Esc` | Close the context-meter panel, the thinking slider, or help; cancel an approval. |
201
- | `y` / `n` | Answer an inline approval prompt. |
202
-
203
- **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.
204
-
205
- ### Slash commands
206
-
207
- Built in: `/help` `/settings` `/new` `/resume <id>` `/model <id>` `/provider <route>` `/clear` `/cancel` `/quit` (`/exit` also works).
208
-
209
- 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.
210
-
211
- ### Interactive prompts
212
-
213
- **Approvals.** When a tool needs permission, the composer area shows `Approval · <tool> · y allow / n deny`. `y` allows once, `n` rejects, `Esc` cancels. The plugin also honours the effective permission preset, so an auto-approving preset does not prompt at all.
214
-
215
- **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:
216
-
217
- | Key | Action |
218
- | --- | --- |
219
- | `Up` / `Down` | Move between options (wrapping). |
220
- | `Space` | Toggle the highlighted option (multi-select) or select it (single choice). |
221
- | `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. |
222
- | any printable key | Jump into the free-text row and start typing. |
223
- | `PgUp` / `PgDn`, wheel | Scroll a long plan or detail pane. |
224
- | `Esc` | Defer: decline to answer here (`Esc` while editing returns to the options). |
225
- | `Ctrl+C` | Still cancels the running turn; the pending question is withdrawn. |
226
-
227
- 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.
228
-
229
- 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.
230
-
231
- 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.
232
-
233
- ### Thinking intensity
234
-
235
- The effective level sits on the composer's top-right border as the bare level name, diagonally opposite the `provider · model` label.
236
-
237
- - `Tab` on the session page cycles the levels of the **current model** (wrapping strongest → weakest); `Shift+Tab` steps backwards.
238
- - `Ctrl+E` opens a slider below the composer: `Tab` or `←`/`→` adjust and persist, `Esc` or `Ctrl+E` close it.
239
- - 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.
240
-
241
- The choice is applied to the session's requests through the `agent/request` waterfall and stored in `agent-default-model.reasoningEffort`.
242
-
243
- ### Context meter and telemetry
244
-
245
- The status row carries a live context-occupancy bar fed by the token-meter `contextPressure` projection — the same source as the Web UI's composer ring: current context length over the model's context window, shifting to the warning and error palette as occupancy climbs. Clicking it opens a breakdown panel (click again or `Esc` to close) with the occupancy reading and the heuristic composition shares — system prompt, tools, and messages — matching the Web UI's ContextMeter dialog. The meter hides itself when the profile has no token-meter projections.
246
-
247
- The footer reports session tokens, average time to first token, decode throughput, and cache-hit rate, folded from durable step, chunk, and message events.
248
-
249
- ### Settings
250
-
251
- `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):
252
-
253
- - **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).
254
- - **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.
255
- - **Update** — see [In-app updates](#in-app-updates).
256
-
257
- 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.
258
-
259
- ### In-app updates
260
-
261
- `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:
262
-
263
- - `Update available x.y.z` a newer stable release exists.
264
- - `Up to date` nothing to do.
265
- - `No stable release — pick from Versions` — the registry has no stable release yet; pick one manually.
266
- - `Install damagedreinstall below` — the global dsh tree is in a mixed old/new state; reinstall it.
267
-
268
- `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-releasesfor 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.
269
-
270
- <details>
271
- <summary><strong>Windows: why dsh installs are deferred to exit</strong></summary>
272
-
273
- 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:
274
-
275
- 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.
276
- 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.
277
- 3. **An already-damaged install is flagged** in the Status row rather than reported as a bogus success.
278
-
279
- macOS/Linux have no DLL lock, but an install is refused while other dsh processes are running.
280
- </details>
281
-
282
- ## How it works
283
-
284
- - 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.
285
- - `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.
286
- - `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.
287
- - `lib/metrics.js` folds durable step/chunk/message events into token, TTFT, throughput, and cache-hit metrics.
288
- - `lib/interrupt.js` owns the clear/cancel/double-exit state machine used by stdin and `SIGINT`.
289
- - `lib/markdown.js` renders model output (headings, lists, quotes, code, inline spans) to styled lines.
290
- - `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`.
291
- - 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.
292
- - `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.
293
-
294
- ## Development
295
-
296
- ```sh
297
- npm run check # node --check over lib/, bin/
298
- npm test # standalone smoke tests (no dsh needed)
299
- ```
300
-
301
- **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:
302
-
303
- ```sh
304
- npm pack # -> dsh-oc-tui-<version>.tgz
305
- dsh plugin --profile tui remove -w dsh-oc-tui # detach the old copy FIRST
306
- Remove-Item .\*.tgz # then drop the stale tarball
307
- npm pack
308
- dsh plugin --profile tui add -w .\dsh-oc-tui-<version>.tgz
309
- ```
310
-
311
- 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`.
312
-
313
- Verify the swap actually landed the version string proves nothing:
314
-
315
- ```powershell
316
- foreach ($rel in @('lib\index.js','lib\ui.js','lib\util.js','lib\term.js','lib\metrics.js',
317
- 'lib\interrupt.js','lib\web-settings.js','lib\updates.js','lib\markdown.js',
318
- 'lib\startup.js','bin\dsh-oc-tui.js','cordis.patch.yml')) {
319
- $a = (Get-FileHash ".\$rel").Hash
320
- $b = (Get-FileHash "$env:USERPROFILE\.dsh\profiles\tui\node_modules\dsh-oc-tui\$rel").Hash
321
- if ($a -ne $b) { "DIFFERS: $rel" }
322
- }
323
- ```
324
-
325
- 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.
326
-
327
- For a zero-install bootstrap that skips packaging entirely, create the profile once and point its patch at this checkout:
328
-
329
- ```sh
330
- dsh --profile tui --dump-config # initializes the base profile once
331
- ```
332
-
333
- ```yaml
334
- # $DSH_HOME/profiles/tui/cordis.patch.yml
335
- - insert:
336
- - id: tui-startup
337
- name: 'file:///D:/Projects/DeepSeekHarnessPlugins/dsh-oc-tui/lib/startup.js'
338
- - id: tui-app
339
- name: 'file:///D:/Projects/DeepSeekHarnessPlugins/dsh-oc-tui/lib/index.js'
340
- config:
341
- sidebar: true
342
- showReasoning: true
343
- ```
344
-
345
- 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.
346
-
347
- ## Troubleshooting
348
-
349
- | Symptom | Cause and fix |
350
- | --- | --- |
351
- | `pnpm failed in profile directory` / `ERR_PNPM_ADDING_TO_ROOT` | The profile is a pnpm workspace root; add `-w` to the `add`/`remove` command. |
352
- | `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. |
353
- | `pnpm not found on PATH` | Install pnpm (`npm install -g pnpm`) and retry. |
354
- | `--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`. |
355
- | 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. |
356
- | `--resume` or Manage sessions unavailable | Both need the shared `sessionQuery` service; keep `@deepseek-ai/dsh-base` first in `dsh.profile.bundles`. |
357
- | `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. |
358
- | 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>`. |
359
- | Source edits have no effect | The installed copy is a tarball; repack and reinstall (see [Development](#development)). |
360
-
361
- More detail, in Chinese: [docs/用户手册.md](docs/用户手册.md).
362
-
363
- ## Known limitations
364
-
365
- - IME composition and bracketed-paste image attachments are not exposed by the zero-dependency terminal engine yet.
366
- - 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.
367
- - `dsh tui` as a bare subcommand needs a shell alias — the stock launcher hardcodes only `web` and `plugin`.
368
- - Harness slash commands need a live session; on the title screen the TUI tells you to start one first.
369
- - 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.
370
- - `--resume`, Settings → Manage sessions, and the context meter depend on services mounted by `@deepseek-ai/dsh-base` (`sessionQuery`, `sessionProjections`); a hand-built profile must provide them.
371
- - 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.
372
-
373
- ## Layout
374
-
375
- ```
376
- lib/index.js plugin entry: agents, events, input, commands, approvals, user questions
377
- lib/startup.js command-line provider (tuiStartup service)
378
- lib/term.js terminal engine (raw mode, screen, key decoding)
379
- lib/ui.js responsive view model + renderer (includes the question modal)
380
- lib/metrics.js durable event telemetry fold
381
- lib/interrupt.js Ctrl+C lifecycle state
382
- lib/web-settings.js shared WebUI settings projection
383
- lib/updates.js in-app update manager (npm registry + installs)
384
- lib/markdown.js markdown -> styled lines
385
- lib/util.js text/display helpers
386
- bin/dsh-oc-tui.js convenience launcher for `dsh --profile tui`
387
- install.sh one-command installer (Linux/macOS)
388
- install.ps1 one-command installer (Windows)
389
- cordis.patch.yml bundle patch layer (TUI rows, agent-presets roster, ask-user tool)
390
- docs/用户手册.md Chinese user manual
391
- tests/smoke.test.mjs standalone smoke tests
392
- ```
393
-
394
- ## License
395
-
396
- [LGPL-3.0-or-later](LICENSE).
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).