dsh-oc-tui 0.1.4 → 0.1.5

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,5 +1,11 @@
1
1
  # dsh-oc-tui
2
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
+
3
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.
4
10
 
5
11
  [![npm latest](https://img.shields.io/npm/v/dsh-oc-tui?label=npm&color=BF392B)](https://www.npmjs.com/package/dsh-oc-tui)
@@ -67,7 +73,16 @@ dsh --version
67
73
  pnpm --version
68
74
  ```
69
75
 
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.
76
+ **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.
77
+
78
+ **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:
79
+
80
+ ```
81
+ dsh: disabling profile plugin row "agent-presets": Plugin @deepseek-ai/dsh-agent-presets@0.1.1-rc.2 is
82
+ incompatible with dsh 0.2.0-rc.2: peerDependencies {…}. Running it may cause crashes or data loss.
83
+ ```
84
+
85
+ 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.
71
86
 
72
87
  ## Install
73
88
 
@@ -140,7 +155,7 @@ Verify without booting:
140
155
  dsh --profile tui --dump-config
141
156
  ```
142
157
 
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.
158
+ 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).
144
159
 
145
160
  ## Quick start
146
161
 
@@ -271,11 +286,11 @@ Figures come from the same sources as the Web UI, projection-first with the plug
271
286
 
272
287
  `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
288
 
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).
289
+ - **Main** — General (busy-Enter behaviour, permission preset), Sessions (new session, manage sessions), System (provider API hints, update-manager shortcut, settings file path).
275
290
  - **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
291
  - **Update** — see [In-app updates](#in-app-updates).
277
292
 
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.
293
+ 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.
279
294
 
280
295
  ### In-app updates
281
296
 
@@ -286,18 +301,20 @@ Only providers you actually added (present in your user settings layer) are list
286
301
  - `No stable release — pick from Versions` — the registry has no stable release yet; pick one manually.
287
302
  - `Install damaged — reinstall below` — the global dsh tree is in a mixed old/new state; reinstall it.
288
303
 
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.
304
+ `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.
305
+
306
+ 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.
290
307
 
291
308
  <details>
292
- <summary><strong>Windows: why dsh installs are deferred to exit</strong></summary>
309
+ <summary><strong>Why an update never installs while the TUI runs</strong></summary>
293
310
 
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:
311
+ 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:
295
312
 
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.
313
+ 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.
314
+ 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.
315
+ 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.
299
316
 
300
- macOS/Linux have no DLL lock, but an install is refused while other dsh processes are running.
317
+ 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.
301
318
  </details>
302
319
 
303
320
  ## How it works
@@ -397,7 +414,7 @@ More detail, in Chinese: [docs/用户手册.md](docs/用户手册.md).
397
414
  - Harness slash commands need a live session; on the title screen the TUI tells you to start one first.
398
415
  - 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
416
  - `--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.
417
+ - 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.
401
418
  - 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
419
 
403
420
  ## Layout
@@ -410,17 +427,18 @@ lib/ui.js responsive view model + renderer (includes the question mod
410
427
  lib/metrics.js whole-session stats + token usage fold (web stats strip / tokenUsage port)
411
428
  lib/interrupt.js Ctrl+C lifecycle state
412
429
  lib/web-settings.js shared WebUI settings projection
413
- lib/updates.js in-app update manager (npm registry + installs)
430
+ lib/updates.js in-app update manager (npm registry, download + staging, install verification)
414
431
  lib/markdown.js markdown -> styled lines
415
432
  lib/util.js text/display helpers
416
433
  lib/bridge.js live-TUI registry the @dsh-std facet forwards through
417
434
  lib/facet.js @dsh-std facet entry (interop shell; never starts the TUI)
418
435
  lib/std/ @dsh-std protocol adapters (adapt, presentation, commands, command-list)
419
436
  bin/dsh-oc-tui.js convenience launcher for `dsh --profile tui`
437
+ bin/apply-update.cjs detached applier: waits for the TUI to exit, installs the staged tarball, relaunches dsh
420
438
  install.sh one-command installer (Linux/macOS)
421
439
  install.ps1 one-command installer (Windows)
422
440
  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)
441
+ cordis.patch.yml bundle patch layer (TUI rows, ask-user tool; no agent-preset row, with the reason in a comment)
424
442
  docs/用户手册.md Chinese user manual
425
443
  docs/dsh-std-接入说明.md @dsh-std interop scope and constraints (Chinese)
426
444
  tests/smoke.test.mjs standalone smoke tests
@@ -0,0 +1,187 @@
1
+ #!/usr/bin/env node
2
+ // Detached applier for a TUI-driven update. Spawned by the running TUI
3
+ // immediately before it exits: `node bin/apply-update.cjs <plan.json>`.
4
+ //
5
+ // The plan is a JSON file (written by the TUI into $DSH_HOME/updates) naming
6
+ // one package (dsh or dsh-oc-tui), the staged tarball to install from, how to
7
+ // verify the result, and how to relaunch. The child waits for the TUI to die
8
+ // *first* — that is the whole point of the restart handoff: a global npm
9
+ // install (or a profile reinstall) while a dsh process is alive can leave a
10
+ // hybrid old/new tree behind, and npm still exits 0.
11
+ //
12
+ // Deliberately self-contained: it outlives the plugin that spawned it,
13
+ // including when that plugin is the package being replaced, so it imports
14
+ // nothing from the plugin and stays plain CJS.
15
+ 'use strict'
16
+
17
+ const fs = require('node:fs')
18
+ const path = require('node:path')
19
+ const cp = require('node:child_process')
20
+ const { createRequire } = require('node:module')
21
+
22
+ const PARENT_WAIT_MS = 600_000
23
+
24
+ function log(line) {
25
+ try { process.stdout.write('[dsh-oc-tui] ' + line + '\n') } catch { /* terminal already gone */ }
26
+ }
27
+
28
+ function alive(pid) {
29
+ try { process.kill(pid, 0); return true } catch { return false }
30
+ }
31
+
32
+ function installedVersion(manifestPath) {
33
+ try {
34
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'))
35
+ return typeof manifest.version === 'string' ? manifest.version : null
36
+ } catch {
37
+ return null
38
+ }
39
+ }
40
+
41
+ // Re-resolve the dsh bin entry through the shim *after* the install, so a
42
+ // layout change inside a new dsh version cannot break the relaunch.
43
+ function entryFromShim(shim) {
44
+ if (typeof shim !== 'string' || shim === '') return null
45
+ try {
46
+ const manifestPath = createRequire(path.resolve(shim)).resolve('@deepseek-ai/dsh/package.json')
47
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'))
48
+ const entry = typeof manifest.bin === 'string' ? manifest.bin : manifest.bin && manifest.bin.dsh
49
+ return typeof entry === 'string' && entry !== '' ? path.resolve(path.dirname(manifestPath), entry) : null
50
+ } catch {
51
+ return null
52
+ }
53
+ }
54
+
55
+ function relaunchTarget(plan) {
56
+ const entry = entryFromShim(plan.dshShim)
57
+ if (entry) return { command: process.execPath, args: [entry, ...(plan.dshArgs ?? [])] }
58
+ const fallback = plan.relaunch
59
+ if (fallback && Array.isArray(fallback.args) && fallback.args.length > 0) return { command: fallback.command ?? process.execPath, args: fallback.args }
60
+ return null
61
+ }
62
+
63
+ function writeMarker(plan, code) {
64
+ try {
65
+ fs.mkdirSync(path.dirname(plan.marker), { recursive: true })
66
+ fs.writeFileSync(plan.marker, JSON.stringify({
67
+ requested: plan.version,
68
+ pkg: plan.pkg === 'tui' ? 'tui' : 'dsh',
69
+ code,
70
+ finishedAt: Date.now(),
71
+ }))
72
+ } catch {
73
+ // Unwritable home: the next boot simply has nothing to verify.
74
+ }
75
+ }
76
+
77
+ // The tarball itself stays in the cache — a rollback to the same version then
78
+ // costs no download. Only the "waiting to be applied" record goes.
79
+ function dropStaged(plan) {
80
+ try { fs.unlinkSync(path.join(path.dirname(plan.tarball), 'staged.json')) } catch { /* nothing staged */ }
81
+ }
82
+
83
+ function runInstall(plan) {
84
+ if (plan.pkg === 'dsh') {
85
+ if (!plan.npm) {
86
+ log('npm could not be located — install manually: npm install -g @deepseek-ai/dsh@' + plan.version)
87
+ return { code: 1 }
88
+ }
89
+ const result = cp.spawnSync(plan.npm.command, [...plan.npm.args, 'install', '-g', plan.tarball], {
90
+ stdio: 'inherit',
91
+ windowsHide: false,
92
+ })
93
+ return { code: result.status === null ? 1 : result.status }
94
+ }
95
+ if (!plan.dsh) {
96
+ log('the dsh CLI could not be located — reinstall the plugin with: dsh plugin --profile ' + plan.profile + ' add -w ' + plan.tarball)
97
+ return { code: 1 }
98
+ }
99
+ // Reinstalling the same version does not reliably replace the installed
100
+ // copy, so detach the dependency first — the documented dev-loop order.
101
+ const detach = cp.spawnSync(plan.dsh.command, [
102
+ ...plan.dsh.args, 'plugin', '--profile', plan.profile, 'remove', '-w', plan.packageName,
103
+ ], { stdio: 'inherit', windowsHide: false })
104
+ if (detach.status !== 0) log('detaching the previous copy exited ' + detach.status + ' — continuing anyway')
105
+ const add = cp.spawnSync(plan.dsh.command, [
106
+ ...plan.dsh.args, 'plugin', '--profile', plan.profile, 'add', '-w', plan.tarball,
107
+ ], { stdio: 'inherit', windowsHide: false })
108
+ return { code: add.status === null ? 1 : add.status }
109
+ }
110
+
111
+ function apply(plan) {
112
+ log('installing ' + plan.packageName + '@' + plan.version + ' from ' + plan.tarball)
113
+ const result = runInstall(plan)
114
+ const found = plan.verifyPath ? installedVersion(plan.verifyPath) : null
115
+ if (result.code === 0) {
116
+ if (found === plan.version) {
117
+ log('verified ' + plan.packageName + '@' + plan.version)
118
+ } else {
119
+ // npm exit 0 is not proof — the on-disk manifest decides. The marker
120
+ // (code 0, mismatched version) is what the next boot reads back as a
121
+ // damaged install.
122
+ log('WARNING: the install reported success but ' + (plan.verifyPath ?? 'the manifest') +
123
+ ' reports ' + (found ?? 'nothing') + ' (expected ' + plan.version + ')')
124
+ }
125
+ } else {
126
+ log('install failed (exit ' + result.code + ')')
127
+ }
128
+ writeMarker(plan, result.code)
129
+ dropStaged(plan)
130
+ relaunch(plan)
131
+ }
132
+
133
+ function relaunch(plan) {
134
+ const target = relaunchTarget(plan)
135
+ if (!target) {
136
+ log('could not locate the dsh entry — start dsh again yourself')
137
+ process.exit(0)
138
+ }
139
+ log('restarting dsh…')
140
+ try {
141
+ const child = cp.spawn(target.command, target.args, {
142
+ cwd: plan.cwd || undefined,
143
+ detached: true,
144
+ stdio: 'inherit',
145
+ windowsHide: false,
146
+ })
147
+ child.unref()
148
+ } catch (error) {
149
+ log('relaunch failed: ' + (error?.message ?? String(error)) + ' — start dsh again yourself')
150
+ }
151
+ process.exit(0)
152
+ }
153
+
154
+ const planFile = process.argv[2] ?? ''
155
+ let plan = null
156
+ if (planFile !== '') {
157
+ try {
158
+ plan = JSON.parse(fs.readFileSync(planFile, 'utf8'))
159
+ } catch {
160
+ plan = null
161
+ }
162
+ }
163
+ if (!plan || typeof plan.tarball !== 'string' || plan.tarball === '' || typeof plan.version !== 'string' || plan.version === '') {
164
+ log('no usable plan at ' + (planFile || '(no path given)') + ' — nothing to do')
165
+ process.exit(1)
166
+ }
167
+ if (!plan.marker) {
168
+ log('plan carries no marker path — nothing to do')
169
+ process.exit(1)
170
+ }
171
+
172
+ ;(function waitForParent() {
173
+ const deadline = Date.now() + PARENT_WAIT_MS
174
+ if (alive(plan.parentPid) && Date.now() < deadline) {
175
+ // Still inside the window: keep polling. The TUI paints its own screen
176
+ // until it is gone, so nothing is logged before this point.
177
+ setTimeout(waitForParent, 250)
178
+ return
179
+ }
180
+ if (alive(plan.parentPid)) {
181
+ // Never went away (or the pid was reused): never install over a live tree.
182
+ writeMarker(plan, 1)
183
+ log('timed out waiting for the TUI to exit — nothing was installed')
184
+ process.exit(0)
185
+ }
186
+ apply(plan)
187
+ })()
package/cordis.patch.yml CHANGED
@@ -1,32 +1,46 @@
1
- # dsh-oc-tui bundle patch: the terminal UI over the dsh-base layer.
2
- # Rows reference this package by name, so the package must be resolvable from
3
- # the profile — the official path is `dsh plugin --profile tui add
4
- # dsh-oc-tui`, which installs it into the profile's node_modules and
5
- # appends this bundle. The zero-install development bootstrap may instead
6
- # restate these rows in the profile's own patch with absolute module paths.
7
-
8
- - insert:
9
- - id: tui-startup
10
- name: 'dsh-oc-tui/startup'
11
-
12
- - id: tui-app
13
- name: 'dsh-oc-tui'
14
- config:
15
- sidebar: true
16
- showReasoning: true
17
-
18
- # The preset roster behind the Settings -> Default preset list.
19
- # Sessions keep composing process-wide from the base (single-session TUI);
20
- # this row only makes the roster and its settings namespace available.
21
- - id: agent-presets
22
- name: '@deepseek-ai/dsh-agent-presets'
23
- config:
24
- default: standard
25
-
26
- # The model-facing `ask_user_question` tool. dsh-base mounts the
27
- # `user-questions` service but NOT this tool — the agent presets mount it,
28
- # and a TUI session composes process-wide from the base rather than from a
29
- # preset, so the row must be declared here or the tool is simply absent.
30
- # lib/index.js answers the `user-questions/request` waterfall with a modal.
31
- - id: tool-ask-user
32
- name: '@deepseek-ai/dsh-tool-ask-user'
1
+ # dsh-oc-tui bundle patch: the terminal UI over the dsh-base layer.
2
+ # Rows reference this package by name, so the package must be resolvable from
3
+ # the profile — the official path is `dsh plugin --profile tui add
4
+ # dsh-oc-tui`, which installs it into the profile's node_modules and
5
+ # appends this bundle. The zero-install development bootstrap may instead
6
+ # restate these rows in the profile's own patch with absolute module paths.
7
+
8
+ - insert:
9
+ - id: tui-startup
10
+ name: 'dsh-oc-tui/startup'
11
+
12
+ - id: tui-app
13
+ name: 'dsh-oc-tui'
14
+ config:
15
+ sidebar: true
16
+ showReasoning: true
17
+
18
+ # The model-facing `ask_user_question` tool. dsh-base mounts the
19
+ # `user-questions` service but NOT this tool, so the row must be declared
20
+ # here or the tool is simply absent. lib/index.js answers the
21
+ # `user-questions/request` waterfall with a modal.
22
+ - id: tool-ask-user
23
+ name: '@deepseek-ai/dsh-tool-ask-user'
24
+
25
+ # There is deliberately no agent-preset row. The Settings -> Default preset list
26
+ # reads the roster the profile mounts, and no single package name serves both dsh
27
+ # lines:
28
+ #
29
+ # * dsh 0.1.x: `@deepseek-ai/dsh-agent-presets` (plural) *is* the roster
30
+ # provider and ships its own presets. Naming it here is what 0.1.0-0.1.4 did.
31
+ # * dsh 0.2.0-rc.1 renamed the pieces: `@deepseek-ai/dsh-agent-presets` peers
32
+ # on the 0.1.x packages, so dsh *disables* the row at every boot with a
33
+ # wall-of-text incompatibility report ("may cause crashes or data loss"),
34
+ # while `@deepseek-ai/dsh-agent-preset` (singular) is only a declarative
35
+ # preset row that injects the service and never activates on its own, and
36
+ # `@deepseek-ai/dsh-agent-preset-registry` mounts an empty roster unless a
37
+ # host declares the presets.
38
+ #
39
+ # A row naming a package this dsh cannot resolve is skipped silently rather than
40
+ # loaded, so the honest options were "always warn on the new line" (plural),
41
+ # "never activate" (singular) or "activate empty" (registry). None of them
42
+ # restores a roster a host-owned composition owns, so the bundle mounts none:
43
+ # the TUI boots clean on both lines and Settings simply omits the Default preset
44
+ # row (lib/web-settings.js hides a row whose namespace has no provider). A dsh
45
+ # 0.2.0 profile that wants a roster mounts the registry plus its own preset
46
+ # declarations, exactly as dsh-web-app does.
@@ -134,7 +134,7 @@ profile 目录把自己声明为 pnpm workspace 根(`pnpm-workspace.yaml` →
134
134
  dsh --profile tui --dump-config
135
135
  ```
136
136
 
137
- 输出中应出现 `# == dsh-oc-tui` 这一层,包含 `tui-startup`、`tui-app`、`agent-presets`(预设名册)与 `tool-ask-user`(提问工具)四行。
137
+ 输出中应出现 `# == dsh-oc-tui` 这一层,包含 `tui-startup`、`tui-app` 与 `tool-ask-user`(提问工具)三行——本 bundle 刻意不挂载 agent 预设名册(原因见 5.7 的破坏性更新说明),因此这一层没有预设行。
138
138
 
139
139
  ## 4. 启动
140
140
 
@@ -268,7 +268,7 @@ dsh 的人类命令(`/compact`、`/goal`、`/plan` 等)会转发给 `ctx.com
268
268
 
269
269
  按 `Ctrl+P` 打开。菜单覆盖与 WebUI 相同的 Host 设置命名空间,修改通过 `ctx.settings` 持久化到 `$DSH_HOME/settings.yaml`。左侧菜单栏用 `Tab`(或鼠标点击)在三个标签间切换,`↑/↓` 移动、`Enter` 打开选项列表、`Esc` 返回,分区标题行不可选中:
270
270
 
271
- - **Main**:General(Busy Enter 行为、默认 agent 预设、权限预设)、Sessions(新建会话、管理会话)、System(Provider API 配置提示、更新管理器快捷入口、设置文件路径)
271
+ - **Main**:General(Busy Enter 行为、权限预设)、Sessions(新建会话、管理会话)、System(Provider API 配置提示、更新管理器快捷入口、设置文件路径)
272
272
  - **Model**:默认 provider / model / 思考强度;随后是你添加过的每个 provider 一组,组内为 Provider URL、Provider API key、Models
273
273
  - **Update**:见 [5.7](#57-程序内更新)
274
274
 
@@ -276,7 +276,7 @@ dsh 的人类命令(`/compact`、`/goal`、`/plan` 等)会转发给 `ctx.com
276
276
 
277
277
  **Models 自动获取**:在某个 provider 的 **Models** 行按 `Enter`,TUI 调用 `ctx.llm.discoverModels` 获取该 provider 的完整模型目录(内置路由直接取内置目录,自定义路由则请求其端点),弹出勾选窗口用 `[x]`/`[ ]` 选择(`Enter` 切换、`Esc` 返回)。选中的模型保存到该 provider 的 `models` 配置;在列出的模型上按 `Enter` 可把它设为默认模型。
278
278
 
279
- 默认 agent 预设来自 profile 挂载的 `agent-presets` 名册(内置预设 + 你在 `$DSH_HOME/.agent-presets` 下自建的预设)。注意 TUI 会话始终从进程级 base 组合生成,因此该默认值只在按预设创建会话时生效。
279
+ **默认 agent 预设** 只有在 profile 挂载了预设名册时才会出现——选项列表就是那份名册,而本插件刻意不挂载(见 5.7),所以默认的 `tui` profile 里这一行是「没有」而不是「空白」。若某一套 profile 确实挂载了名册,请注意 TUI 会话始终从进程级 base 组合生成,该默认值只在按预设创建会话时生效。
280
280
 
281
281
  WebUI 专属选项(`ui-theme` 外观、`locale` 语言)不在 TUI 中显示,因为它们在终端里没有效果。
282
282
 
@@ -293,15 +293,22 @@ WebUI 专属选项(`ui-theme` 外观、`locale` 语言)不在 TUI 中显示
293
293
 
294
294
  `Enter` 打开某个包的 **Versions** 完整版本列表(新→旧),`[latest]`(绿)、`[next]`(青)、`(installed)`(浅蓝)等标识区分频道与当前版本;选中任意版本(含 rc/alpha)后按 `Enter` 出现 y/n 确认(文案含目标版本号)。`Check now` 重新拉取 registry;`Startup check` 开关启动后 2 秒的静默稳定版检查。
295
295
 
296
- 安装过程后台进行、不阻塞界面、不会自动重启;完成后 toast `... installed — restart to apply`,重启后生效。
296
+ **更新分两步:先下载,再重启生效。** 确认后 TUI 自己把 tarball 下载到 `$DSH_HOME/updates`,界面上有实时字节进度条(`Esc` 随时取消),落盘前按 registry 上的 sha512 校验完整性;下载完成后再问一次「重启即应用」,确认后 TUI 先派生一个独立安装进程、自己退出,该子进程等本进程消失才执行安装(dsh 走 `npm install -g <tarball>`,TUI 走 `dsh plugin … add -w <tarball>`),校验磁盘实际版本,然后带着原来的参数和工作目录把 dsh 重新拉起来。因此:
297
297
 
298
- **Windows 上的 dsh 更新(重要)**:Windows 会锁定运行中程序加载的原生 DLL,而 `npm install -g` 需要替换整个 dsh 目录——若更新时有任何 dsh 进程在运行,npm 可能**静默留下新旧混杂的损坏安装**并仍报成功。因此更新器有三层保护:
298
+ - 版本列表会打 `[cached]`(该版本 tarball 已在磁盘上,回退/重试都不需要重新下载)和 `[staged]`(等待重启应用的那个版本)标记;
299
+ - Update 页面对下载未应用的版本会多出 **Restart to apply x.y.z**(直接 `Enter` 重启应用)与 **Discard downloaded x.y.z**(丢弃)两行,且只有 tarball 确实还在磁盘上时才会出现——上次下载后崩溃也不会留下一个假入口;
300
+ - 安装结果写入 `$DSH_HOME/tui-dsh-install.json`,下次打开 Update 页自动读取并清除;已损坏的安装会被标出(Status 行 `Install damaged — reinstall below`),而不是报一个虚假的成功。
299
301
 
300
- 1. **dsh 安装在 Windows 上延迟到 TUI 退出后执行**:确认后 toast 提示 `will install when this TUI exits`,关闭 TUI 时一个独立小进程等待本进程退出、执行 `npm install`,并把结果写入 `$DSH_HOME/tui-dsh-install.json`,下次打开 Update 页自动核对。
301
- 2. **每次直接安装后校验磁盘实际版本**与目标是否一致,不一致(静默损坏)时 toast 报 `install corrupt` 并给出修复指引。
302
- 3. **已损坏的安装会被标出**(Status 行 `Install damaged — reinstall below`),而不是报一个虚假的成功。
302
+ **为什么更新绝不在 TUI 运行期间安装**:给一个正在被 `dsh` 进程加载的包做全局安装,可能**静默**留下新旧混杂的损坏安装——Windows 上运行中的进程锁着内存映射的原生 DLL,npm 替换目录失败却仍以 0 退出(2026-09-04 就是这样把 dsh 弄坏的)。改成「下载 → 重启应用」之后,这台戏在任何平台都不再有窗口期:应用进程会等本进程消失才动手,且 TUI 在确认重启前会检查是否有别的 dsh 进程在跑(有就拒绝)。残留窗口只有一个:该检查发生在确认**之前**,之后又开起来的 `dsh web` 仍可能撞上安装。
303
303
 
304
- macOS/Linux 没有 DLL 锁,但检测到其它 dsh 进程运行时也会拒绝安装。
304
+ **dsh 0.2.0 是破坏性更新——先升 TUI,再升 dsh。** 从 `0.2.0-rc.1` 起,预设相关包被拆开:原来的名册提供方 `@deepseek-ai/dsh-agent-presets`(自带预设)改名为 `@deepseek-ai/dsh-agent-preset-registry`,而新的 `@deepseek-ai/dsh-agent-preset` 只是**声明式**的预设行——它注入注册表提供的服务,自己不会激活。`0.1.5` 以下的 TUI 引用的还是旧名,于是 dsh 每次启动都会打印一段墙一样的不兼容报告:
305
+
306
+ ```
307
+ dsh: disabling profile plugin row "agent-presets": Plugin @deepseek-ai/dsh-agent-presets@0.1.1-rc.2 is
308
+ incompatible with dsh 0.2.0-rc.2: peerDependencies {…}. Running it may cause crashes or data loss.
309
+ ```
310
+
311
+ TUI 仍然能启动,但 Settings → Default preset 不再有名册可选(没有人提供名册时该行直接隐藏)。把 TUI 更到 `0.1.5` 或更高,这段报告就消失:本 bundle **根本不挂载名册行**——因为没有任何一个包名能在两条 dsh 线上同时成立:旧名在 0.2.0 上会触发上面的报告,单数名永远不会激活,注册表名册在没有主机声明预设时激活出来也是空的。需要名册的 dsh 0.2.0 profile 应当自己挂载注册表并声明预设(`dsh-web-app` 正是这么做的)。两个方向都不影响会话数据。
305
312
 
306
313
  ## 6. 工作原理
307
314
 
@@ -413,7 +420,7 @@ bin/dsh-oc-tui.js 便捷启动器
413
420
  install.sh 一键安装脚本(Linux/macOS)
414
421
  install.ps1 一键安装脚本(Windows)
415
422
  dsh-plugin.json @dsh-std 组件清单(仅用于发现与预检)
416
- cordis.patch.yml bundle 补丁层(TUI 行、预设名册、提问工具)
423
+ cordis.patch.yml bundle 补丁层(TUI 行、提问工具;刻意不含预设名册行,注释里写了原因)
417
424
  docs/用户手册.md 本手册
418
425
  docs/dsh-std-接入说明.md @dsh-std 接入范围与约束
419
426
  tests/smoke.test.mjs 独立冒烟测试
package/dsh-plugin.json CHANGED
@@ -1,65 +1,65 @@
1
- {
2
- "$schema": "https://raw.githubusercontent.com/Yan-Zero/dsh-std/main/packages/manifest/schema/dsh-plugin-0.15.schema.json",
3
- "manifestVersion": "0.15",
4
- "id": "io.github.rayafriandion.dsh-oc-tui",
5
- "name": "dsh-oc-tui",
6
- "version": "0.1.4",
7
- "license": "LGPL-3.0-or-later",
8
- "source": { "repository": "https://github.com/rayafriandion/dsh-oc-tui" },
9
- "facets": { "host": { "entry": "lib/facet.js", "apiVersion": "v1alpha1" } },
10
- "requires": {
11
- "contracts": [
12
- { "apiVersion": "commands.dsh/v1alpha1", "kind": "Command", "optional": true }
13
- ]
14
- },
15
- "permissions": [
16
- { "name": "storage.local.read", "scope": "io.github.rayafriandion.dsh-oc-tui",
17
- "reason": "Read TUI settings from the host settings store." },
18
- { "name": "storage.local.write", "scope": "io.github.rayafriandion.dsh-oc-tui",
19
- "reason": "Persist TUI settings chosen in the Settings pages." }
20
- ],
21
- "contributes": {
22
- "x-dev.dsh-std.extensions": [
23
- { "id": "io.github.rayafriandion.dsh-oc-tui.settings",
24
- "apiVersion": "commands.dsh/v1alpha1", "kind": "Command", "name": "settings",
25
- "spec": { "title": "Open the TUI settings pages",
26
- "placements": [{ "apiVersion": "tui.dsh/v1alpha1", "kind": "CommandLine" }] } },
27
- { "id": "io.github.rayafriandion.dsh-oc-tui.help",
28
- "apiVersion": "commands.dsh/v1alpha1", "kind": "Command", "name": "help",
29
- "spec": { "title": "Show the TUI key and command reference",
30
- "placements": [{ "apiVersion": "tui.dsh/v1alpha1", "kind": "CommandLine" }] } },
31
- { "id": "io.github.rayafriandion.dsh-oc-tui.stats",
32
- "apiVersion": "commands.dsh/v1alpha1", "kind": "Command", "name": "stats",
33
- "spec": { "title": "Show session token and cache statistics",
34
- "placements": [{ "apiVersion": "tui.dsh/v1alpha1", "kind": "CommandLine" }] } },
35
- { "id": "io.github.rayafriandion.dsh-oc-tui.new",
36
- "apiVersion": "commands.dsh/v1alpha1", "kind": "Command", "name": "new",
37
- "spec": { "title": "Start a new session",
38
- "placements": [{ "apiVersion": "tui.dsh/v1alpha1", "kind": "CommandLine" }] } },
39
- { "id": "io.github.rayafriandion.dsh-oc-tui.resume",
40
- "apiVersion": "commands.dsh/v1alpha1", "kind": "Command", "name": "resume",
41
- "spec": { "title": "Resume a persisted session",
42
- "placements": [{ "apiVersion": "tui.dsh/v1alpha1", "kind": "CommandLine" }] } },
43
- { "id": "io.github.rayafriandion.dsh-oc-tui.clear",
44
- "apiVersion": "commands.dsh/v1alpha1", "kind": "Command", "name": "clear",
45
- "spec": { "title": "Clear the transcript",
46
- "placements": [{ "apiVersion": "tui.dsh/v1alpha1", "kind": "CommandLine" }] } },
47
- { "id": "io.github.rayafriandion.dsh-oc-tui.cancel",
48
- "apiVersion": "commands.dsh/v1alpha1", "kind": "Command", "name": "cancel",
49
- "spec": { "title": "Cancel the running turn",
50
- "placements": [{ "apiVersion": "tui.dsh/v1alpha1", "kind": "CommandLine" }] } },
51
- { "id": "io.github.rayafriandion.dsh-oc-tui.rewind",
52
- "apiVersion": "commands.dsh/v1alpha1", "kind": "Command", "name": "rewind",
53
- "spec": { "title": "Rewind the session to an earlier point",
54
- "placements": [{ "apiVersion": "tui.dsh/v1alpha1", "kind": "CommandLine" }] } },
55
- { "id": "io.github.rayafriandion.dsh-oc-tui.quit",
56
- "apiVersion": "commands.dsh/v1alpha1", "kind": "Command", "name": "quit",
57
- "spec": { "title": "Leave the TUI", "aliases": ["exit"],
58
- "placements": [{ "apiVersion": "tui.dsh/v1alpha1", "kind": "CommandLine" }] } }
59
- ]
60
- },
61
- "overrides": [
62
- { "target": "@deepseek-ai/dsh-base", "kind": "patch",
63
- "description": "cordis.patch.yml inserts the tui-startup, tui-app, agent-presets and tool-ask-user rows." }
64
- ]
65
- }
1
+ {
2
+ "$schema": "https://raw.githubusercontent.com/Yan-Zero/dsh-std/main/packages/manifest/schema/dsh-plugin-0.15.schema.json",
3
+ "manifestVersion": "0.15",
4
+ "id": "io.github.rayafriandion.dsh-oc-tui",
5
+ "name": "dsh-oc-tui",
6
+ "version": "0.1.5",
7
+ "license": "LGPL-3.0-or-later",
8
+ "source": { "repository": "https://github.com/rayafriandion/dsh-oc-tui" },
9
+ "facets": { "host": { "entry": "lib/facet.js", "apiVersion": "v1alpha1" } },
10
+ "requires": {
11
+ "contracts": [
12
+ { "apiVersion": "commands.dsh/v1alpha1", "kind": "Command", "optional": true }
13
+ ]
14
+ },
15
+ "permissions": [
16
+ { "name": "storage.local.read", "scope": "io.github.rayafriandion.dsh-oc-tui",
17
+ "reason": "Read TUI settings from the host settings store." },
18
+ { "name": "storage.local.write", "scope": "io.github.rayafriandion.dsh-oc-tui",
19
+ "reason": "Persist TUI settings chosen in the Settings pages." }
20
+ ],
21
+ "contributes": {
22
+ "x-dev.dsh-std.extensions": [
23
+ { "id": "io.github.rayafriandion.dsh-oc-tui.settings",
24
+ "apiVersion": "commands.dsh/v1alpha1", "kind": "Command", "name": "settings",
25
+ "spec": { "title": "Open the TUI settings pages",
26
+ "placements": [{ "apiVersion": "tui.dsh/v1alpha1", "kind": "CommandLine" }] } },
27
+ { "id": "io.github.rayafriandion.dsh-oc-tui.help",
28
+ "apiVersion": "commands.dsh/v1alpha1", "kind": "Command", "name": "help",
29
+ "spec": { "title": "Show the TUI key and command reference",
30
+ "placements": [{ "apiVersion": "tui.dsh/v1alpha1", "kind": "CommandLine" }] } },
31
+ { "id": "io.github.rayafriandion.dsh-oc-tui.stats",
32
+ "apiVersion": "commands.dsh/v1alpha1", "kind": "Command", "name": "stats",
33
+ "spec": { "title": "Show session token and cache statistics",
34
+ "placements": [{ "apiVersion": "tui.dsh/v1alpha1", "kind": "CommandLine" }] } },
35
+ { "id": "io.github.rayafriandion.dsh-oc-tui.new",
36
+ "apiVersion": "commands.dsh/v1alpha1", "kind": "Command", "name": "new",
37
+ "spec": { "title": "Start a new session",
38
+ "placements": [{ "apiVersion": "tui.dsh/v1alpha1", "kind": "CommandLine" }] } },
39
+ { "id": "io.github.rayafriandion.dsh-oc-tui.resume",
40
+ "apiVersion": "commands.dsh/v1alpha1", "kind": "Command", "name": "resume",
41
+ "spec": { "title": "Resume a persisted session",
42
+ "placements": [{ "apiVersion": "tui.dsh/v1alpha1", "kind": "CommandLine" }] } },
43
+ { "id": "io.github.rayafriandion.dsh-oc-tui.clear",
44
+ "apiVersion": "commands.dsh/v1alpha1", "kind": "Command", "name": "clear",
45
+ "spec": { "title": "Clear the transcript",
46
+ "placements": [{ "apiVersion": "tui.dsh/v1alpha1", "kind": "CommandLine" }] } },
47
+ { "id": "io.github.rayafriandion.dsh-oc-tui.cancel",
48
+ "apiVersion": "commands.dsh/v1alpha1", "kind": "Command", "name": "cancel",
49
+ "spec": { "title": "Cancel the running turn",
50
+ "placements": [{ "apiVersion": "tui.dsh/v1alpha1", "kind": "CommandLine" }] } },
51
+ { "id": "io.github.rayafriandion.dsh-oc-tui.rewind",
52
+ "apiVersion": "commands.dsh/v1alpha1", "kind": "Command", "name": "rewind",
53
+ "spec": { "title": "Rewind the session to an earlier point",
54
+ "placements": [{ "apiVersion": "tui.dsh/v1alpha1", "kind": "CommandLine" }] } },
55
+ { "id": "io.github.rayafriandion.dsh-oc-tui.quit",
56
+ "apiVersion": "commands.dsh/v1alpha1", "kind": "Command", "name": "quit",
57
+ "spec": { "title": "Leave the TUI", "aliases": ["exit"],
58
+ "placements": [{ "apiVersion": "tui.dsh/v1alpha1", "kind": "CommandLine" }] } }
59
+ ]
60
+ },
61
+ "overrides": [
62
+ { "target": "@deepseek-ai/dsh-base", "kind": "patch",
63
+ "description": "cordis.patch.yml inserts the tui-startup, tui-app, agent-presets and tool-ask-user rows." }
64
+ ]
65
+ }