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 +32 -14
- package/bin/apply-update.cjs +187 -0
- package/cordis.patch.yml +46 -32
- package/docs//347/224/250/346/210/267/346/211/213/345/206/214.md +17 -10
- package/dsh-plugin.json +65 -65
- package/lib/index.js +197 -66
- package/lib/markdown.js +296 -296
- package/lib/ui.js +13 -0
- package/lib/updates.js +1010 -697
- package/lib/web-settings.js +468 -454
- package/package.json +132 -100
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
|
[](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` (
|
|
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 `
|
|
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,
|
|
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.
|
|
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
|
|
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>
|
|
309
|
+
<summary><strong>Why an update never installs while the TUI runs</strong></summary>
|
|
293
310
|
|
|
294
|
-
|
|
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. **
|
|
297
|
-
2. **The on-disk version
|
|
298
|
-
3. **
|
|
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
|
-
|
|
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
|
-
-
|
|
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 +
|
|
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-
|
|
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
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
+
}
|