@aiwayds/dsh-tui-pi 2.30.0 → 2.32.0
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 +14 -2
- package/bin/boot.mjs +47 -0
- package/bin/dsh-tui-pi +4 -4
- package/bin/onboard-core.mjs +275 -0
- package/lib/approval.d.ts +23 -0
- package/lib/approval.js +124 -0
- package/lib/approval.js.map +1 -0
- package/lib/ask-user.d.ts +2 -0
- package/lib/ask-user.js +3 -2
- package/lib/ask-user.js.map +1 -1
- package/lib/dialog-queue.d.ts +8 -0
- package/lib/dialog-queue.js +41 -0
- package/lib/dialog-queue.js.map +1 -0
- package/lib/index.js +19 -3
- package/lib/index.js.map +1 -1
- package/lib/profile.d.ts +82 -0
- package/lib/profile.js +665 -0
- package/lib/profile.js.map +1 -0
- package/package.json +4 -3
- package/scripts/postinstall.mjs +54 -0
- package/scripts/smoke-boot.mjs +22 -0
package/README.md
CHANGED
|
@@ -17,6 +17,7 @@ https://github.com/user-attachments/assets/67a7c6ca-ff42-4005-b543-437ba61771bb
|
|
|
17
17
|
- [**Footer — live session overview**](docs/features/footer.md) — provider/model, context pressure and the session cache-hit rate at a glance, always in view.
|
|
18
18
|
- [**Think & tool panels**](docs/features/think-tool-panels.md) — reasoning and tool activity stay out of the transcript, so the conversation reads clean.
|
|
19
19
|
- [**Subagents**](docs/features/subagents.md) — every running subagent gets a status line; watch its live transcript and decode speed (tok/s), steer it in place.
|
|
20
|
+
- [**Tool approvals**](docs/features/approval.md) — review the operation and allow it once or deny it; no automatic approval.
|
|
20
21
|
- [**Ask User Question**](docs/features/ask-user-question.md) — the model can pause and ask you structured questions, answered without leaving the TUI.
|
|
21
22
|
- [**Feishu integration**](docs/features/feishu-demo.md) — dsh-tui-pi on the desktop and Feishu/Lark on the phone driving (and answering for) the same dsh session.
|
|
22
23
|
- [**Dynamic context pruning (DCP)**](docs/features/dcp.md) — context stays within limits automatically, with zero LLM calls.
|
|
@@ -37,13 +38,24 @@ https://github.com/user-attachments/assets/67a7c6ca-ff42-4005-b543-437ba61771bb
|
|
|
37
38
|
## Install and launch
|
|
38
39
|
|
|
39
40
|
```sh
|
|
41
|
+
npm i -g @aiwayds/dsh-tui-pi
|
|
42
|
+
dsh-tui-pi # or the short form: dst
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
The global install sets the machine up by itself, printing one progress line per step: it installs the `@deepseek-ai/dsh` host when missing (via npm; if npm itself is not on PATH, it prints what to install and stops), installs pnpm, and creates the `tui` profile with this plugin and its bundled companions. Whatever the install cannot finish (e.g. an offline machine) completes automatically on the first launch. After a later `npm i -g` upgrade of this package, the profile's mounted copy is moved to the new version on the next launch — a profile that is newer than the global install (dev-upgraded ahead) is left untouched.
|
|
46
|
+
|
|
47
|
+
The manual steps do exactly what the onboarding runs, if you prefer them:
|
|
48
|
+
|
|
49
|
+
```sh
|
|
50
|
+
npm i -g @deepseek-ai/dsh
|
|
51
|
+
npm i -g pnpm
|
|
40
52
|
dsh plugin --profile tui add @aiwayds/dsh-tui-pi
|
|
41
|
-
dsh --profile tui # launch
|
|
53
|
+
dsh --profile tui # launch
|
|
42
54
|
```
|
|
43
55
|
|
|
44
56
|
Legacy `session_projcache` records (written before dsh 0.1.2-alpha.4) are migrated automatically at profile load — idempotent, with per-file backups, never blocking startup; the `dsh-tui-pi` launcher runs the same preflight.
|
|
45
57
|
|
|
46
|
-
Everything that used to need manual patching — the canvas background, the `@deepseek-ai` module closure, the compaction backend — now happens automatically.
|
|
58
|
+
Everything that used to need manual patching — the canvas background, the `@deepseek-ai` module closure, the compaction backend — now happens automatically. For a normal install, upgrading the global package upgrades the profile on the next launch (see above). `dev-upgrade` remains for dev machines that install the profile from a checked-out repo:
|
|
47
59
|
|
|
48
60
|
```sh
|
|
49
61
|
node scripts/dev-upgrade.mjs # latest
|
package/bin/boot.mjs
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Launcher entry behind the `dsh-tui-pi` and `dst` bin shims: boot the dsh
|
|
4
|
+
* `tui` profile, running the one-time onboarding first when this machine is
|
|
5
|
+
* missing any of the three pieces (dsh host, pnpm, mounted tui profile).
|
|
6
|
+
*
|
|
7
|
+
* Fast path — host present and profile current: no output, straight to the
|
|
8
|
+
* TUI, exactly like the plain `dsh --profile tui` this replaces.
|
|
9
|
+
*
|
|
10
|
+
* Version follow: after `npm i -g` upgrades this package, the profile still
|
|
11
|
+
* holds the old copy; the next launch (or the package's postinstall) re-runs
|
|
12
|
+
* the add to move the profile to the new version. A profile that is NEWER
|
|
13
|
+
* than the global install (dev-upgraded ahead) is left untouched.
|
|
14
|
+
*
|
|
15
|
+
* See bin/onboard-core.mjs for the setup logic and its failure contract.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { spawnSync } from 'node:child_process'
|
|
19
|
+
import {
|
|
20
|
+
dshBinPath,
|
|
21
|
+
profileBehind,
|
|
22
|
+
profileMounted,
|
|
23
|
+
runOnboard,
|
|
24
|
+
} from './onboard-core.mjs'
|
|
25
|
+
|
|
26
|
+
const args = process.argv.slice(2)
|
|
27
|
+
|
|
28
|
+
function boot(dsh) {
|
|
29
|
+
const result = spawnSync(dsh, ['--profile', 'tui', ...args], { stdio: 'inherit' })
|
|
30
|
+
process.exit(result.status ?? 1)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const dsh = dshBinPath()
|
|
34
|
+
|
|
35
|
+
if (dsh && profileMounted() && !profileBehind()) {
|
|
36
|
+
boot(dsh)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
console.log('dsh-tui-pi setup (first run / upgrade only):')
|
|
40
|
+
const setup = runOnboard({ log: (line) => console.log(line) })
|
|
41
|
+
if (!setup.ok) {
|
|
42
|
+
console.error(`\ndsh-tui-pi: setup stopped at "${setup.step}".`)
|
|
43
|
+
console.error(`dsh-tui-pi: ${setup.hint}`)
|
|
44
|
+
process.exit(1)
|
|
45
|
+
}
|
|
46
|
+
console.log('')
|
|
47
|
+
boot(setup.dsh)
|
package/bin/dsh-tui-pi
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env sh
|
|
2
|
-
#
|
|
3
|
-
#
|
|
4
|
-
#
|
|
2
|
+
# Launcher for the dsh `tui` profile that mounts @aiwayds/dsh-tui-pi.
|
|
3
|
+
# First run performs a one-time setup (dsh host, pnpm, profile plugins) with
|
|
4
|
+
# progress output; every later run boots straight into the TUI. Aliased as `dst`.
|
|
5
5
|
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$(readlink -f -- "$0" 2>/dev/null || echo "$0")")" && pwd)
|
|
6
6
|
node "$SCRIPT_DIR/preflight-projcache.mjs" >/dev/null 2>&1 || true
|
|
7
|
-
exec
|
|
7
|
+
exec node "$SCRIPT_DIR/boot.mjs" "$@"
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One-time onboarding for the `dsh-tui-pi` / `dst` launcher, shared by two
|
|
3
|
+
* entry points:
|
|
4
|
+
* - scripts/postinstall.mjs — runs at `npm i -g @aiwayds/dsh-tui-pi` time,
|
|
5
|
+
* so the package is usable the moment the install prints "added 1 package";
|
|
6
|
+
* - bin/boot.mjs — runs on the first `dsh-tui-pi` invocation, catching up
|
|
7
|
+
* anything the postinstall could not finish (offline install, pnpm that
|
|
8
|
+
* does not run postinstall scripts, non-global context, …).
|
|
9
|
+
*
|
|
10
|
+
* Contract:
|
|
11
|
+
* - idempotent: every step checks before it acts, so a re-run only prints
|
|
12
|
+
* "found" lines and the launcher fast-paths to a silent boot;
|
|
13
|
+
* - the dsh host is installed globally WITH NPM when missing (largest
|
|
14
|
+
* step); npm itself is a prerequisite — when it is not on PATH we print
|
|
15
|
+
* what to install and stop instead of guessing;
|
|
16
|
+
* - all functions are synchronous and never throw past the boundary:
|
|
17
|
+
* failures come back as { ok: false, step, hint }.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { execFileSync, spawnSync } from 'node:child_process'
|
|
21
|
+
import { accessSync, constants, existsSync, readFileSync, statSync, writeFileSync } from 'node:fs'
|
|
22
|
+
import { homedir } from 'node:os'
|
|
23
|
+
import { delimiter, dirname, join } from 'node:path'
|
|
24
|
+
import { fileURLToPath } from 'node:url'
|
|
25
|
+
|
|
26
|
+
const PKG = '@aiwayds/dsh-tui-pi'
|
|
27
|
+
const HOST = '@deepseek-ai/dsh'
|
|
28
|
+
const PROFILE = 'tui'
|
|
29
|
+
const INSTALL_TIMEOUT_MS = 10 * 60_000
|
|
30
|
+
|
|
31
|
+
const packageRoot = join(dirname(fileURLToPath(import.meta.url)), '..')
|
|
32
|
+
|
|
33
|
+
function readSelfPackage() {
|
|
34
|
+
return JSON.parse(readFileSync(join(packageRoot, 'package.json'), 'utf8'))
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function selfVersion() {
|
|
38
|
+
try {
|
|
39
|
+
return readSelfPackage().version ?? null
|
|
40
|
+
} catch {
|
|
41
|
+
return null
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function dshHome() {
|
|
46
|
+
return process.env.DSH_HOME || join(homedir(), '.dsh')
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// The profile counts as set up once this package is resolvable inside it.
|
|
50
|
+
export function profileMounted() {
|
|
51
|
+
return existsSync(join(dshHome(), 'profiles', PROFILE, 'node_modules', PKG, 'package.json'))
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function profileVersion() {
|
|
55
|
+
try {
|
|
56
|
+
return JSON.parse(
|
|
57
|
+
readFileSync(join(dshHome(), 'profiles', PROFILE, 'node_modules', PKG, 'package.json'), 'utf8'),
|
|
58
|
+
).version ?? null
|
|
59
|
+
} catch {
|
|
60
|
+
return null
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Companion plugins ship as npm dependencies of this package and land in the
|
|
65
|
+
// profile's node_modules together with it when `dsh plugin add` runs.
|
|
66
|
+
export function companions() {
|
|
67
|
+
try {
|
|
68
|
+
const deps = readSelfPackage().dependencies ?? {}
|
|
69
|
+
return Object.keys(deps).filter((name) => name.startsWith('@aiwayds/') && name !== PKG)
|
|
70
|
+
} catch {
|
|
71
|
+
return []
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function which(cmd) {
|
|
76
|
+
const dirs = (process.env.PATH || '').split(delimiter).filter(Boolean)
|
|
77
|
+
const suffixes = process.platform === 'win32' ? ['.cmd', '.exe', '.bat', ''] : ['']
|
|
78
|
+
for (const dir of dirs) {
|
|
79
|
+
for (const suffix of suffixes) {
|
|
80
|
+
const candidate = join(dir, cmd + suffix)
|
|
81
|
+
try {
|
|
82
|
+
if (!statSync(candidate).isFile()) continue
|
|
83
|
+
accessSync(candidate, constants.X_OK)
|
|
84
|
+
return candidate
|
|
85
|
+
} catch {
|
|
86
|
+
/* keep scanning */
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return null
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function queryVersion(cmd, args) {
|
|
94
|
+
try {
|
|
95
|
+
return execFileSync(cmd, args, {
|
|
96
|
+
encoding: 'utf8',
|
|
97
|
+
timeout: 20_000,
|
|
98
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
99
|
+
}).trim() || null
|
|
100
|
+
} catch {
|
|
101
|
+
return null
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function dshBinPath() {
|
|
106
|
+
const onPath = which('dsh')
|
|
107
|
+
if (onPath) return onPath
|
|
108
|
+
// A host installed moments ago can be missing from this shell's PATH cache;
|
|
109
|
+
// fall back to the npm global prefix the installer just wrote to.
|
|
110
|
+
try {
|
|
111
|
+
const prefix = execFileSync('npm', ['prefix', '-g'], {
|
|
112
|
+
encoding: 'utf8',
|
|
113
|
+
timeout: 20_000,
|
|
114
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
115
|
+
}).trim()
|
|
116
|
+
const candidate = join(prefix, 'bin', 'dsh')
|
|
117
|
+
if (existsSync(candidate)) return candidate
|
|
118
|
+
} catch {
|
|
119
|
+
/* no npm, no fallback */
|
|
120
|
+
}
|
|
121
|
+
return null
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Global installs always go through npm — one predictable path, regardless
|
|
125
|
+
// of which package manager pulled this package in.
|
|
126
|
+
function installGlobal(name) {
|
|
127
|
+
const result = spawnSync('npm', ['install', '-g', name], {
|
|
128
|
+
stdio: 'inherit',
|
|
129
|
+
timeout: INSTALL_TIMEOUT_MS,
|
|
130
|
+
shell: process.platform === 'win32',
|
|
131
|
+
})
|
|
132
|
+
return result.status === 0
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// True when the mounted profile copy is OLDER than this package. Prerelease
|
|
136
|
+
// tags compare numerically per dot segment (rc.10 > rc.4); a profile that is
|
|
137
|
+
// NEWER (dev-upgraded ahead of the global install) is left alone.
|
|
138
|
+
export function profileBehind() {
|
|
139
|
+
const mounted = profileVersion()
|
|
140
|
+
const self = selfVersion()
|
|
141
|
+
if (!mounted || !self) return false
|
|
142
|
+
if (mounted === self) return false
|
|
143
|
+
return compareVersions(mounted, self) < 0
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function compareVersions(a, b) {
|
|
147
|
+
const parse = (version) => {
|
|
148
|
+
const [core, pre = ''] = String(version).replace(/^v/, '').split(/[-+]/, 2)
|
|
149
|
+
return { core: core.split('.').map((n) => Number.parseInt(n, 10) || 0), pre: pre ? pre.split('.') : [] }
|
|
150
|
+
}
|
|
151
|
+
const pa = parse(a)
|
|
152
|
+
const pb = parse(b)
|
|
153
|
+
for (let i = 0; i < 3; i += 1) {
|
|
154
|
+
if (pa.core[i] !== pb.core[i]) return pa.core[i] - pb.core[i]
|
|
155
|
+
}
|
|
156
|
+
if (pa.pre.length === 0 && pb.pre.length === 0) return 0
|
|
157
|
+
if (pa.pre.length === 0) return 1
|
|
158
|
+
if (pb.pre.length === 0) return -1
|
|
159
|
+
for (let i = 0; i < Math.max(pa.pre.length, pb.pre.length); i += 1) {
|
|
160
|
+
const x = pa.pre[i]
|
|
161
|
+
const y = pb.pre[i]
|
|
162
|
+
if (x === undefined) return -1
|
|
163
|
+
if (y === undefined) return 1
|
|
164
|
+
const xn = Number.parseInt(x, 10)
|
|
165
|
+
const yn = Number.parseInt(y, 10)
|
|
166
|
+
if (!Number.isNaN(xn) && !Number.isNaN(yn) && xn !== yn) return xn - yn
|
|
167
|
+
if (x !== y) return x < y ? -1 : 1
|
|
168
|
+
}
|
|
169
|
+
return 0
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// pnpm ≥10 fails a fresh `add` with ERR_PNPM_IGNORED_BUILDS when the added
|
|
173
|
+
// packages carry lifecycle scripts. The only scripts our packages ship are
|
|
174
|
+
// the closure-linker postinstalls, which no-op on published tarballs (they
|
|
175
|
+
// guard on the repo's src/), so approving them is safe and is what `pnpm
|
|
176
|
+
// approve-builds` would do interactively. pnpm sometimes records an
|
|
177
|
+
// allowBuilds skeleton in the profile's pnpm-workspace.yaml with a
|
|
178
|
+
// "set this to true or false" placeholder — fill those placeholders; when
|
|
179
|
+
// it does not (behavior varies by pnpm build and environment), write the
|
|
180
|
+
// allowBuilds block ourselves for exactly the packages this suite ships.
|
|
181
|
+
// A user-written true/false decision is never overwritten. Returns whether
|
|
182
|
+
// the workspace file was updated.
|
|
183
|
+
function approvePrintedBuilds() {
|
|
184
|
+
const workspace = join(dshHome(), 'profiles', PROFILE, 'pnpm-workspace.yaml')
|
|
185
|
+
try {
|
|
186
|
+
const text = readFileSync(workspace, 'utf8')
|
|
187
|
+
if (text.includes('set this to true or false')) {
|
|
188
|
+
writeFileSync(workspace, text.replaceAll('set this to true or false', 'true'))
|
|
189
|
+
return true
|
|
190
|
+
}
|
|
191
|
+
if (text.includes('allowBuilds:')) return false
|
|
192
|
+
const names = [PKG, ...companions()]
|
|
193
|
+
const block = ['allowBuilds:', ...names.map((name) => ` '${name}': true`)].join('\n')
|
|
194
|
+
writeFileSync(workspace, `${text.replace(/\n*$/, '\n')}${block}\n`)
|
|
195
|
+
return true
|
|
196
|
+
} catch {
|
|
197
|
+
return false
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Bring the machine to a bootable state, printing one progress line per step.
|
|
203
|
+
* Steps:
|
|
204
|
+
* 1/3 dsh host — installed globally with npm when missing; when npm
|
|
205
|
+
* itself is not on PATH, print what to install and stop
|
|
206
|
+
* 2/3 pnpm — the host's plugin manager shells out to it; installed
|
|
207
|
+
* globally when missing
|
|
208
|
+
* 3/3 tui profile — `dsh plugin --profile tui add` this package (+ companions)
|
|
209
|
+
* Returns { ok: true, dsh } or { ok: false, step, hint }.
|
|
210
|
+
*/
|
|
211
|
+
export function runOnboard({ log = () => {} } = {}) {
|
|
212
|
+
let dsh = dshBinPath()
|
|
213
|
+
if (dsh) {
|
|
214
|
+
log(` [1/3] dsh host found ${queryVersion(dsh, ['-V']) ?? '(version unknown)'}`)
|
|
215
|
+
} else if (!which('npm')) {
|
|
216
|
+
log(` [1/3] dsh host not found, and npm is not on PATH either`)
|
|
217
|
+
return {
|
|
218
|
+
ok: false,
|
|
219
|
+
step: 'npm',
|
|
220
|
+
hint: 'install Node.js (ships with npm) first: https://nodejs.org — then re-run dsh-tui-pi',
|
|
221
|
+
}
|
|
222
|
+
} else {
|
|
223
|
+
log(` [1/3] dsh host not found — installing ${HOST} globally with npm (largest step, a few minutes)…`)
|
|
224
|
+
if (!installGlobal(HOST)) {
|
|
225
|
+
return { ok: false, step: 'dsh host install', hint: `install it with: npm install -g ${HOST} — then re-run dsh-tui-pi` }
|
|
226
|
+
}
|
|
227
|
+
dsh = dshBinPath()
|
|
228
|
+
if (!dsh) {
|
|
229
|
+
return { ok: false, step: 'dsh host lookup', hint: `installed, but still not found — run: npm install -g ${HOST}` }
|
|
230
|
+
}
|
|
231
|
+
log(` [1/3] dsh host installed ${queryVersion(dsh, ['-V']) ?? ''}`)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const pnpm = which('pnpm')
|
|
235
|
+
if (pnpm) {
|
|
236
|
+
log(` [2/3] pnpm found ${queryVersion(pnpm, ['-v']) ?? ''}`)
|
|
237
|
+
} else {
|
|
238
|
+
log(' [2/3] pnpm not found — installing globally…')
|
|
239
|
+
if (!installGlobal('pnpm')) {
|
|
240
|
+
return { ok: false, step: 'pnpm install', hint: 'install it with: npm install -g pnpm — then re-run dsh-tui-pi' }
|
|
241
|
+
}
|
|
242
|
+
log(` [2/3] pnpm installed ${queryVersion('pnpm', ['-v']) ?? ''}`)
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (profileMounted() && !profileBehind()) {
|
|
246
|
+
log(' [3/3] tui profile already set up')
|
|
247
|
+
} else {
|
|
248
|
+
const verb = profileMounted() ? 'updating' : 'creating'
|
|
249
|
+
const list = companions()
|
|
250
|
+
log(` [3/3] tui profile ${verb}: installing ${PKG}${list.length ? ` + ${list.length} bundled companions` : ''}…`)
|
|
251
|
+
if (list.length) log(` ${list.join(', ')}`)
|
|
252
|
+
const result = spawnSync(dsh, ['plugin', '--profile', PROFILE, 'add', PKG], {
|
|
253
|
+
stdio: 'inherit',
|
|
254
|
+
timeout: INSTALL_TIMEOUT_MS,
|
|
255
|
+
})
|
|
256
|
+
if (result.status !== 0) {
|
|
257
|
+
if (!approvePrintedBuilds()) {
|
|
258
|
+
return { ok: false, step: 'tui profile setup', hint: `run it manually with: dsh plugin --profile ${PROFILE} add ${PKG} — then re-run dsh-tui-pi` }
|
|
259
|
+
}
|
|
260
|
+
// pnpm's skeleton is filled; the same idempotent add now succeeds
|
|
261
|
+
// (everything is already downloaded).
|
|
262
|
+
log(' [3/3] tui profile approved the printed build scripts (our closure-linker no-ops) — retrying…')
|
|
263
|
+
const retry = spawnSync(dsh, ['plugin', '--profile', PROFILE, 'add', PKG], {
|
|
264
|
+
stdio: 'inherit',
|
|
265
|
+
timeout: INSTALL_TIMEOUT_MS,
|
|
266
|
+
})
|
|
267
|
+
if (retry.status !== 0) {
|
|
268
|
+
return { ok: false, step: 'tui profile setup', hint: `run it manually with: dsh plugin --profile ${PROFILE} add ${PKG} — then re-run dsh-tui-pi` }
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
log(` [3/3] tui profile ${verb === 'updating' ? 'updated' : 'done'}`)
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return { ok: true, dsh }
|
|
275
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
2
|
+
import type { AskUserPanelDeps } from './ask-user.ts';
|
|
3
|
+
import type { DialogQueue } from './dialog-queue.ts';
|
|
4
|
+
export type ApprovalOutcome = 'allowed-once' | 'rejected' | 'cancelled' | 'unavailable';
|
|
5
|
+
export interface ApprovalRequest {
|
|
6
|
+
agent: {
|
|
7
|
+
session: {
|
|
8
|
+
id: unknown;
|
|
9
|
+
seq?: number;
|
|
10
|
+
eventAt?: (seq: number) => unknown;
|
|
11
|
+
};
|
|
12
|
+
};
|
|
13
|
+
toolName: string;
|
|
14
|
+
callId?: string;
|
|
15
|
+
reason?: string;
|
|
16
|
+
signal?: AbortSignal;
|
|
17
|
+
}
|
|
18
|
+
/** Keep the exact tool arguments visible; the reason alone is not the operation. */
|
|
19
|
+
export declare function approvalDetails(request: ApprovalRequest): string;
|
|
20
|
+
/** A one-shot decision. Enter defaults to deny; paste never implies consent. */
|
|
21
|
+
export declare function openApprovalPanel(deps: AskUserPanelDeps, request: ApprovalRequest, signal: AbortSignal): Promise<ApprovalOutcome>;
|
|
22
|
+
/** Claim only the active session; other answerers can serve other sessions. */
|
|
23
|
+
export declare function registerApprovalProvider(ctx: Context, deps: AskUserPanelDeps, queue: DialogQueue): () => void;
|
package/lib/approval.js
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { matchesKey, Text } from '@earendil-works/pi-tui';
|
|
2
|
+
import { ansiFg, RESET } from "./theme/index.js";
|
|
3
|
+
import { clipToWidth, visibleWidth } from "./text.js";
|
|
4
|
+
/** Keep the exact tool arguments visible; the reason alone is not the operation. */
|
|
5
|
+
export function approvalDetails(request) {
|
|
6
|
+
const lines = ['Tool: ' + request.toolName];
|
|
7
|
+
if (request.reason)
|
|
8
|
+
lines.push('Reason: ' + request.reason);
|
|
9
|
+
if (request.callId) {
|
|
10
|
+
lines.push('Call: ' + request.callId);
|
|
11
|
+
const session = request.agent.session;
|
|
12
|
+
for (let seq = (session.seq ?? 0) - 1; seq >= 0; seq--) {
|
|
13
|
+
const event = session.eventAt?.(seq);
|
|
14
|
+
if (event?.type === 'tool/call' && event.data?.callId === request.callId) {
|
|
15
|
+
if (event.data.arguments)
|
|
16
|
+
lines.push('Arguments: ' + event.data.arguments);
|
|
17
|
+
break;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return lines.join('\n').replace(/[\u0000-\u0008\u000b-\u001f\u007f-\u009f]/g, char => '\\u' + char.charCodeAt(0).toString(16).padStart(4, '0'));
|
|
22
|
+
}
|
|
23
|
+
/** A one-shot decision. Enter defaults to deny; paste never implies consent. */
|
|
24
|
+
export function openApprovalPanel(deps, request, signal) {
|
|
25
|
+
if (signal.aborted)
|
|
26
|
+
return Promise.resolve('cancelled');
|
|
27
|
+
return new Promise(resolve => {
|
|
28
|
+
let selected = 0;
|
|
29
|
+
let offset = 0;
|
|
30
|
+
let settled = false;
|
|
31
|
+
let unmount;
|
|
32
|
+
const details = approvalDetails(request);
|
|
33
|
+
const detailText = new Text(details, 0, 0);
|
|
34
|
+
let detailsWidth = -1;
|
|
35
|
+
let content = [];
|
|
36
|
+
const finish = (outcome) => {
|
|
37
|
+
if (settled)
|
|
38
|
+
return;
|
|
39
|
+
settled = true;
|
|
40
|
+
signal.removeEventListener('abort', abort);
|
|
41
|
+
unmount?.();
|
|
42
|
+
deps.setModalActive(false);
|
|
43
|
+
deps.restoreFocus();
|
|
44
|
+
resolve(outcome);
|
|
45
|
+
};
|
|
46
|
+
const abort = () => finish('cancelled');
|
|
47
|
+
const panel = {
|
|
48
|
+
invalidate() { },
|
|
49
|
+
render(width) {
|
|
50
|
+
const inner = Math.max(1, width - 4);
|
|
51
|
+
const theme = deps.theme();
|
|
52
|
+
const border = ansiFg(theme.palette.borderDefault);
|
|
53
|
+
const row = (text) => {
|
|
54
|
+
const clipped = clipToWidth(text, inner);
|
|
55
|
+
return border + '│ ' + RESET + clipped + ' '.repeat(Math.max(0, inner - visibleWidth(clipped))) + border + ' │' + RESET;
|
|
56
|
+
};
|
|
57
|
+
if (inner !== detailsWidth) {
|
|
58
|
+
content = detailText.render(inner);
|
|
59
|
+
detailsWidth = inner;
|
|
60
|
+
}
|
|
61
|
+
const budget = Math.max(1, Math.min(10, (deps.tui.terminal.rows || 24) - 16));
|
|
62
|
+
offset = Math.max(0, Math.min(offset, content.length - budget));
|
|
63
|
+
return [
|
|
64
|
+
border + '┌' + '─'.repeat(Math.max(0, width - 2)) + '┐' + RESET,
|
|
65
|
+
row('Approval required'),
|
|
66
|
+
...content.slice(offset, offset + budget).map(row),
|
|
67
|
+
row('Details ' + (offset + 1) + '–' + Math.min(offset + budget, content.length) + '/' + content.length + ' · PgUp/PgDn scroll'),
|
|
68
|
+
row((selected === 0 ? '▸ ' : ' ') + '1. Deny'),
|
|
69
|
+
row((selected === 1 ? '▸ ' : ' ') + '2. Allow once'),
|
|
70
|
+
row('↑↓ choose · Enter confirm · Esc/Ctrl+C deny'),
|
|
71
|
+
border + '└' + '─'.repeat(Math.max(0, width - 2)) + '┘' + RESET,
|
|
72
|
+
];
|
|
73
|
+
},
|
|
74
|
+
handleInput(data) {
|
|
75
|
+
if (data.startsWith('\x1b[200~'))
|
|
76
|
+
return;
|
|
77
|
+
if (matchesKey(data, 'escape') || matchesKey(data, 'ctrl+c')) {
|
|
78
|
+
finish('rejected');
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (data === '1') {
|
|
82
|
+
finish('rejected');
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (data === '2') {
|
|
86
|
+
selected = 1;
|
|
87
|
+
}
|
|
88
|
+
else if (matchesKey(data, 'up') || matchesKey(data, 'down'))
|
|
89
|
+
selected = selected === 0 ? 1 : 0;
|
|
90
|
+
else if (matchesKey(data, 'pageUp'))
|
|
91
|
+
offset = Math.max(0, offset - 5);
|
|
92
|
+
else if (matchesKey(data, 'pageDown'))
|
|
93
|
+
offset += 5;
|
|
94
|
+
else if (matchesKey(data, 'enter')) {
|
|
95
|
+
finish(selected === 1 ? 'allowed-once' : 'rejected');
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
deps.tui.requestRender();
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
try {
|
|
102
|
+
unmount = deps.mount(panel);
|
|
103
|
+
deps.tui.hideOverlay();
|
|
104
|
+
deps.tui.setFocus(panel);
|
|
105
|
+
deps.setModalActive(true);
|
|
106
|
+
signal.addEventListener('abort', abort, { once: true });
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
finish('unavailable');
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
/** Claim only the active session; other answerers can serve other sessions. */
|
|
114
|
+
export function registerApprovalProvider(ctx, deps, queue) {
|
|
115
|
+
const register = ctx.on.bind(ctx);
|
|
116
|
+
return register('approval/request', (request, next) => {
|
|
117
|
+
const mine = deps.getSessionId?.();
|
|
118
|
+
if (mine === undefined || String(request.agent.session.id) !== mine)
|
|
119
|
+
return next();
|
|
120
|
+
return queue.run(signal => openApprovalPanel(deps, request, signal), request.signal)
|
|
121
|
+
.catch(error => error instanceof Error && error.name === 'AbortError' ? 'cancelled' : 'unavailable');
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
//# sourceMappingURL=approval.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"approval.js","sourceRoot":"","sources":["../src/approval.ts"],"names":[],"mappings":"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"}
|
package/lib/ask-user.d.ts
CHANGED
|
@@ -521,6 +521,8 @@ export interface AskUserPanelDeps {
|
|
|
521
521
|
* human exactly as before (direct callers, tests, embedders).
|
|
522
522
|
*/
|
|
523
523
|
resolveTimeouts?: () => Promise<AskUserTimeouts>;
|
|
524
|
+
/** Serialize this panel with other capturing dialogs. */
|
|
525
|
+
openPanel?: (questions: AskUserQuestionItem[], signal: AbortSignal | undefined, timeouts: AskUserTimeouts) => Promise<AskUserQuestionAnswer>;
|
|
524
526
|
}
|
|
525
527
|
/** Result promise from `openAskUserPanel`. Declined carries the canonical decline envelope. */
|
|
526
528
|
export type AskUserResult = AskUserQuestionAnswer;
|
package/lib/ask-user.js
CHANGED
|
@@ -1664,6 +1664,7 @@ export function openAskUserPanel(deps, questions, signal, timeouts = ASK_USER_TI
|
|
|
1664
1664
|
* mounted tool with no UI and no trace.
|
|
1665
1665
|
*/
|
|
1666
1666
|
export function registerAskUserProvider(ctx, deps) {
|
|
1667
|
+
const openPanel = deps.openPanel ?? ((questions, signal, timeouts) => openAskUserPanel(deps, questions, signal, timeouts));
|
|
1667
1668
|
// dsh-ask-router present: register as ONE surface among several (claim by
|
|
1668
1669
|
// session, first answer wins across surfaces) instead of taking an ask
|
|
1669
1670
|
// slot. Absent router keeps the standalone waterfall path below.
|
|
@@ -1685,7 +1686,7 @@ export function registerAskUserProvider(ctx, deps) {
|
|
|
1685
1686
|
const controller = new AbortController();
|
|
1686
1687
|
controllers.set(request, controller);
|
|
1687
1688
|
request.signal?.addEventListener('abort', () => controller.abort(), { once: true });
|
|
1688
|
-
const open = (timeouts) =>
|
|
1689
|
+
const open = (timeouts) => openPanel(request.questions, controller.signal, timeouts);
|
|
1689
1690
|
if (deps.resolveTimeouts === undefined)
|
|
1690
1691
|
return open(ASK_USER_TIMEOUTS_DISABLED);
|
|
1691
1692
|
// Resolved fresh per ask so a committed settings change applies to
|
|
@@ -1723,7 +1724,7 @@ export function registerAskUserProvider(ctx, deps) {
|
|
|
1723
1724
|
if (asking !== undefined && String(asking) !== mine)
|
|
1724
1725
|
return next();
|
|
1725
1726
|
}
|
|
1726
|
-
const open = (timeouts) =>
|
|
1727
|
+
const open = (timeouts) => openPanel(request.questions, request.signal, timeouts);
|
|
1727
1728
|
if (deps.resolveTimeouts === undefined)
|
|
1728
1729
|
return open(ASK_USER_TIMEOUTS_DISABLED);
|
|
1729
1730
|
return deps.resolveTimeouts().then(open, () => open(ASK_USER_TIMEOUTS_DISABLED));
|