@aiwayds/dsh-tui-pi 0.17.0 → 0.18.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 +64 -2
- package/README.zh.md +34 -0
- package/cordis.patch.yml +8 -0
- package/lib/ask-user.d.ts +207 -0
- package/lib/ask-user.js +714 -0
- package/lib/ask-user.js.map +1 -0
- package/lib/frame.d.ts +4 -3
- package/lib/frame.js +11 -8
- package/lib/frame.js.map +1 -1
- package/lib/index.js +35 -0
- package/lib/index.js.map +1 -1
- package/package.json +10 -1
package/README.md
CHANGED
|
@@ -162,6 +162,55 @@ Inside a subagent, a committed compaction is visible too: DCP appends one `user/
|
|
|
162
162
|
|
|
163
163
|
---
|
|
164
164
|
|
|
165
|
+
### APPEND_SYSTEM.md
|
|
166
|
+
|
|
167
|
+
A user-editable markdown file whose content is appended to the **system prompt of the main agent this TUI creates** — borrows pi's `~/.pi/agent/APPEND_SYSTEM.md` convention, dsh side: `$DSH_HOME/APPEND_SYSTEM.md` (default `~/.dsh/APPEND_SYSTEM.md`, honors the same `$DSH_HOME` override as the rest of dsh).
|
|
168
|
+
|
|
169
|
+
- **Hot-applied** — the section provider reads the file at every prompt assembly, so editing the file picks up on the **next request**: no restart, no watcher, no `/reload`.
|
|
170
|
+
- **Auto-seeded on first run** — when the file is missing, the TUI seeds it once at startup from the shipped template `templates/APPEND_SYSTEM.md` (the English orchestrator-identity template: identity, core rules, execution workflow). An existing file is yours — the TUI never overwrites user content.
|
|
171
|
+
- **TUI-owned section** — a marked block (`<!-- dsh-tui-pi:todo-lifecycle -->`) is appended once and then maintained idempotently so the model clears its `todo/write` list when every item is done. A marked file is left byte-identical on later startups.
|
|
172
|
+
- **Legacy migration** — the same todo block used to be delivered through `~/.dsh/AGENTS.md`. On startup the TUI strips that block once (no-op when absent), so the guidance is never duplicated.
|
|
173
|
+
- **Empty / unreadable = no section** — if the file is missing or can't be read, the section is silently dropped. No error, no TUI startup failure.
|
|
174
|
+
|
|
175
|
+
#### Scope: main agent only
|
|
176
|
+
|
|
177
|
+
The section is registered on the main agent's **scoped** agent context (`installAppendSystem` in `src/session.ts`) — it lands in that agent's own prompt-scope layer, which subagent scopes never merge. An orchestrator identity ("dispatch sub-agents, never execute yourself") riding on the children would defeat its own purpose, so children see nothing from this file. The mechanism is the same one `dsh-subagent-registry` uses for per-child personas.
|
|
178
|
+
|
|
179
|
+
#### Example
|
|
180
|
+
|
|
181
|
+
```sh
|
|
182
|
+
# Auto-seeded on first run from templates/APPEND_SYSTEM.md — open and edit.
|
|
183
|
+
$EDITOR ~/.dsh/APPEND_SYSTEM.md
|
|
184
|
+
|
|
185
|
+
# Or replace with your own from scratch (the TUI still keeps its marked
|
|
186
|
+
# todo-lifecycle section — it gets re-appended when missing).
|
|
187
|
+
cat > ~/.dsh/APPEND_SYSTEM.md <<'EOF'
|
|
188
|
+
# Project ground rules
|
|
189
|
+
|
|
190
|
+
- Always run `pnpm test` before claiming a task is done.
|
|
191
|
+
- Prefer dispatching `workhorse` for multi-step investigations.
|
|
192
|
+
EOF
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
There's no slash command to toggle the feature — it's always on, controlled by the file's contents.
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
### Ask User Question
|
|
200
|
+
|
|
201
|
+
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 framed overlay opens in place, the tool call stays pending until you answer, and your answers flow back to the model as a normal tool result.
|
|
202
|
+
|
|
203
|
+
- **One overlay, all questions flattened** — every question renders as a header row followed by its supporting `detail` text (when the model supplies any), option rows, plus a `Type something.` sentinel row for free text. Single-select replaces on Enter; multi-select toggles (`[+]` marks).
|
|
204
|
+
- **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.
|
|
205
|
+
- **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). After committing free text on one question the cursor hops to the next unanswered one.
|
|
206
|
+
- **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 closing the overlay through any other path — theme swap, `/reload`, or the tool call being aborted — settles as declined too.
|
|
207
|
+
- **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.
|
|
208
|
+
- **Keyboard** — `↑↓` navigate · `Enter` select/toggle/confirm · type into the sentinel for free text · `Esc` twice to decline.
|
|
209
|
+
|
|
210
|
+
Inspired by [juicesharp/rpiv-ask-user-question](https://github.com/juicesharp/rpiv-ask-user-question).
|
|
211
|
+
|
|
212
|
+
---
|
|
213
|
+
|
|
165
214
|
## Slash commands
|
|
166
215
|
|
|
167
216
|
| Command | What it does |
|
|
@@ -340,7 +389,7 @@ dsh --profile tui # or: dsh-tui-pi (bin shim)
|
|
|
340
389
|
```sh
|
|
341
390
|
pnpm check # tsc --noEmit
|
|
342
391
|
pnpm build # emit lib/
|
|
343
|
-
pnpm test # unit tests, node --test against lib/ (
|
|
392
|
+
pnpm test # unit tests, node --test against lib/ (652 tests, pretest builds)
|
|
344
393
|
```
|
|
345
394
|
|
|
346
395
|
Local type-checking symlinks `node_modules/@deepseek-ai/*` to the installed
|
|
@@ -372,8 +421,10 @@ src/
|
|
|
372
421
|
editor.ts CwdBorderEditor (top border: cwd + git branch)
|
|
373
422
|
subagent-policy.ts maxAgents guard + maxRounds wrap-up injection
|
|
374
423
|
subagent-viewer.ts Ctrl+G picker + live transcript panel + Enter steer injection
|
|
424
|
+
ask-user.ts Ask User Question overlay: pure state reducers +
|
|
425
|
+
framed overlay UI + ctx.userQuestions provider
|
|
375
426
|
theme/ GitHub light/dark palettes + terminal detection
|
|
376
|
-
test/*.test.mjs unit tests (
|
|
427
|
+
test/*.test.mjs unit tests (652 across 39 files)
|
|
377
428
|
```
|
|
378
429
|
|
|
379
430
|
---
|
|
@@ -381,3 +432,14 @@ test/*.test.mjs unit tests (569 across 38 files)
|
|
|
381
432
|
## Changelog
|
|
382
433
|
|
|
383
434
|
See [CHANGELOG.md](CHANGELOG.md) for the release history.
|
|
435
|
+
|
|
436
|
+
---
|
|
437
|
+
|
|
438
|
+
## Credits
|
|
439
|
+
|
|
440
|
+
- [Ask User Question](#ask-user-question) is inspired by
|
|
441
|
+
[juicesharp/rpiv-ask-user-question](https://github.com/juicesharp/rpiv-ask-user-question) —
|
|
442
|
+
the interaction design (flattened option list with a free-text sentinel,
|
|
443
|
+
multi-question review page, decline gesture) was adapted to this TUI's
|
|
444
|
+
framed-overlay and dsh `userQuestions` provider architecture. All code here
|
|
445
|
+
is original.
|
package/README.zh.md
CHANGED
|
@@ -154,6 +154,40 @@ dsh plugin --profile tui add @aiwayds/dsh-dcp
|
|
|
154
154
|
|
|
155
155
|
---
|
|
156
156
|
|
|
157
|
+
### APPEND_SYSTEM.md
|
|
158
|
+
|
|
159
|
+
一份用户可编辑的 markdown 文件,内容会**追加到 TUI 创建的主代理的系统提示末尾** —— 借鉴 pi 的 `~/.pi/agent/APPEND_SYSTEM.md` 约定,dsh 侧对应 `$DSH_HOME/APPEND_SYSTEM.md`(默认 `~/.dsh/APPEND_SYSTEM.md`,沿用 dsh 其余部分共用的 `$DSH_HOME` 覆盖)。
|
|
160
|
+
|
|
161
|
+
- **热应用** —— section 提供者在每次组装提示词时读盘,改完文件**下一次请求**即生效:无需重启、无需 watcher、无需 `/reload`。
|
|
162
|
+
- **首次启动自动播种** —— 文件不存在时,TUI 在启动时一次性从随包模板 `templates/APPEND_SYSTEM.md`(英文版协调者身份模板:身份、核心规则、执行工作流)创建。已有文件归用户所有 —— TUI 永远不会覆盖用户内容。
|
|
163
|
+
- **TUI 自有段落** —— 一段带标记的 block(`<!-- dsh-tui-pi:todo-lifecycle -->`)只在缺失时追加一次,并保持幂等,确保模型在所有 todo 都完成时清空 `todo/write` 列表。已带标记的文件后续启动原样保留。
|
|
164
|
+
- **旧版迁移** —— 同一段 todo block 早期通过 `~/.dsh/AGENTS.md` 下发。启动时 TUI 一次性把它剥掉(无标记时 no-op),避免重复下发。
|
|
165
|
+
- **空 / 读不到 = 不挂载该 section** —— 文件缺失或读不了时该 section 被静默丢弃,无报错、不影响 TUI 启动。
|
|
166
|
+
|
|
167
|
+
#### 作用范围:仅限主代理
|
|
168
|
+
|
|
169
|
+
该 section 注册在主代理**带作用域**的 agent context 上(`src/session.ts` 里的 `installAppendSystem`)—— 落在该 agent 自己的 prompt-scope 层,子代理的 scope 不会合并。协调者身份(「调度子代理、不要自己执行」)如果下发到子代理会自废武功,所以子代理完全看不到这个文件。机制与 `dsh-subagent-registry` 给每个子代理设置人设时相同。
|
|
170
|
+
|
|
171
|
+
#### 示例
|
|
172
|
+
|
|
173
|
+
```sh
|
|
174
|
+
# 首次启动从 templates/APPEND_SYSTEM.md 自动播种 —— 直接打开编辑即可。
|
|
175
|
+
$EDITOR ~/.dsh/APPEND_SYSTEM.md
|
|
176
|
+
|
|
177
|
+
# 或者完全替换为自己的版本(TUI 仍会保留它的标记 todo-lifecycle section,
|
|
178
|
+
# 缺失时会重新追加)。
|
|
179
|
+
cat > ~/.dsh/APPEND_SYSTEM.md <<'EOF'
|
|
180
|
+
# 项目约定
|
|
181
|
+
|
|
182
|
+
- 任何任务都先跑 `pnpm test` 再声称完成。
|
|
183
|
+
- 多步调研优先派发给 `workhorse` 子代理。
|
|
184
|
+
EOF
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
该功能没有开关斜杠命令 —— 它始终启用,完全由文件内容控制。
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
157
191
|
## 斜杠命令
|
|
158
192
|
|
|
159
193
|
| 命令 | 功能 |
|
package/cordis.patch.yml
CHANGED
|
@@ -8,7 +8,15 @@
|
|
|
8
8
|
# its own bundle patch (@aiwayds/dsh-dcp/cordis.patch.yml). Adding dsh-dcp to
|
|
9
9
|
# the profile's bundles mounts it; mounting it here too would duplicate the
|
|
10
10
|
# entry id and crash the loader.
|
|
11
|
+
#
|
|
12
|
+
# dsh-tool-ask-user is mounted here: the upstream tool is a soft-linked
|
|
13
|
+
# closure dependency (not a regular npm dep) and needs an explicit entry in
|
|
14
|
+
# the bundle to be wired into the model-facing tool catalog. Without this
|
|
15
|
+
# insert, the `ask_user_question` tool never registers with the model and
|
|
16
|
+
# the user-questions seam has no visible surface in this profile.
|
|
11
17
|
|
|
12
18
|
- insert:
|
|
13
19
|
- id: tui-pi
|
|
14
20
|
name: '@aiwayds/dsh-tui-pi'
|
|
21
|
+
- id: tool-ask-user
|
|
22
|
+
name: '@deepseek-ai/dsh-tool-ask-user'
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ask the human a question while the model is mid-turn.
|
|
3
|
+
*
|
|
4
|
+
* Wires the upstream `ctx.userQuestions` capability seam (`dsh-user-questions`)
|
|
5
|
+
* with a terminal-side UI: an in-place multi-question overlay that pauses the
|
|
6
|
+
* tool call until the human answers, then feeds the canonical
|
|
7
|
+
* `AskUserQuestionAnswer` envelope back to `dsh-tool-ask-user` as a normal
|
|
8
|
+
* tool result.
|
|
9
|
+
*
|
|
10
|
+
* Layout — one framed overlay, all questions flattened:
|
|
11
|
+
*
|
|
12
|
+
* ● Question 1
|
|
13
|
+
* Where should we deploy? (left = header / right = question text)
|
|
14
|
+
* ▸ staging
|
|
15
|
+
* production
|
|
16
|
+
* Type something. (sentinel row — inline input)
|
|
17
|
+
* Question 2 ··· (continues vertically)
|
|
18
|
+
* ▸ …
|
|
19
|
+
* ⏎ Confirm answers (when ≥ 2 questions, or any multiSelect question)
|
|
20
|
+
*
|
|
21
|
+
* Single-question single-select overlay: Enter on an option (or committing a
|
|
22
|
+
* filled sentinel) submits immediately. A multiSelect question — even a lone
|
|
23
|
+
* one — gets a Confirm row instead of auto-submitting, so the user can pick
|
|
24
|
+
* several options first. Multi-question overlay: Enter on the Confirm row
|
|
25
|
+
* hops to the review page (all answers listed, each editable in place).
|
|
26
|
+
* Esc double-press within 200 ms declines — but terminal key auto-repeat
|
|
27
|
+
* (holding Esc) is ignored below `ESC_REPEAT_GUARD_MS`, so a long press
|
|
28
|
+
* cannot accidentally fire the decline. The provider returns the "declined"
|
|
29
|
+
* envelope and the model reads it as a normal user reply. An aborted
|
|
30
|
+
* request signal (`request.signal`) settles declined too — we resolve the
|
|
31
|
+
* declined envelope instead of rejecting ASK_ABORTED because the upstream
|
|
32
|
+
* service already screens entry-time aborts and an aborted step discards
|
|
33
|
+
* the result anyway.
|
|
34
|
+
*
|
|
35
|
+
* Pure logic lives in the top of this file (initial state, answer envelope,
|
|
36
|
+
* declined envelope, double-Esc state machine, row-layout math) so it can
|
|
37
|
+
* be unit-tested without a TTY. The component below owns the TUI render +
|
|
38
|
+
* keyboard handling; the install function at the bottom registers the
|
|
39
|
+
* provider under `ctx.userQuestions` (a Cordis effect, single active
|
|
40
|
+
* provider in the tree).
|
|
41
|
+
*
|
|
42
|
+
* Inspired by juicesharp/rpiv-ask-user-question
|
|
43
|
+
* (https://github.com/juicesharp/rpiv-ask-user-question).
|
|
44
|
+
*/
|
|
45
|
+
import { type TUI } from '@earendil-works/pi-tui';
|
|
46
|
+
import type { AskUserQuestionAnswer, AskUserQuestionItem, AskUserQuestionOption } from '@deepseek-ai/dsh-user-questions';
|
|
47
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
48
|
+
import { type TuiTheme } from './theme/index.ts';
|
|
49
|
+
/** Maximum time between two Esc presses before the second one re-arms instead of firing. */
|
|
50
|
+
export declare const DOUBLE_ESC_WINDOW_MS = 200;
|
|
51
|
+
/**
|
|
52
|
+
* Minimum gap between two Esc presses below which the second one is treated as
|
|
53
|
+
* terminal key auto-repeat (the user is HOLDING Esc) rather than a deliberate
|
|
54
|
+
* press: repeats leave the state untouched, so a long press can never fire the
|
|
55
|
+
* decline gesture on its own.
|
|
56
|
+
*/
|
|
57
|
+
export declare const ESC_REPEAT_GUARD_MS = 50;
|
|
58
|
+
/** Sentinel row label appended to every question's option list. */
|
|
59
|
+
export declare const SENTINEL_LABEL = "Type something.";
|
|
60
|
+
/** Decline message embedded into the answer envelope when the user bails. */
|
|
61
|
+
export declare const DECLINE_MESSAGE = "User declined to answer questions.";
|
|
62
|
+
/** Transient hint when Enter lands on an incomplete confirm/submit row. */
|
|
63
|
+
export declare const INCOMPLETE_HINT = "Answer every question first";
|
|
64
|
+
/** A row in the multi-question view. */
|
|
65
|
+
export interface FlatRow {
|
|
66
|
+
kind: 'question-header' | 'option' | 'sentinel' | 'confirm';
|
|
67
|
+
questionIndex: number;
|
|
68
|
+
/** optionIndex for `option` rows, undefined for everything else. */
|
|
69
|
+
optionIndex?: number;
|
|
70
|
+
label: string;
|
|
71
|
+
description?: string;
|
|
72
|
+
/** Upstream `detail` (supporting text rendered under the header, never in labels). */
|
|
73
|
+
detail?: string;
|
|
74
|
+
selectable: boolean;
|
|
75
|
+
}
|
|
76
|
+
/** Pending answer for a single question. */
|
|
77
|
+
export interface PendingAnswer {
|
|
78
|
+
/** Selected option labels in display order. Empty when the sentinel/custom path owns this question. */
|
|
79
|
+
selected: readonly string[];
|
|
80
|
+
/** Free-text answer from the sentinel path. Optional — set only when the user typed. */
|
|
81
|
+
custom?: string;
|
|
82
|
+
}
|
|
83
|
+
/** Whole-state of one AskUser overlay. All setters are pure reducers. */
|
|
84
|
+
export interface AskUserState {
|
|
85
|
+
questions: readonly AskUserQuestionItem[];
|
|
86
|
+
/** Per-question pending answer. */
|
|
87
|
+
perQuestion: PendingAnswer[];
|
|
88
|
+
/** Cursor position in the flat row list (see `buildRowList`). */
|
|
89
|
+
cursorIndex: number;
|
|
90
|
+
/** Live inline-edit text per question; non-null when the sentinel is engaged for that question. */
|
|
91
|
+
customInputs: (string | null)[];
|
|
92
|
+
/** Live inline edit owner (question index); null when not editing. */
|
|
93
|
+
customEditingFor: number | null;
|
|
94
|
+
/** Phase: multi-question uses a review step before submission. */
|
|
95
|
+
phase: 'questions' | 'review';
|
|
96
|
+
/** Cursor over the review rows. Q rows + 1 submit row. */
|
|
97
|
+
reviewIndex: number;
|
|
98
|
+
/** Last Esc timestamp for the double-Esc guard. */
|
|
99
|
+
lastEscAt: number | null;
|
|
100
|
+
/** The status hint shown above the footer after one Esc press. */
|
|
101
|
+
cancelHint: boolean;
|
|
102
|
+
/** Transient "you can't do that yet" hint (e.g. Enter on an incomplete confirm row). Cleared by any navigation. */
|
|
103
|
+
attentionHint: string | null;
|
|
104
|
+
}
|
|
105
|
+
/** Initial state for a `questions` payload — defaults: cursor on Q0/option-0, no edit, no Esc history. */
|
|
106
|
+
export declare function initialState(questions: readonly AskUserQuestionItem[]): AskUserState;
|
|
107
|
+
/** Toggle/add an option. Single-select replaces; multi-select toggles membership. */
|
|
108
|
+
export declare function toggleOption(state: AskUserState, questionIndex: number, optionLabel: string): AskUserState;
|
|
109
|
+
/** Set the custom answer for a question (clears selected options). */
|
|
110
|
+
export declare function setCustomAnswer(state: AskUserState, questionIndex: number, text: string): AskUserState;
|
|
111
|
+
/** Mutate the live custom input buffer for a question. */
|
|
112
|
+
export declare function patchCustomInput(state: AskUserState, questionIndex: number, mutator: (current: string) => string): AskUserState;
|
|
113
|
+
/** Enter inline-edit mode for a question (sentinel row got pressed). */
|
|
114
|
+
export declare function enterCustomEdit(state: AskUserState, questionIndex: number): AskUserState;
|
|
115
|
+
/** Leave inline-edit mode; commit when `commit === true` and the buffer is non-empty. */
|
|
116
|
+
export declare function exitCustomEdit(state: AskUserState, commit: boolean): AskUserState;
|
|
117
|
+
/**
|
|
118
|
+
* Double-Esc state machine: 1st press arms; 2nd within the window fires
|
|
119
|
+
* (caller reads `lastEscAt===null + cancelHint===false`). Presses closer than
|
|
120
|
+
* `repeatGuardMs` are terminal key auto-repeat (held key) and are ignored
|
|
121
|
+
* entirely — the armed state stays at its original timestamp, so holding Esc
|
|
122
|
+
* neither fires the decline nor refreshes the window.
|
|
123
|
+
*/
|
|
124
|
+
export declare function advanceDoubleEsc(state: AskUserState, now: number, windowMs?: number, repeatGuardMs?: number): AskUserState;
|
|
125
|
+
/** Was the most recent double-Esc press a "fired" event (the second within the window)? */
|
|
126
|
+
export declare function didDoubleEscFire(prevState: AskUserState, nextState: AskUserState, now: number, windowMs?: number): boolean;
|
|
127
|
+
/** Canonical envelope for a normal submission. The model reads it as a tool result. */
|
|
128
|
+
export declare function buildAnswerEnvelope(state: AskUserState): AskUserQuestionAnswer;
|
|
129
|
+
/** Canonical envelope for the decline path: empty selected + custom decline message on every question. */
|
|
130
|
+
export declare function buildDeclinedEnvelope(questions: readonly AskUserQuestionItem[], message?: string): AskUserQuestionAnswer;
|
|
131
|
+
/** Build the ordered, flat list of rows for the questions pane. */
|
|
132
|
+
export declare function buildRowList(questions: readonly AskUserQuestionItem[], perQuestion: readonly PendingAnswer[]): FlatRow[];
|
|
133
|
+
/** Find the next selectable index from `i` going in `direction`, clamped into range. */
|
|
134
|
+
export declare function nextSelectableIndex(rows: readonly FlatRow[], from: number, direction: 1 | -1): number;
|
|
135
|
+
/** True when every question has at least one selected option or a non-empty custom answer. */
|
|
136
|
+
export declare function allQuestionsAnswered(state: AskUserState): boolean;
|
|
137
|
+
/**
|
|
138
|
+
* Whether the overlay needs an explicit `⏎ Confirm answers` row: any
|
|
139
|
+
* multi-question layout, plus a lone multiSelect question — multiSelect can
|
|
140
|
+
* never auto-submit (the user may want more toggles), so it needs a path to
|
|
141
|
+
* the review page.
|
|
142
|
+
*/
|
|
143
|
+
export declare function needsConfirmRow(questions: readonly AskUserQuestionItem[]): boolean;
|
|
144
|
+
/**
|
|
145
|
+
* True when a successful action on THIS state can submit immediately without
|
|
146
|
+
* the review step: exactly one question, single-select (a multiSelect
|
|
147
|
+
* question may want further toggles), and every question answered.
|
|
148
|
+
*/
|
|
149
|
+
export declare function canAutoSubmit(state: AskUserState): boolean;
|
|
150
|
+
/**
|
|
151
|
+
* Row index to land on after answering question `answeredQi`: the first
|
|
152
|
+
* selectable row of the nearest LATER unanswered question, or -1 when every
|
|
153
|
+
* later question is already answered (caller keeps the current cursor).
|
|
154
|
+
*/
|
|
155
|
+
export declare function nextUnansweredRow(rows: readonly FlatRow[], perQuestion: readonly PendingAnswer[], answeredQi: number): number;
|
|
156
|
+
/** Render the questions pane as a flat table line list. */
|
|
157
|
+
export declare function renderQuestionsView(theme: TuiTheme, state: AskUserState, width: number): string[];
|
|
158
|
+
/** Render the review page for multi-question overlays. */
|
|
159
|
+
export declare function renderReviewView(theme: TuiTheme, state: AskUserState, width: number): string[];
|
|
160
|
+
/** Options for assembling the panel + provider function. */
|
|
161
|
+
export interface AskUserPanelDeps {
|
|
162
|
+
tui: TUI;
|
|
163
|
+
/** Live theme getter — re-read on every render so a mid-overlay hot-swap applies (frame included). */
|
|
164
|
+
theme: () => TuiTheme;
|
|
165
|
+
/** Re-focus the current editor on overlay close. */
|
|
166
|
+
restoreFocus: () => void;
|
|
167
|
+
/** Injectable clock for tests; defaults to Date.now. */
|
|
168
|
+
now?: () => number;
|
|
169
|
+
/** Width and height of the framed overlay. */
|
|
170
|
+
width?: `${number}%` | number;
|
|
171
|
+
maxHeight?: `${number}%` | number;
|
|
172
|
+
}
|
|
173
|
+
/** Result promise from `openAskUserPanel`. Declined carries the canonical decline envelope. */
|
|
174
|
+
export type AskUserResult = AskUserQuestionAnswer;
|
|
175
|
+
/**
|
|
176
|
+
* Open the AskUser overlay for one set of questions.
|
|
177
|
+
*
|
|
178
|
+
* `signal` is the caller's abort signal (the tool execution's). Already-aborted
|
|
179
|
+
* settles declined WITHOUT staging an overlay; a live signal closes the
|
|
180
|
+
* overlay and settles declined when it fires. We resolve the declined envelope
|
|
181
|
+
* rather than rejecting with the upstream ASK_ABORTED code on purpose: the
|
|
182
|
+
* upstream service already screens entry-time aborts, and a step aborted after
|
|
183
|
+
* this point discards the tool result anyway — resolving keeps the pending
|
|
184
|
+
* promise from ever hanging either way.
|
|
185
|
+
*/
|
|
186
|
+
export declare function openAskUserPanel(deps: AskUserPanelDeps, questions: readonly AskUserQuestionItem[], signal?: AbortSignal): Promise<AskUserResult>;
|
|
187
|
+
/**
|
|
188
|
+
* True only for the upstream's documented duplicate-registration failure
|
|
189
|
+
* (`UserQuestionError` with code `DUPLICATE_PROVIDER`) — the one case where
|
|
190
|
+
* yielding the single provider slot to the prior UI is correct. Matched
|
|
191
|
+
* structurally on `name` + `code` so a cross-realm HarnessError instance or a
|
|
192
|
+
* test double classifies identically.
|
|
193
|
+
*/
|
|
194
|
+
export declare function isDuplicateProviderError(error: unknown): boolean;
|
|
195
|
+
/**
|
|
196
|
+
* Wires the provider into `ctx.userQuestions`. Call from inside `ctx.effect`.
|
|
197
|
+
*
|
|
198
|
+
* Failure semantics are deliberate (review round BM):
|
|
199
|
+
* - missing service → warn + no-op disposer. The tool stays mounted by the
|
|
200
|
+
* bundle patch; without a provider its calls fail with the upstream
|
|
201
|
+
* NO_PROVIDER error, which is better than crashing the whole TUI plugin.
|
|
202
|
+
* - DUPLICATE_PROVIDER → silent no-op disposer (a prior UI owns the slot).
|
|
203
|
+
* - anything else → rethrown so the effect fails loudly instead of leaving a
|
|
204
|
+
* mounted tool with no UI and no trace.
|
|
205
|
+
*/
|
|
206
|
+
export declare function registerAskUserProvider(ctx: Context, deps: AskUserPanelDeps): () => void;
|
|
207
|
+
export type { AskUserQuestionOption };
|