@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 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 (or: dsh-tui-pi)
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. Upgrade an existing profile after a release:
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
- # Convenience launcher: boots the dsh `tui` profile that mounts @aiwayds/dsh-tui-pi.
3
- # Requires the dsh CLI on PATH and @aiwayds/dsh-tui-pi installed into that profile
4
- # (dsh plugin --profile tui add @aiwayds/dsh-tui-pi).
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 dsh --profile tui "$@"
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;
@@ -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) => openAskUserPanel(deps, request.questions, controller.signal, 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) => openAskUserPanel(deps, request.questions, request.signal, 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));