@aiwayds/dsh-tui-pi 0.22.0 → 0.23.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 +12 -9
- package/lib/ask-user.d.ts +86 -17
- package/lib/ask-user.js +290 -63
- package/lib/ask-user.js.map +1 -1
- package/lib/index.js +23 -3
- package/lib/index.js.map +1 -1
- package/lib/notice-bridge.d.ts +66 -0
- package/lib/notice-bridge.js +105 -0
- package/lib/notice-bridge.js.map +1 -0
- package/lib/retention.d.ts +12 -7
- package/lib/retention.js +29 -16
- package/lib/retention.js.map +1 -1
- package/lib/sessions.d.ts +4 -3
- package/lib/sessions.js +10 -8
- package/lib/sessions.js.map +1 -1
- package/lib/theme-settings.js +7 -2
- package/lib/theme-settings.js.map +1 -1
- package/lib/tui.d.ts +12 -0
- package/lib/tui.js +72 -0
- package/lib/tui.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -201,12 +201,13 @@ There's no slash command to toggle the feature — it's always on, controlled by
|
|
|
201
201
|
|
|
202
202
|
While the model is mid-turn it can pause and ask you structured questions via the `ask_user_question` tool (`@deepseek-ai/dsh-tool-ask-user`, mounted by this profile's bundle patch). The TUI hosts the answering side: a bordered panel pins itself directly above the chat input (the Todos-panel slot — no floating popup), takes the keyboard while open, the tool call stays pending until you answer, and your answers flow back to the model as a normal tool result.
|
|
203
203
|
|
|
204
|
-
- **One
|
|
204
|
+
- **One question at a time, tabs for the rest** — with several questions the panel shows exactly ONE question block (header row + supporting `detail` text, option rows, plus a `Type something.` sentinel row for free text); a tab strip under the title (`[1] · 2✓ · 3` — brackets mark the focused tab, ✓ an answered one) folds the other questions away. `←`/`→` (and Tab/Shift-Tab) switch tabs; answering a single-select tab auto-advances to the next unanswered one (or onto the Confirm row once everything is answered). Single-select replaces on Enter; multi-select toggles (`●`/`○` marks) and never auto-advances.
|
|
205
|
+
- **Ctrl+T folds the panel to a 3-line strip** — the questions panel can block the transcript it stacks on while you think; Ctrl+T collapses it to borders + one summary line (phase, tab position, answered count, how to expand) and the same key unfolds it. While folded only the toggle and the Esc chain act; folding mid-edit commits the buffer like the ↑↓ arrow-exit does.
|
|
205
206
|
- **Single-question fast path** — a lone single-select question submits immediately on Enter: picking an option or committing typed free text both submit right away (a question without options is answered by typing alone). A lone multiSelect question instead gets a `⏎ Confirm answers` row so you can pick several options before submitting.
|
|
206
|
-
- **Multi-question review page** — with ≥ 2 questions a `⏎ Confirm answers` row hops to a review listing every answer, each row editable in place; `Submit answers` commits (Enter on it while an answer is missing flashes a hint instead of failing silently).
|
|
207
|
+
- **Multi-question review page** — with ≥ 2 questions a `⏎ Confirm answers` row hops to a review listing every answer, each row editable in place (jumping back re-focuses that question's tab); `Submit answers` commits (Enter on it while an answer is missing flashes a hint instead of failing silently).
|
|
207
208
|
- **Double-Esc declines** — two Esc presses within 200 ms return a declined envelope (the model reads it as a normal reply that no answer was given); holding Esc does not accidentally fire (key auto-repeat below a minimum gap is ignored), and the tool call being aborted settles as declined too. While the panel is open it owns the keyboard exactly like an open overlay: Esc never arms the running-task stop, and app keys (Ctrl+L/G/O, Tab) yield to the panel.
|
|
208
209
|
- **Conservative-use guidance** — a system-prompt section nudges the model to ask only when it genuinely needs you (1–3 questions, 2–4 options each), so the TUI doesn't turn into a questionnaire.
|
|
209
|
-
- **Keyboard** — `↑↓` navigate · `Enter` select/toggle/confirm · type into the sentinel for free text · `Esc` twice to decline.
|
|
210
|
+
- **Keyboard** — `←→` switch question tabs · `↑↓` navigate · `Enter` select/toggle/confirm · type into the sentinel for free text · `Ctrl+T` fold/unfold the panel · `Esc` twice to decline.
|
|
210
211
|
|
|
211
212
|
Inspired by [juicesharp/rpiv-ask-user-question](https://github.com/juicesharp/rpiv-ask-user-question).
|
|
212
213
|
|
|
@@ -304,8 +305,9 @@ Precedence per field: an explicit value in settings.yaml > the
|
|
|
304
305
|
`DSH_TUI_RETENTION_MAX_COUNT` / `DSH_TUI_RETENTION_MAX_AGE_DAYS` /
|
|
305
306
|
`DSH_TUI_RETENTION_MIN_IDLE_HOURS` and `DSH_TUI_RESUME_MAX_AGE_DAYS` /
|
|
306
307
|
`DSH_TUI_RESUME_MIN_BYTES` environment variables > the defaults above.
|
|
307
|
-
An invalid settings value
|
|
308
|
-
|
|
308
|
+
An invalid settings value surfaces a transient notice via the shared
|
|
309
|
+
notice bridge (silently dropped when no TUI sink is registered — headless
|
|
310
|
+
runs never print it) and falls to the next level; an invalid env value — a typo
|
|
309
311
|
never widens or guts the policy. `maxCount` and `minBytes` must be
|
|
310
312
|
integers at every layer (a fractional cap or byte floor is garbage, not a
|
|
311
313
|
window).
|
|
@@ -500,7 +502,8 @@ See [CHANGELOG.md](CHANGELOG.md) for the release history.
|
|
|
500
502
|
|
|
501
503
|
- [Ask User Question](#ask-user-question) is inspired by
|
|
502
504
|
[juicesharp/rpiv-ask-user-question](https://github.com/juicesharp/rpiv-ask-user-question) —
|
|
503
|
-
the interaction design (
|
|
504
|
-
multi-question review page, decline gesture
|
|
505
|
-
|
|
506
|
-
|
|
505
|
+
the interaction design (numbered option list with a free-text sentinel,
|
|
506
|
+
multi-question review page, decline gesture; since reworked into a
|
|
507
|
+
one-question-at-a-time tab view with a fold-away strip) was adapted to this
|
|
508
|
+
TUI's docked-panel and dsh `userQuestions` provider architecture. All code
|
|
509
|
+
here is original.
|
package/lib/ask-user.d.ts
CHANGED
|
@@ -8,12 +8,17 @@
|
|
|
8
8
|
* `AskUserQuestionAnswer` envelope back to `dsh-tool-ask-user` as a normal
|
|
9
9
|
* tool result.
|
|
10
10
|
*
|
|
11
|
-
* Layout — one bordered dock panel
|
|
12
|
-
* divider between
|
|
13
|
-
* so the cursor row is always visible even when the dock is
|
|
11
|
+
* Layout — one bordered dock panel showing EXACTLY ONE question at a time
|
|
12
|
+
* (numbered rows, a divider between the question block and its options, and
|
|
13
|
+
* a scroll window so the cursor row is always visible even when the dock is
|
|
14
|
+
* height-capped). With ≥ 2 questions a tab strip under the title carries one
|
|
15
|
+
* tab per question (`[1] · 2✓ · 3` — brackets mark the focused tab, ✓ an
|
|
16
|
+
* answered one); ←/→ (and Tab/Shift-Tab) switch tabs so a question batch
|
|
17
|
+
* never floods the dock:
|
|
14
18
|
*
|
|
15
19
|
* ┌────────────────────────────────────┐
|
|
16
|
-
* │ ● Questions (2)
|
|
20
|
+
* │ ● Questions (1/2) │
|
|
21
|
+
* │ [1] · 2 │ (tab strip, ≥ 2 questions only)
|
|
17
22
|
* │ SELECTION │
|
|
18
23
|
* │ ───────────────────────────── │ (table chrome)
|
|
19
24
|
* │ Fruit Where should we deploy? │ (header — not selectable)
|
|
@@ -26,6 +31,11 @@
|
|
|
26
31
|
* │ ⏎ Confirm answers │ (when ≥ 2 questions, or any multiSelect)
|
|
27
32
|
* └────────────────────────────────────┘
|
|
28
33
|
*
|
|
34
|
+
* Ctrl+T folds the whole panel into a 3-line strip (borders + one summary
|
|
35
|
+
* line) so the transcript stays readable while the question pends; the same
|
|
36
|
+
* key unfolds it. While folded only the toggle and the Esc chain do
|
|
37
|
+
* anything — answering keys are inert until the panel is back.
|
|
38
|
+
*
|
|
29
39
|
* While the panel is open it owns the keyboard: it is mounted into the dock
|
|
30
40
|
* slot between the live widgets and the editor, takes focus through
|
|
31
41
|
* `tui.setFocus`, and `setModalActive(true)` routes the app keymap exactly
|
|
@@ -45,7 +55,9 @@
|
|
|
45
55
|
* Single-question single-select panel: Enter on an option (or committing a
|
|
46
56
|
* filled sentinel) submits immediately. A multiSelect question — even a lone
|
|
47
57
|
* one — gets a Confirm row instead of auto-submitting, so the user can pick
|
|
48
|
-
* several options first. Multi-question panel:
|
|
58
|
+
* several options first. Multi-question panel: answering a single-select tab
|
|
59
|
+
* auto-advances the focus to the next unanswered tab (or onto the Confirm
|
|
60
|
+
* row once everything is answered). Enter on the Confirm row hops
|
|
49
61
|
* to the review page (all answers listed, each editable in place). Esc
|
|
50
62
|
* double-press within 200 ms declines — but terminal key auto-repeat (holding
|
|
51
63
|
* Esc) is ignored below `ESC_REPEAT_GUARD_MS`, so a long press cannot
|
|
@@ -65,7 +77,7 @@
|
|
|
65
77
|
* Inspired by juicesharp/rpiv-ask-user-question
|
|
66
78
|
* (https://github.com/juicesharp/rpiv-ask-user-question).
|
|
67
79
|
*/
|
|
68
|
-
import { type Component, type TUI } from '@earendil-works/pi-tui';
|
|
80
|
+
import { type Component, type KeyId, type TUI } from '@earendil-works/pi-tui';
|
|
69
81
|
import type { AskUserQuestionAnswer, AskUserQuestionItem, AskUserQuestionOption } from '@deepseek-ai/dsh-user-questions';
|
|
70
82
|
import type { Context } from '@deepseek-ai/cordis';
|
|
71
83
|
import { type TuiTheme } from './theme/index.ts';
|
|
@@ -80,6 +92,13 @@ export declare const DOUBLE_ESC_WINDOW_MS = 200;
|
|
|
80
92
|
export declare const ESC_REPEAT_GUARD_MS = 50;
|
|
81
93
|
/** Sentinel row label appended to every question's option list. */
|
|
82
94
|
export declare const SENTINEL_LABEL = "Type something.";
|
|
95
|
+
/**
|
|
96
|
+
* Key that folds the whole panel into a 3-line strip (and unfolds it again).
|
|
97
|
+
* A control key on purpose: it must never read as free-text input while the
|
|
98
|
+
* sentinel editor is engaged (folding commits the buffer, exactly like the
|
|
99
|
+
* ↑↓ arrow-exit path does).
|
|
100
|
+
*/
|
|
101
|
+
export declare const ASK_COLLAPSE_KEY: KeyId;
|
|
83
102
|
/** Decline message embedded into the answer envelope when the user bails. */
|
|
84
103
|
export declare const DECLINE_MESSAGE = "User declined to answer questions.";
|
|
85
104
|
/** Transient hint when Enter lands on an incomplete confirm/submit row. */
|
|
@@ -99,9 +118,11 @@ export declare const ASK_USER_MAX_VISIBLE = 6;
|
|
|
99
118
|
* panel budget is the terminal height minus the reserved dock rows, minus
|
|
100
119
|
* the panel's own chrome. At the 24-row e2e floor this yields exactly
|
|
101
120
|
* ASK_USER_MAX_VISIBLE (6); larger terminals scale up. Without a usable row
|
|
102
|
-
* count it degrades to ASK_USER_MAX_VISIBLE.
|
|
121
|
+
* count it degrades to ASK_USER_MAX_VISIBLE. `extraChromeLines` covers
|
|
122
|
+
* optional interior lines the default budget does not count (the tab strip
|
|
123
|
+
* of a multi-question panel).
|
|
103
124
|
*/
|
|
104
|
-
export declare function askUserMaxVisibleForRows(termRows: number | undefined): number;
|
|
125
|
+
export declare function askUserMaxVisibleForRows(termRows: number | undefined, extraChromeLines?: number): number;
|
|
105
126
|
/** A row in the multi-question view. */
|
|
106
127
|
export interface FlatRow {
|
|
107
128
|
kind: 'question-header' | 'option' | 'sentinel' | 'confirm';
|
|
@@ -133,8 +154,15 @@ export interface AskUserState {
|
|
|
133
154
|
questions: readonly AskUserQuestionItem[];
|
|
134
155
|
/** Per-question pending answer. */
|
|
135
156
|
perQuestion: PendingAnswer[];
|
|
136
|
-
/** Cursor position in the
|
|
157
|
+
/** Cursor position in the focused question's row list (see `buildRowList`). */
|
|
137
158
|
cursorIndex: number;
|
|
159
|
+
/**
|
|
160
|
+
* Which question tab the questions pane shows. The pane renders exactly
|
|
161
|
+
* one question at a time; ←/→ (and Tab/Shift-Tab) move this index.
|
|
162
|
+
*/
|
|
163
|
+
focusQuestion: number;
|
|
164
|
+
/** Panel folded to the 3-line strip (Ctrl+T). While folded only the toggle key and the Esc chain act. */
|
|
165
|
+
collapsed: boolean;
|
|
138
166
|
/** Live inline-edit text per question; non-null when the sentinel is engaged for that question. */
|
|
139
167
|
customInputs: (string | null)[];
|
|
140
168
|
/** Live inline edit owner (question index); null when not editing. */
|
|
@@ -187,8 +215,13 @@ export declare function didDoubleEscFire(prevState: AskUserState, nextState: Ask
|
|
|
187
215
|
export declare function buildAnswerEnvelope(state: AskUserState): AskUserQuestionAnswer;
|
|
188
216
|
/** Canonical envelope for the decline path: empty selected + custom decline message on every question. */
|
|
189
217
|
export declare function buildDeclinedEnvelope(questions: readonly AskUserQuestionItem[], message?: string): AskUserQuestionAnswer;
|
|
190
|
-
/**
|
|
191
|
-
|
|
218
|
+
/**
|
|
219
|
+
* Build the ordered row list for the questions pane: the FOCUSED question's
|
|
220
|
+
* block only (header + options + sentinel), followed by the panel-wide
|
|
221
|
+
* Confirm pseudo-row when `needsConfirmRow` says so — the pane shows one
|
|
222
|
+
* question at a time; ←/→ swaps `focusQuestion` and rebuilds this list.
|
|
223
|
+
*/
|
|
224
|
+
export declare function buildRowList(questions: readonly AskUserQuestionItem[], perQuestion: readonly PendingAnswer[], focusQuestion?: number): FlatRow[];
|
|
192
225
|
/** Find the next selectable index from `i` going in `direction`, clamped into range. */
|
|
193
226
|
export declare function nextSelectableIndex(rows: readonly FlatRow[], from: number, direction: 1 | -1): number;
|
|
194
227
|
/** True when every question has at least one selected option or a non-empty custom answer. */
|
|
@@ -233,15 +266,51 @@ export declare function needsConfirmRow(questions: readonly AskUserQuestionItem[
|
|
|
233
266
|
*/
|
|
234
267
|
export declare function canAutoSubmit(state: AskUserState): boolean;
|
|
235
268
|
/**
|
|
236
|
-
* Row index to land on after
|
|
237
|
-
*
|
|
238
|
-
*
|
|
269
|
+
* Row index to land on after a single-select answer on question `answeredQi`
|
|
270
|
+
* (option toggle or committed custom text): advance the tab focus to the
|
|
271
|
+
* nearest LATER unanswered question (cursor on its answer), or — once every
|
|
272
|
+
* question is answered — park the cursor on the Confirm row of the current
|
|
273
|
+
* tab so submission is one Enter away. Returns the state unchanged when no
|
|
274
|
+
* Confirm row exists (the lone single-select fast path submits instead).
|
|
239
275
|
*/
|
|
240
|
-
export declare function
|
|
241
|
-
/**
|
|
276
|
+
export declare function advanceAfterAnswer(state: AskUserState, answeredQi: number): AskUserState;
|
|
277
|
+
/** Index of the nearest question AFTER `answeredQi` with no answer yet, or -1. */
|
|
278
|
+
export declare function nextUnansweredQuestion(questions: readonly AskUserQuestionItem[], perQuestion: readonly PendingAnswer[], answeredQi: number): number;
|
|
279
|
+
/**
|
|
280
|
+
* Cursor position for a freshly focused question tab: its first selected
|
|
281
|
+
* option when one exists, else the sentinel when a custom answer exists,
|
|
282
|
+
* else the first selectable row — so revisiting an answered tab lands on
|
|
283
|
+
* the answer, not on option 1.
|
|
284
|
+
*/
|
|
285
|
+
export declare function focusCursor(rows: readonly FlatRow[], perQuestion: readonly PendingAnswer[], questionIndex: number): number;
|
|
286
|
+
/**
|
|
287
|
+
* Move the tab focus by `direction` (clamped at the ends — no wrap-around:
|
|
288
|
+
* predictable ends beat flourish mid-questionnaire) and land the cursor on
|
|
289
|
+
* the tab's answer per `focusCursor`. Clears the transient hints like any
|
|
290
|
+
* navigation. The component must exit an engaged sentinel edit BEFORE
|
|
291
|
+
* switching (committing the buffer), same as the ↑↓ arrow-exit path.
|
|
292
|
+
*/
|
|
293
|
+
export declare function switchFocus(state: AskUserState, direction: 1 | -1): AskUserState;
|
|
294
|
+
/**
|
|
295
|
+
* Flip the Ctrl+T fold. Folding while a sentinel edit is engaged first
|
|
296
|
+
* commits the buffer (the arrow-exit semantics — the text stays visible on
|
|
297
|
+
* the sentinel's ✎ mark), then collapses; transient hints clear on both
|
|
298
|
+
* directions. The double-Esc clock is deliberately untouched so an armed
|
|
299
|
+
* decline keeps its hint when the panel unfolds.
|
|
300
|
+
*/
|
|
301
|
+
export declare function toggleCollapse(state: AskUserState): AskUserState;
|
|
302
|
+
/** Render the questions pane (one focused question tab) as a flat table line list behind a scroll window. */
|
|
242
303
|
export declare function renderQuestionsView(theme: TuiTheme, state: AskUserState, width: number, maxVisible?: number): string[];
|
|
243
304
|
/** Render the review page for multi-question overlays (scroll-windowed). */
|
|
244
305
|
export declare function renderReviewView(theme: TuiTheme, state: AskUserState, width: number, maxVisible?: number): string[];
|
|
306
|
+
/**
|
|
307
|
+
* The ONE interior line of the folded (Ctrl+T) panel. The panel is a hard
|
|
308
|
+
* modal that owns the keyboard while it pends, so the strip keeps the
|
|
309
|
+
* state machine discoverable: which phase, how far along, how to unfold —
|
|
310
|
+
* and an armed decline replaces the summary so the 200 ms window is not
|
|
311
|
+
* silently ticking off-screen.
|
|
312
|
+
*/
|
|
313
|
+
export declare function renderCollapsedLine(theme: TuiTheme, state: AskUserState, width: number): string;
|
|
245
314
|
/** Options for assembling the panel + provider function. */
|
|
246
315
|
export interface AskUserPanelDeps {
|
|
247
316
|
tui: TUI;
|
|
@@ -291,7 +360,7 @@ export declare function isDuplicateProviderError(error: unknown): boolean;
|
|
|
291
360
|
* Wires the provider into `ctx.userQuestions`. Call from inside `ctx.effect`.
|
|
292
361
|
*
|
|
293
362
|
* Failure semantics are deliberate (review round BM):
|
|
294
|
-
* - missing service →
|
|
363
|
+
* - missing service → one notice + no-op disposer. The tool stays mounted by the
|
|
295
364
|
* bundle patch; without a provider its calls fail with the upstream
|
|
296
365
|
* NO_PROVIDER error, which is better than crashing the whole TUI plugin.
|
|
297
366
|
* - DUPLICATE_PROVIDER → silent no-op disposer (a prior UI owns the slot).
|