@hheei/pi-ask-user 0.1.0 → 0.2.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 +2 -8
- package/dist/ask-prompt.d.ts +3 -15
- package/dist/ask-prompt.js +6 -132
- package/dist/batch-prompt.d.ts +1 -3
- package/dist/batch-prompt.js +5 -9
- package/dist/constants.d.ts +2 -6
- package/dist/constants.js +2 -6
- package/dist/custom-prompt.d.ts +0 -4
- package/dist/custom-prompt.js +2 -25
- package/dist/execute.js +6 -12
- package/dist/index.js +0 -2
- package/dist/prompt-help.d.ts +0 -4
- package/dist/prompt-help.js +0 -1
- package/dist/prompt-layout.d.ts +2 -40
- package/dist/prompt-layout.js +5 -81
- package/dist/schema.d.ts +1 -4
- package/dist/schema.js +0 -2
- package/dist/shortcuts.d.ts +0 -2
- package/dist/shortcuts.js +0 -8
- package/dist/tool.js +0 -5
- package/package.json +56 -59
- package/skills/ask-user/SKILL.md +0 -1
- package/skills/ask-user/references/ask-user-skill-extension-spec.md +0 -13
- package/dist/preferences.d.ts +0 -8
- package/dist/preferences.js +0 -25
- package/dist/prompt-overflow.d.ts +0 -5
- package/dist/prompt-overflow.js +0 -59
- package/dist/settings-provider.d.ts +0 -15
- package/dist/settings-provider.js +0 -42
package/README.md
CHANGED
|
@@ -10,10 +10,9 @@ Forked from [`edlsh/pi-ask-user`](https://github.com/edlsh/pi-ask-user) at revis
|
|
|
10
10
|
- Uses the `typebox` package instead of `@sinclair/typebox`.
|
|
11
11
|
- Tests run on vitest instead of `bun test`.
|
|
12
12
|
- Code conforms to this repo's Biome and `exactOptionalPropertyTypes` rules.
|
|
13
|
-
-
|
|
13
|
+
- Renders only inline: upstream's centered overlay mode, its `displayMode` parameter, and the whole `askUser` settings section are gone, so the prompt always sits in pi's input dock and reports `blocked` to the terminal through the host.
|
|
14
14
|
- Drops upstream's searchable split-pane list, optional comment stage, overlay hide shortcut, review-page scrolling, and `ctrl+j`/`ctrl+k` vim aliases: the prompt now matches pi's own select-list keys.
|
|
15
15
|
- Drops the upstream `pi-tern` native-presentation integration. Prompts render through pi's own TUI components, and results publish presentation facts that `@hheei/pi-ext-ui` frames with its 0/1/2 fold levels when it is installed.
|
|
16
|
-
- Registers an `ask_user` settings page through `@hheei/pi-ext-core`, so the preferences below are editable in the host settings UI instead of by hand.
|
|
17
16
|
|
|
18
17
|
Module boundaries, invariants, and the test layout are recorded in
|
|
19
18
|
[`docs/architecture/ask-user.md`](../../docs/architecture/ask-user.md).
|
|
@@ -36,7 +35,6 @@ pi install npm:@hheei/pi-ask-user
|
|
|
36
35
|
| `options` | `{title, description?}[]?` | — | Choices offered to the user |
|
|
37
36
|
| `allowMultiple` | `boolean?` | `false` | Allow selecting several options |
|
|
38
37
|
| `allowFreeform` | `boolean?` | `true` | Add a freeform answer row |
|
|
39
|
-
| `displayMode` | `"overlay" \| "inline"` | `overlay` | Centered modal or in-place prompt |
|
|
40
38
|
| `timeout_ms` | `integer?` | — | Auto-dismiss after N milliseconds; the result is cancelled |
|
|
41
39
|
|
|
42
40
|
## Keys
|
|
@@ -47,14 +45,10 @@ pi install npm:@hheei/pi-ask-user
|
|
|
47
45
|
| Single select | `enter` choose (on the freeform row: open the editor) |
|
|
48
46
|
| Multi select | `space` tick, `enter` submit |
|
|
49
47
|
| Editor | `enter` submit, `shift+enter`/`ctrl+j` newline, `esc` back or cancel |
|
|
50
|
-
| Prompt | `ctrl+e` expand
|
|
48
|
+
| Prompt | `ctrl+e` expand or collapse context |
|
|
51
49
|
| Batch | `tab`/`shift+tab` pages, `1`-`N` jump from review, `enter` submit, `esc` cancel |
|
|
52
50
|
| Anywhere | `esc`/`ctrl+c` cancel |
|
|
53
51
|
|
|
54
|
-
## Preferences
|
|
55
|
-
|
|
56
|
-
One setting, editable from the host settings UI or by hand: `displayMode` in the `askUser` section of `<agent-dir>/ext_settings.json` (project `.pi/ext_settings.json` wins), read on every call. A call parameter overrides it.
|
|
57
|
-
|
|
58
52
|
`ask:answered` and `ask:cancelled` events carry only the question and the response kind; context and the actual answer stay in the tool result.
|
|
59
53
|
|
|
60
54
|
## Skill
|
package/dist/ask-prompt.d.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { Container, type KeybindingsManager, type TUI } from "@earendil-works/pi-tui";
|
|
3
|
-
import type { PromptSettings } from "./preferences";
|
|
4
3
|
import { type AskUIResult } from "./response";
|
|
5
4
|
import type { BatchQuestion } from "./schema";
|
|
6
5
|
/**
|
|
@@ -9,7 +8,6 @@ import type { BatchQuestion } from "./schema";
|
|
|
9
8
|
*/
|
|
10
9
|
export declare class AskComponent extends Container {
|
|
11
10
|
private readonly prompt;
|
|
12
|
-
private readonly settings;
|
|
13
11
|
private readonly contextPane;
|
|
14
12
|
private readonly tui;
|
|
15
13
|
private readonly theme;
|
|
@@ -18,9 +16,6 @@ export declare class AskComponent extends Container {
|
|
|
18
16
|
private mode;
|
|
19
17
|
private chrome;
|
|
20
18
|
private freeformDraft;
|
|
21
|
-
private promptScrollOffset;
|
|
22
|
-
private promptMaxScrollOffset;
|
|
23
|
-
private promptViewportRows;
|
|
24
19
|
private get contextIsCollapsible();
|
|
25
20
|
private get contextExpanded();
|
|
26
21
|
private questionText;
|
|
@@ -30,21 +25,17 @@ export declare class AskComponent extends Container {
|
|
|
30
25
|
private _focused;
|
|
31
26
|
get focused(): boolean;
|
|
32
27
|
set focused(value: boolean);
|
|
33
|
-
constructor(prompt: BatchQuestion,
|
|
28
|
+
constructor(prompt: BatchQuestion, tui: TUI, theme: Theme, keybindings: KeybindingsManager, onDone: (result: AskUIResult | null) => void);
|
|
34
29
|
invalidate(): void;
|
|
35
30
|
render(width: number): string[];
|
|
36
31
|
private renderInlineLayout;
|
|
37
|
-
private renderOverlayLayout;
|
|
38
32
|
private buildPromptLines;
|
|
39
|
-
|
|
40
|
-
private renderEditorModeLines;
|
|
41
|
-
/** Header shared by the container children (inline) and the overlay budget path. */
|
|
33
|
+
/** Header shown above the freeform editor. */
|
|
42
34
|
private editorHeader;
|
|
43
|
-
private renderPromptPane;
|
|
44
35
|
private updateStaticText;
|
|
45
36
|
/** Batch pages: show the question strip and the page-navigation hint above the question. */
|
|
46
37
|
setBatchChrome(frameTitle: string, navigationHint: string): void;
|
|
47
|
-
/** The hint row for the current mode
|
|
38
|
+
/** The hint row for the current mode. */
|
|
48
39
|
private helpLines;
|
|
49
40
|
/** The select-mode body; both lists share the wiring and differ in the selection shape. */
|
|
50
41
|
private get optionList();
|
|
@@ -53,8 +44,5 @@ export declare class AskComponent extends Container {
|
|
|
53
44
|
private showSelectMode;
|
|
54
45
|
private showEditorMode;
|
|
55
46
|
private toggleContext;
|
|
56
|
-
private setPromptScrollOffset;
|
|
57
|
-
private handlePromptScrollInput;
|
|
58
|
-
private scrollPromptBy;
|
|
59
47
|
handleInput(data: string): void;
|
|
60
48
|
}
|
package/dist/ask-prompt.js
CHANGED
|
@@ -3,10 +3,8 @@ import { INLINE_CONTEXT_MAX_ROWS } from "./constants";
|
|
|
3
3
|
import { MultiSelectList } from "./multi-select-list";
|
|
4
4
|
import { PromptContextPane } from "./prompt-context";
|
|
5
5
|
import { buildHelpText } from "./prompt-help";
|
|
6
|
-
import {
|
|
7
|
-
import { applyOverflowMarkers, limitEditorLines, limitLines } from "./prompt-overflow";
|
|
6
|
+
import { railRow } from "./prompt-layout";
|
|
8
7
|
import { createFreeformResponse, createSelectionResponse } from "./response";
|
|
9
|
-
import { pageDelta } from "./shortcuts";
|
|
10
8
|
import { WrappedSingleSelectList } from "./single-select-list";
|
|
11
9
|
function editorTheme(theme) {
|
|
12
10
|
return {
|
|
@@ -26,7 +24,6 @@ function editorTheme(theme) {
|
|
|
26
24
|
*/
|
|
27
25
|
export class AskComponent extends Container {
|
|
28
26
|
prompt;
|
|
29
|
-
settings;
|
|
30
27
|
contextPane;
|
|
31
28
|
tui;
|
|
32
29
|
theme;
|
|
@@ -35,9 +32,6 @@ export class AskComponent extends Container {
|
|
|
35
32
|
mode = "select";
|
|
36
33
|
chrome = "";
|
|
37
34
|
freeformDraft = "";
|
|
38
|
-
promptScrollOffset = 0;
|
|
39
|
-
promptMaxScrollOffset = 0;
|
|
40
|
-
promptViewportRows = 0;
|
|
41
35
|
get contextIsCollapsible() {
|
|
42
36
|
return this.contextPane.collapsible;
|
|
43
37
|
}
|
|
@@ -61,10 +55,9 @@ export class AskComponent extends Container {
|
|
|
61
55
|
this.editor.focused = value;
|
|
62
56
|
}
|
|
63
57
|
}
|
|
64
|
-
constructor(prompt,
|
|
58
|
+
constructor(prompt, tui, theme, keybindings, onDone) {
|
|
65
59
|
super();
|
|
66
60
|
this.prompt = prompt;
|
|
67
|
-
this.settings = settings;
|
|
68
61
|
this.tui = tui;
|
|
69
62
|
this.theme = theme;
|
|
70
63
|
this.keybindings = keybindings;
|
|
@@ -94,8 +87,6 @@ export class AskComponent extends Container {
|
|
|
94
87
|
this.updateStaticText();
|
|
95
88
|
}
|
|
96
89
|
render(width) {
|
|
97
|
-
if (this.settings.displayMode === "overlay")
|
|
98
|
-
return this.renderOverlayLayout(width);
|
|
99
90
|
if (this.mode === "select")
|
|
100
91
|
this.optionList.setMaxVisibleRows(12);
|
|
101
92
|
return this.renderInlineLayout(width);
|
|
@@ -109,51 +100,9 @@ export class AskComponent extends Container {
|
|
|
109
100
|
"",
|
|
110
101
|
...this.modeContainer.render(width),
|
|
111
102
|
railRow(this.theme, width),
|
|
112
|
-
...this.helpLines(width
|
|
103
|
+
...this.helpLines(width),
|
|
113
104
|
];
|
|
114
105
|
}
|
|
115
|
-
renderOverlayLayout(width) {
|
|
116
|
-
const maxLines = getOverlayMaxRenderLinesForRows(this.tui.terminal.rows);
|
|
117
|
-
if (maxLines <= 0)
|
|
118
|
-
return [];
|
|
119
|
-
const bodyCapacity = Math.max(0, maxLines - RAIL_ROWS);
|
|
120
|
-
let helpLines = this.helpLines(width, false);
|
|
121
|
-
const questionLines = this.questionText.render(width);
|
|
122
|
-
const fullContextLines = this.contextPane.fullLines(width);
|
|
123
|
-
this.contextPane.setCollapsible(contextShouldCollapse({
|
|
124
|
-
displayMode: this.settings.displayMode,
|
|
125
|
-
mode: this.mode,
|
|
126
|
-
contextCollapsible: this.contextIsCollapsible,
|
|
127
|
-
questionRows: questionLines.length,
|
|
128
|
-
contextRows: fullContextLines.length,
|
|
129
|
-
bodyCapacity,
|
|
130
|
-
helpRows: helpLines.length,
|
|
131
|
-
}));
|
|
132
|
-
const promptLines = this.buildPromptLines(width, fullContextLines);
|
|
133
|
-
const plan = planOverlayBody({
|
|
134
|
-
maxRenderLines: bodyCapacity,
|
|
135
|
-
mode: this.mode,
|
|
136
|
-
contextCollapsible: this.contextIsCollapsible,
|
|
137
|
-
promptRows: promptLines.length,
|
|
138
|
-
helpRows: helpLines.length,
|
|
139
|
-
});
|
|
140
|
-
const modeLines = this.renderModeLines(width, plan.modeBudget);
|
|
141
|
-
const promptBudget = plan.promptBudget + Math.max(0, plan.modeBudget - modeLines.length);
|
|
142
|
-
const promptPaneLines = this.renderPromptPane(promptLines, promptBudget, width);
|
|
143
|
-
helpLines = this.helpLines(width, promptLines.length > promptBudget);
|
|
144
|
-
const visibleHelpLines = limitLines(this.theme, helpLines, plan.helpBudget, width, false);
|
|
145
|
-
const contentLines = [
|
|
146
|
-
...promptPaneLines,
|
|
147
|
-
...(plan.separatorRows > 0 && promptPaneLines.length > 0 && modeLines.length > 0 ? [""] : []),
|
|
148
|
-
...modeLines,
|
|
149
|
-
];
|
|
150
|
-
return [
|
|
151
|
-
railRow(this.theme, width),
|
|
152
|
-
...contentLines.slice(0, plan.bodyCapacity),
|
|
153
|
-
railRow(this.theme, width),
|
|
154
|
-
...visibleHelpLines,
|
|
155
|
-
].slice(0, maxLines);
|
|
156
|
-
}
|
|
157
106
|
buildPromptLines(width, fullContextLines) {
|
|
158
107
|
const questionLines = this.questionText.render(width);
|
|
159
108
|
const contextLines = this.contextPane.displayLines(fullContextLines, width);
|
|
@@ -163,46 +112,10 @@ export class AskComponent extends Container {
|
|
|
163
112
|
...(contextLines.length > 0 ? [...contextSeparator, ...contextLines] : []),
|
|
164
113
|
];
|
|
165
114
|
}
|
|
166
|
-
|
|
167
|
-
const safeBudget = Math.max(0, Math.floor(budget));
|
|
168
|
-
if (safeBudget <= 0)
|
|
169
|
-
return [];
|
|
170
|
-
if (this.mode === "select") {
|
|
171
|
-
this.optionList.setMaxVisibleRows(Math.max(1, safeBudget));
|
|
172
|
-
return limitLines(this.theme, this.modeContainer.render(width), safeBudget, width, true);
|
|
173
|
-
}
|
|
174
|
-
return this.renderEditorModeLines(width, safeBudget);
|
|
175
|
-
}
|
|
176
|
-
renderEditorModeLines(width, budget) {
|
|
177
|
-
const headerLines = [...new Text(this.editorHeader(), 1, 0).render(width), ""];
|
|
178
|
-
const minimumEditorRows = Math.min(3, budget);
|
|
179
|
-
const headerBudget = Math.max(0, budget - minimumEditorRows);
|
|
180
|
-
const visibleHeaderLines = limitLines(this.theme, headerLines, headerBudget, width, true);
|
|
181
|
-
const editorBudget = Math.max(0, budget - visibleHeaderLines.length);
|
|
182
|
-
return [
|
|
183
|
-
...visibleHeaderLines,
|
|
184
|
-
...limitEditorLines(this.theme, this.ensureEditor().render(width), editorBudget, width),
|
|
185
|
-
];
|
|
186
|
-
}
|
|
187
|
-
/** Header shared by the container children (inline) and the overlay budget path. */
|
|
115
|
+
/** Header shown above the freeform editor. */
|
|
188
116
|
editorHeader() {
|
|
189
117
|
return this.theme.fg("accent", this.theme.bold("Custom response"));
|
|
190
118
|
}
|
|
191
|
-
renderPromptPane(promptLines, budget, width) {
|
|
192
|
-
const viewportRows = Math.max(0, Math.floor(budget));
|
|
193
|
-
this.promptViewportRows = viewportRows;
|
|
194
|
-
if (viewportRows <= 0 || promptLines.length === 0) {
|
|
195
|
-
this.promptMaxScrollOffset = 0;
|
|
196
|
-
this.promptScrollOffset = 0;
|
|
197
|
-
return [];
|
|
198
|
-
}
|
|
199
|
-
this.promptMaxScrollOffset = Math.max(0, promptLines.length - viewportRows);
|
|
200
|
-
this.promptScrollOffset = Math.max(0, Math.min(this.promptScrollOffset, this.promptMaxScrollOffset));
|
|
201
|
-
const visibleLines = promptLines.slice(this.promptScrollOffset, this.promptScrollOffset + viewportRows);
|
|
202
|
-
const hasHiddenAbove = this.promptScrollOffset > 0;
|
|
203
|
-
const hasHiddenBelow = this.promptScrollOffset + viewportRows < promptLines.length;
|
|
204
|
-
return applyOverflowMarkers(this.theme, visibleLines, width, hasHiddenAbove, hasHiddenBelow);
|
|
205
|
-
}
|
|
206
119
|
updateStaticText() {
|
|
207
120
|
const theme = this.theme;
|
|
208
121
|
const question = theme.fg("text", theme.bold(this.prompt.question));
|
|
@@ -215,17 +128,15 @@ export class AskComponent extends Container {
|
|
|
215
128
|
this.chrome = `${theme.fg("dim", frameTitle)} ${theme.fg("dim", navigationHint)}`;
|
|
216
129
|
this.updateStaticText();
|
|
217
130
|
}
|
|
218
|
-
/** The hint row for the current mode
|
|
219
|
-
helpLines(width
|
|
131
|
+
/** The hint row for the current mode. */
|
|
132
|
+
helpLines(width) {
|
|
220
133
|
return wrapTextWithAnsi(buildHelpText({
|
|
221
134
|
theme: this.theme,
|
|
222
135
|
mode: this.mode,
|
|
223
|
-
displayMode: this.settings.displayMode,
|
|
224
136
|
allowMultiple: this.prompt.allowMultiple,
|
|
225
137
|
hasOptions: this.prompt.options.length > 0,
|
|
226
138
|
contextCollapsible: this.contextIsCollapsible,
|
|
227
139
|
contextToggleKey: this.contextPane.toggleKey(),
|
|
228
|
-
promptScrollable,
|
|
229
140
|
keybindings: this.keybindings,
|
|
230
141
|
}), width);
|
|
231
142
|
}
|
|
@@ -291,51 +202,14 @@ export class AskComponent extends Container {
|
|
|
291
202
|
return false;
|
|
292
203
|
if (!this.contextPane.toggle())
|
|
293
204
|
return false;
|
|
294
|
-
this.promptScrollOffset = 0;
|
|
295
205
|
this.invalidate();
|
|
296
206
|
this.tui.requestRender();
|
|
297
207
|
return true;
|
|
298
208
|
}
|
|
299
|
-
setPromptScrollOffset(nextOffset) {
|
|
300
|
-
this.promptScrollOffset = Math.max(0, Math.min(Math.floor(nextOffset), this.promptMaxScrollOffset));
|
|
301
|
-
}
|
|
302
|
-
handlePromptScrollInput(data) {
|
|
303
|
-
if (this.settings.displayMode !== "overlay")
|
|
304
|
-
return false;
|
|
305
|
-
if (this.promptMaxScrollOffset <= 0)
|
|
306
|
-
return false;
|
|
307
|
-
// Prompt scrolling is select-mode only: in freeform mode the editor owns
|
|
308
|
-
// PageUp/PageDown (tui.editor.pageUp/pageDown) for paging through long
|
|
309
|
-
// input, so intercepting them here would steal editor keys.
|
|
310
|
-
if (this.mode !== "select")
|
|
311
|
-
return false;
|
|
312
|
-
const pageRows = Math.max(1, this.promptViewportRows - 1);
|
|
313
|
-
const halfPageRows = Math.max(1, Math.floor(this.promptViewportRows / 2));
|
|
314
|
-
const delta = pageDelta(data, pageRows);
|
|
315
|
-
if (delta !== 0)
|
|
316
|
-
return this.scrollPromptBy(delta);
|
|
317
|
-
if (matchesKey(data, Key.home))
|
|
318
|
-
return this.scrollPromptBy(-this.promptScrollOffset);
|
|
319
|
-
if (matchesKey(data, Key.end))
|
|
320
|
-
return this.scrollPromptBy(this.promptMaxScrollOffset);
|
|
321
|
-
if (matchesKey(data, Key.ctrl("u")))
|
|
322
|
-
return this.scrollPromptBy(-halfPageRows);
|
|
323
|
-
if (matchesKey(data, Key.ctrl("d")))
|
|
324
|
-
return this.scrollPromptBy(halfPageRows);
|
|
325
|
-
return false;
|
|
326
|
-
}
|
|
327
|
-
scrollPromptBy(delta) {
|
|
328
|
-
this.setPromptScrollOffset(this.promptScrollOffset + delta);
|
|
329
|
-
return true;
|
|
330
|
-
}
|
|
331
209
|
handleInput(data) {
|
|
332
210
|
if (matchesKey(data, this.contextPane.toggleKey()) && this.toggleContext()) {
|
|
333
211
|
return;
|
|
334
212
|
}
|
|
335
|
-
if (this.handlePromptScrollInput(data)) {
|
|
336
|
-
this.tui.requestRender();
|
|
337
|
-
return;
|
|
338
|
-
}
|
|
339
213
|
if (this.mode === "freeform") {
|
|
340
214
|
if (matchesKey(data, Key.escape)) {
|
|
341
215
|
// Without options there is no list to go back to, so esc cancels.
|
package/dist/batch-prompt.d.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { type Component, type KeybindingsManager, type TUI } from "@earendil-works/pi-tui";
|
|
3
|
-
import type { PromptSettings } from "./preferences";
|
|
4
3
|
import { type BatchAnswer } from "./response";
|
|
5
4
|
import type { BatchQuestion } from "./schema";
|
|
6
5
|
/**
|
|
@@ -11,7 +10,6 @@ import type { BatchQuestion } from "./schema";
|
|
|
11
10
|
*/
|
|
12
11
|
export declare class BatchAskComponent implements Component {
|
|
13
12
|
private questions;
|
|
14
|
-
private settings;
|
|
15
13
|
private tui;
|
|
16
14
|
private theme;
|
|
17
15
|
private keybindings;
|
|
@@ -23,7 +21,7 @@ export declare class BatchAskComponent implements Component {
|
|
|
23
21
|
private title;
|
|
24
22
|
private confirmingSkips;
|
|
25
23
|
private _focused;
|
|
26
|
-
constructor(questions: BatchQuestion[],
|
|
24
|
+
constructor(questions: BatchQuestion[], tui: TUI, theme: Theme, keybindings: KeybindingsManager, onDone: (answers: BatchAnswer[] | null) => void, heading?: string | undefined);
|
|
27
25
|
get focused(): boolean;
|
|
28
26
|
set focused(value: boolean);
|
|
29
27
|
invalidate(): void;
|
package/dist/batch-prompt.js
CHANGED
|
@@ -2,7 +2,7 @@ import { decodeKittyPrintable, Key, matchesKey, wrapTextWithAnsi, } from "@earen
|
|
|
2
2
|
import { AskComponent } from "./ask-prompt";
|
|
3
3
|
import { INLINE_DOCK_RESERVED_ROWS } from "./constants";
|
|
4
4
|
import { formatKeyList } from "./prompt-help";
|
|
5
|
-
import {
|
|
5
|
+
import { getMaxRenderLinesForRows, railRow } from "./prompt-layout";
|
|
6
6
|
import { formatResponseSummary, } from "./response";
|
|
7
7
|
/**
|
|
8
8
|
* One prompt for a whole `questions` batch: a page per question plus a review
|
|
@@ -12,7 +12,6 @@ import { formatResponseSummary, } from "./response";
|
|
|
12
12
|
*/
|
|
13
13
|
export class BatchAskComponent {
|
|
14
14
|
questions;
|
|
15
|
-
settings;
|
|
16
15
|
tui;
|
|
17
16
|
theme;
|
|
18
17
|
keybindings;
|
|
@@ -24,16 +23,15 @@ export class BatchAskComponent {
|
|
|
24
23
|
title = "";
|
|
25
24
|
confirmingSkips = false;
|
|
26
25
|
_focused = false;
|
|
27
|
-
constructor(questions,
|
|
26
|
+
constructor(questions, tui, theme, keybindings, onDone, heading) {
|
|
28
27
|
this.questions = questions;
|
|
29
|
-
this.settings = settings;
|
|
30
28
|
this.tui = tui;
|
|
31
29
|
this.theme = theme;
|
|
32
30
|
this.keybindings = keybindings;
|
|
33
31
|
this.onDone = onDone;
|
|
34
32
|
this.heading = heading;
|
|
35
33
|
this.answers = questions.map(() => undefined);
|
|
36
|
-
this.pages = questions.map((entry, index) => new AskComponent(entry,
|
|
34
|
+
this.pages = questions.map((entry, index) => new AskComponent(entry, tui, theme, keybindings, (result) => this.handlePageDone(index, result)));
|
|
37
35
|
this.updateChrome();
|
|
38
36
|
}
|
|
39
37
|
get focused() {
|
|
@@ -180,13 +178,11 @@ export class BatchAskComponent {
|
|
|
180
178
|
/** Rows the review page may use, rails included. */
|
|
181
179
|
reviewLineCap() {
|
|
182
180
|
const rows = Number.isFinite(this.tui.terminal.rows) ? Math.floor(this.tui.terminal.rows) : 24;
|
|
183
|
-
const overlayCap = getOverlayMaxRenderLinesForRows(rows);
|
|
184
|
-
if (this.settings.displayMode === "overlay")
|
|
185
|
-
return overlayCap;
|
|
186
181
|
// Inline prompts sit in Pi's fullscreen input dock, which clips anything
|
|
187
182
|
// taller than what remains after the transcript's minimum row, the
|
|
188
183
|
// working status, and the 2-3 row footer. Components only learn their
|
|
189
184
|
// width, so stay within that space instead of growing with the content.
|
|
190
|
-
|
|
185
|
+
const cap = getMaxRenderLinesForRows(rows);
|
|
186
|
+
return Math.max(4, Math.min(cap, rows - INLINE_DOCK_RESERVED_ROWS));
|
|
191
187
|
}
|
|
192
188
|
}
|
package/dist/constants.d.ts
CHANGED
|
@@ -1,15 +1,11 @@
|
|
|
1
1
|
export declare const BATCH_MIN_QUESTIONS = 2;
|
|
2
2
|
export declare const BATCH_MAX_QUESTIONS = 4;
|
|
3
3
|
export type AskMode = "select" | "freeform";
|
|
4
|
-
export declare const
|
|
5
|
-
export declare const
|
|
6
|
-
export declare const ASK_OVERLAY_WIDTH = "92%";
|
|
7
|
-
export declare const ASK_OVERLAY_MIN_WIDTH = 40;
|
|
4
|
+
export declare const ASK_MAX_RENDER_LINES_RATIO = 0.85;
|
|
5
|
+
export declare const ASK_MIN_RENDER_LINES = 8;
|
|
8
6
|
export declare const FREEFORM_SENTINEL = "\u270F\uFE0F Type custom response...";
|
|
9
7
|
export declare const FREEFORM_LABEL = "Type something.";
|
|
10
8
|
export declare const FREEFORM_HINT = "Enter a custom response";
|
|
11
9
|
export declare const CONTEXT_TOGGLE_KEYS: readonly ["ctrl+e", "ctrl+x", "ctrl+y"];
|
|
12
10
|
export declare const INLINE_CONTEXT_MAX_ROWS = 3;
|
|
13
11
|
export declare const INLINE_DOCK_RESERVED_ROWS = 5;
|
|
14
|
-
/** Section of `<agent-dir>/ext_settings.json` (and its project override) holding these preferences. */
|
|
15
|
-
export declare const SETTINGS_SECTION = "askUser";
|
package/dist/constants.js
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
import { Key } from "@earendil-works/pi-tui";
|
|
2
2
|
export const BATCH_MIN_QUESTIONS = 2;
|
|
3
3
|
export const BATCH_MAX_QUESTIONS = 4;
|
|
4
|
-
export const
|
|
5
|
-
export const
|
|
6
|
-
export const ASK_OVERLAY_WIDTH = "92%";
|
|
7
|
-
export const ASK_OVERLAY_MIN_WIDTH = 40;
|
|
4
|
+
export const ASK_MAX_RENDER_LINES_RATIO = 0.85;
|
|
5
|
+
export const ASK_MIN_RENDER_LINES = 8;
|
|
8
6
|
export const FREEFORM_SENTINEL = "\u270f\ufe0f Type custom response...";
|
|
9
7
|
export const FREEFORM_LABEL = "Type something.";
|
|
10
8
|
export const FREEFORM_HINT = "Enter a custom response";
|
|
@@ -13,5 +11,3 @@ export const INLINE_CONTEXT_MAX_ROWS = 3;
|
|
|
13
11
|
// Rows Pi's fullscreen layout keeps outside the input dock: the transcript's
|
|
14
12
|
// minimum row, the working status, and up to three footer rows.
|
|
15
13
|
export const INLINE_DOCK_RESERVED_ROWS = 5;
|
|
16
|
-
/** Section of `<agent-dir>/ext_settings.json` (and its project override) holding these preferences. */
|
|
17
|
-
export const SETTINGS_SECTION = "askUser";
|
package/dist/custom-prompt.d.ts
CHANGED
|
@@ -1,12 +1,8 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionContext, KeybindingsManager, Theme } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import type { Component, TUI } from "@earendil-works/pi-tui";
|
|
3
|
-
import type { AskDisplayMode } from "./schema";
|
|
4
|
-
/** Report the session as blocked on the user (`herdr:blocked`) for the duration of `run`. */
|
|
5
|
-
export declare function whileBlocked<T>(pi: ExtensionAPI, run: () => Promise<T>): Promise<T>;
|
|
6
3
|
export interface CustomPromptRequest<T> {
|
|
7
4
|
signal?: AbortSignal | undefined;
|
|
8
5
|
timeout?: number | undefined;
|
|
9
|
-
displayMode: AskDisplayMode;
|
|
10
6
|
/** Absent when the host's own dialog answers the prompt, e.g. a question without options. */
|
|
11
7
|
createComponent?: (tui: TUI, theme: Theme, keybindings: KeybindingsManager, complete: (value: T | null) => void) => Component;
|
|
12
8
|
/** Non-TUI hosts (RPC, print) answer through dialogs instead of a custom surface. */
|
package/dist/custom-prompt.js
CHANGED
|
@@ -1,33 +1,11 @@
|
|
|
1
1
|
import { openTuiSurface } from "@hheei/pi-ext-core";
|
|
2
|
-
import { ASK_OVERLAY_MIN_WIDTH, ASK_OVERLAY_WIDTH } from "./constants";
|
|
3
|
-
/** Report the session as blocked on the user (`herdr:blocked`) for the duration of `run`. */
|
|
4
|
-
export async function whileBlocked(pi, run) {
|
|
5
|
-
pi.events.emit("herdr:blocked", { active: true, label: "Waiting for user response" });
|
|
6
|
-
try {
|
|
7
|
-
return await run();
|
|
8
|
-
}
|
|
9
|
-
finally {
|
|
10
|
-
pi.events.emit("herdr:blocked", { active: false });
|
|
11
|
-
}
|
|
12
|
-
}
|
|
13
|
-
function overlayOptions(displayMode) {
|
|
14
|
-
if (displayMode !== "overlay")
|
|
15
|
-
return undefined;
|
|
16
|
-
return {
|
|
17
|
-
anchor: "center",
|
|
18
|
-
width: ASK_OVERLAY_WIDTH,
|
|
19
|
-
minWidth: ASK_OVERLAY_MIN_WIDTH,
|
|
20
|
-
maxHeight: "85%",
|
|
21
|
-
margin: 1,
|
|
22
|
-
};
|
|
23
|
-
}
|
|
24
2
|
/**
|
|
25
3
|
* Runs one prompt and owns everything around it: the surface slot, the abort
|
|
26
4
|
* forwarding, and the timeout. `null` means the user cancelled, the deadline
|
|
27
5
|
* passed, or the host has no custom UI.
|
|
28
6
|
*/
|
|
29
7
|
export async function runCustomPrompt(pi, ctx, request) {
|
|
30
|
-
const { signal, timeout,
|
|
8
|
+
const { signal, timeout, createComponent } = request;
|
|
31
9
|
const deadline = timeout && timeout > 0 ? Date.now() + timeout : undefined;
|
|
32
10
|
const local = new AbortController();
|
|
33
11
|
const abortLocally = () => local.abort(signal?.reason);
|
|
@@ -39,11 +17,10 @@ export async function runCustomPrompt(pi, ctx, request) {
|
|
|
39
17
|
if (ctx.mode !== "tui" || createComponent === undefined) {
|
|
40
18
|
return await request.fallback(local.signal, deadline);
|
|
41
19
|
}
|
|
42
|
-
const options = overlayOptions(displayMode);
|
|
43
20
|
const result = await openTuiSurface(pi, ctx, {
|
|
44
21
|
hostId: "ask-user",
|
|
45
22
|
signal: local.signal,
|
|
46
|
-
|
|
23
|
+
blocked: { kind: "question", message: "Waiting for user response" },
|
|
47
24
|
create: ({ tui, theme, keybindings, close }) => createComponent(tui, theme, keybindings, close),
|
|
48
25
|
});
|
|
49
26
|
return result.status === "closed" ? result.value : null;
|
package/dist/execute.js
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { AskComponent } from "./ask-prompt";
|
|
2
2
|
import { BatchAskComponent } from "./batch-prompt";
|
|
3
|
-
import { runCustomPrompt
|
|
3
|
+
import { runCustomPrompt } from "./custom-prompt";
|
|
4
4
|
import { askBatchViaDialogs, askText, askViaDialogs } from "./dialogs";
|
|
5
5
|
import { createAskEventEmitter } from "./events";
|
|
6
6
|
import { formatBatchAnswers, formatBatchForMessage, formatSingleForMessage } from "./format";
|
|
7
|
-
import { resolvePromptSettings } from "./preferences";
|
|
8
7
|
import { createFreeformResponse, formatResponseSummary, } from "./response";
|
|
9
8
|
import { batchHeading, coerceOptions, malformedOptionsError, normalizeBatchQuestions, } from "./schema";
|
|
10
9
|
/** Host text dialog for a prompt without options; shares the caller's deadline and abort. */
|
|
@@ -20,14 +19,13 @@ async function askForText(ctx, question, context, signal, deadline) {
|
|
|
20
19
|
}
|
|
21
20
|
/** One prompt run: the surface, its deadline, the abort forwarding, and the dialog fallback. */
|
|
22
21
|
async function runInteractive(pi, ctx, request) {
|
|
23
|
-
const {
|
|
24
|
-
return await
|
|
22
|
+
const { createComponent } = request;
|
|
23
|
+
return await runCustomPrompt(pi, ctx, {
|
|
25
24
|
signal: request.signal,
|
|
26
25
|
timeout: request.timeout,
|
|
27
|
-
displayMode: settings.displayMode,
|
|
28
26
|
...(createComponent === undefined ? {} : { createComponent }),
|
|
29
27
|
fallback: request.fallback,
|
|
30
|
-
})
|
|
28
|
+
});
|
|
31
29
|
}
|
|
32
30
|
export async function executeSingle(pi, params, signal, onUpdate, ctx) {
|
|
33
31
|
const question = params.question;
|
|
@@ -40,7 +38,6 @@ export async function executeSingle(pi, params, signal, onUpdate, ctx) {
|
|
|
40
38
|
details: { question, context: params.context, options: [], response: null, cancelled: true },
|
|
41
39
|
};
|
|
42
40
|
}
|
|
43
|
-
const settings = await resolvePromptSettings(params, signal);
|
|
44
41
|
const context = params.context?.trim() || undefined;
|
|
45
42
|
const rawOptions = params.options ?? [];
|
|
46
43
|
const options = coerceOptions(rawOptions);
|
|
@@ -72,12 +69,11 @@ export async function executeSingle(pi, params, signal, onUpdate, ctx) {
|
|
|
72
69
|
});
|
|
73
70
|
}
|
|
74
71
|
const response = await runInteractive(pi, ctx, {
|
|
75
|
-
settings,
|
|
76
72
|
signal,
|
|
77
73
|
timeout: params.timeout_ms,
|
|
78
74
|
createComponent: options.length === 0
|
|
79
75
|
? undefined
|
|
80
|
-
: (tui, theme, keybindings, complete) => new AskComponent(prompt,
|
|
76
|
+
: (tui, theme, keybindings, complete) => new AskComponent(prompt, tui, theme, keybindings, complete),
|
|
81
77
|
fallback: (fallbackSignal, deadline) => options.length === 0
|
|
82
78
|
? askForText(ctx, question, context, fallbackSignal, deadline)
|
|
83
79
|
: askViaDialogs(ctx.ui, question, context, options, prompt.allowMultiple, prompt.allowFreeform, { signal: fallbackSignal }, deadline),
|
|
@@ -117,7 +113,6 @@ export async function executeBatch(pi, params, signal, onUpdate, ctx) {
|
|
|
117
113
|
details: details([], true),
|
|
118
114
|
};
|
|
119
115
|
}
|
|
120
|
-
const settings = await resolvePromptSettings(params, signal);
|
|
121
116
|
const events = createAskEventEmitter(pi);
|
|
122
117
|
if (!ctx.hasUI || !ctx.ui) {
|
|
123
118
|
throw new Error(formatBatchForMessage(questions, heading));
|
|
@@ -127,10 +122,9 @@ export async function executeBatch(pi, params, signal, onUpdate, ctx) {
|
|
|
127
122
|
details: details([], false),
|
|
128
123
|
});
|
|
129
124
|
const answers = await runInteractive(pi, ctx, {
|
|
130
|
-
settings,
|
|
131
125
|
signal,
|
|
132
126
|
timeout: params.timeout_ms,
|
|
133
|
-
createComponent: (tui, theme, keybindings, complete) => new BatchAskComponent(questions,
|
|
127
|
+
createComponent: (tui, theme, keybindings, complete) => new BatchAskComponent(questions, tui, theme, keybindings, complete, heading),
|
|
134
128
|
fallback: (fallbackSignal, deadline) => askBatchViaDialogs(ctx.ui, questions, fallbackSignal, deadline, heading),
|
|
135
129
|
});
|
|
136
130
|
if (answers === null || signal?.aborted) {
|
package/dist/index.js
CHANGED
package/dist/prompt-help.d.ts
CHANGED
|
@@ -1,18 +1,14 @@
|
|
|
1
1
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import type { KeybindingsManager } from "@earendil-works/pi-tui";
|
|
3
3
|
import type { AskMode } from "./constants";
|
|
4
|
-
import type { AskDisplayMode } from "./schema";
|
|
5
4
|
export declare function formatKeyList(keys: readonly string[]): string;
|
|
6
5
|
export interface HelpHintInput {
|
|
7
6
|
theme: Theme;
|
|
8
7
|
mode: AskMode;
|
|
9
|
-
displayMode: AskDisplayMode;
|
|
10
8
|
allowMultiple: boolean;
|
|
11
9
|
hasOptions: boolean;
|
|
12
10
|
contextCollapsible: boolean;
|
|
13
11
|
contextToggleKey: string;
|
|
14
|
-
/** True when the prompt pane clipped, so the scroll keys are worth showing. */
|
|
15
|
-
promptScrollable: boolean;
|
|
16
12
|
keybindings: KeybindingsManager;
|
|
17
13
|
}
|
|
18
14
|
/**
|
package/dist/prompt-help.js
CHANGED
|
@@ -30,7 +30,6 @@ export function buildHelpText(input) {
|
|
|
30
30
|
: [binding("tui.select.confirm"), "select"],
|
|
31
31
|
input.allowMultiple ? ["␣", "toggle"] : null,
|
|
32
32
|
input.contextCollapsible ? [input.contextToggleKey, "context"] : null,
|
|
33
|
-
input.promptScrollable ? ["PgUp/PgDn", "prompt"] : null,
|
|
34
33
|
[binding("tui.select.cancel"), "cancel"],
|
|
35
34
|
]
|
|
36
35
|
: [
|
package/dist/prompt-layout.d.ts
CHANGED
|
@@ -1,43 +1,5 @@
|
|
|
1
1
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { type AskMode } from "./constants";
|
|
3
|
-
import type { AskDisplayMode } from "./schema";
|
|
4
|
-
/** A surface is bracketed by two `──` rails, the way `pi-ext-ui` groups are. */
|
|
5
|
-
export declare const RAIL_ROWS = 2;
|
|
6
2
|
/** The native separator row: one full-width `─` rule in `borderMuted`. */
|
|
7
3
|
export declare function railRow(theme: Theme, width: number): string;
|
|
8
|
-
/** Rows
|
|
9
|
-
export declare function
|
|
10
|
-
export declare function modeMinRows(mode: AskMode): number;
|
|
11
|
-
export declare function modePreferredRows(mode: AskMode): number;
|
|
12
|
-
export declare function overlayHelpBudget(bodyCapacity: number, renderedHelpRows: number): number;
|
|
13
|
-
export interface ContextCollapseInput {
|
|
14
|
-
displayMode: AskDisplayMode;
|
|
15
|
-
mode: AskMode;
|
|
16
|
-
contextCollapsible: boolean;
|
|
17
|
-
questionRows: number;
|
|
18
|
-
contextRows: number;
|
|
19
|
-
bodyCapacity: number;
|
|
20
|
-
helpRows: number;
|
|
21
|
-
}
|
|
22
|
-
/**
|
|
23
|
-
* Inline mode always follows the row budget; the overlay keeps context open
|
|
24
|
-
* when the prompt still fits next to the minimum mode rows.
|
|
25
|
-
*/
|
|
26
|
-
export declare function contextShouldCollapse(input: ContextCollapseInput): boolean;
|
|
27
|
-
export interface BodyPlanInput {
|
|
28
|
-
maxRenderLines: number;
|
|
29
|
-
mode: AskMode;
|
|
30
|
-
contextCollapsible: boolean;
|
|
31
|
-
promptRows: number;
|
|
32
|
-
helpRows: number;
|
|
33
|
-
}
|
|
34
|
-
export interface BodyPlan {
|
|
35
|
-
bodyCapacity: number;
|
|
36
|
-
helpBudget: number;
|
|
37
|
-
contentRows: number;
|
|
38
|
-
promptBudget: number;
|
|
39
|
-
modeBudget: number;
|
|
40
|
-
separatorRows: number;
|
|
41
|
-
}
|
|
42
|
-
/** Split the overlay rows between prompt, mode body and help. */
|
|
43
|
-
export declare function planOverlayBody(input: BodyPlanInput): BodyPlan;
|
|
4
|
+
/** Rows a prompt may draw for the terminal height: a ratio cap with a usable floor. */
|
|
5
|
+
export declare function getMaxRenderLinesForRows(rows: number): number;
|
package/dist/prompt-layout.js
CHANGED
|
@@ -1,89 +1,13 @@
|
|
|
1
|
-
import {
|
|
2
|
-
/** Rows each mode needs to stay usable, and the size it grows to when there is room. */
|
|
3
|
-
const MODE_ROWS = {
|
|
4
|
-
select: { min: 3, preferred: 8 },
|
|
5
|
-
freeform: { min: 5, preferred: 10 },
|
|
6
|
-
};
|
|
7
|
-
/** A surface is bracketed by two `──` rails, the way `pi-ext-ui` groups are. */
|
|
8
|
-
export const RAIL_ROWS = 2;
|
|
1
|
+
import { ASK_MAX_RENDER_LINES_RATIO, ASK_MIN_RENDER_LINES } from "./constants";
|
|
9
2
|
/** The native separator row: one full-width `─` rule in `borderMuted`. */
|
|
10
3
|
export function railRow(theme, width) {
|
|
11
4
|
return theme.fg("borderMuted", "─".repeat(Math.max(0, width)));
|
|
12
5
|
}
|
|
13
|
-
/** Rows
|
|
14
|
-
export function
|
|
6
|
+
/** Rows a prompt may draw for the terminal height: a ratio cap with a usable floor. */
|
|
7
|
+
export function getMaxRenderLinesForRows(rows) {
|
|
15
8
|
const normalizedRows = Number.isFinite(rows) ? Math.max(1, Math.floor(rows)) : 24;
|
|
16
9
|
const availableRows = Math.max(1, normalizedRows - 2);
|
|
17
|
-
const ratioRows = Math.max(1, Math.floor(normalizedRows *
|
|
18
|
-
const minimumRows = Math.min(
|
|
10
|
+
const ratioRows = Math.max(1, Math.floor(normalizedRows * ASK_MAX_RENDER_LINES_RATIO));
|
|
11
|
+
const minimumRows = Math.min(ASK_MIN_RENDER_LINES, availableRows);
|
|
19
12
|
return Math.min(availableRows, Math.max(minimumRows, ratioRows));
|
|
20
13
|
}
|
|
21
|
-
export function modeMinRows(mode) {
|
|
22
|
-
return MODE_ROWS[mode].min;
|
|
23
|
-
}
|
|
24
|
-
export function modePreferredRows(mode) {
|
|
25
|
-
return MODE_ROWS[mode].preferred;
|
|
26
|
-
}
|
|
27
|
-
export function overlayHelpBudget(bodyCapacity, renderedHelpRows) {
|
|
28
|
-
if (renderedHelpRows <= 0 || bodyCapacity <= 0)
|
|
29
|
-
return 0;
|
|
30
|
-
if (bodyCapacity >= 12)
|
|
31
|
-
return Math.min(2, renderedHelpRows);
|
|
32
|
-
return 1;
|
|
33
|
-
}
|
|
34
|
-
/**
|
|
35
|
-
* Inline mode always follows the row budget; the overlay keeps context open
|
|
36
|
-
* when the prompt still fits next to the minimum mode rows.
|
|
37
|
-
*/
|
|
38
|
-
export function contextShouldCollapse(input) {
|
|
39
|
-
if (input.displayMode === "inline")
|
|
40
|
-
return input.contextCollapsible;
|
|
41
|
-
if (input.mode !== "select")
|
|
42
|
-
return input.contextCollapsible;
|
|
43
|
-
if (input.contextRows === 0)
|
|
44
|
-
return false;
|
|
45
|
-
const helpBudget = overlayHelpBudget(input.bodyCapacity, input.helpRows);
|
|
46
|
-
const contentRows = Math.max(0, input.bodyCapacity - helpBudget);
|
|
47
|
-
const separatorRows = contentRows >= 4 ? 1 : 0;
|
|
48
|
-
const promptCapacity = Math.max(0, contentRows - separatorRows - modeMinRows(input.mode));
|
|
49
|
-
return input.questionRows + 1 + input.contextRows > promptCapacity;
|
|
50
|
-
}
|
|
51
|
-
/** Split the overlay rows between prompt, mode body and help. */
|
|
52
|
-
export function planOverlayBody(input) {
|
|
53
|
-
const bodyCapacity = Math.max(0, input.maxRenderLines);
|
|
54
|
-
const helpBudget = overlayHelpBudget(bodyCapacity, input.helpRows);
|
|
55
|
-
const contentRows = Math.max(0, bodyCapacity - helpBudget);
|
|
56
|
-
let promptBudget = 0;
|
|
57
|
-
let modeBudget = 0;
|
|
58
|
-
let separatorRows = 0;
|
|
59
|
-
if (input.mode === "select") {
|
|
60
|
-
separatorRows = contentRows >= 4 ? 1 : 0;
|
|
61
|
-
const promptAndModeRows = Math.max(0, contentRows - separatorRows);
|
|
62
|
-
promptBudget = promptAndModeRows;
|
|
63
|
-
if (promptAndModeRows > 0) {
|
|
64
|
-
const promptMinRows = input.promptRows > 0 ? 1 : 0;
|
|
65
|
-
const maximumModeRows = Math.max(0, promptAndModeRows - promptMinRows);
|
|
66
|
-
const modeMin = Math.min(modeMinRows(input.mode), maximumModeRows);
|
|
67
|
-
modeBudget = Math.min(modePreferredRows(input.mode), maximumModeRows);
|
|
68
|
-
modeBudget = Math.max(modeMin, modeBudget);
|
|
69
|
-
promptBudget = promptAndModeRows - modeBudget;
|
|
70
|
-
const usefulPromptTarget = input.contextCollapsible ? 3 : 2;
|
|
71
|
-
const usefulPromptRows = Math.min(input.promptRows, promptAndModeRows >= modeMin + usefulPromptTarget ? usefulPromptTarget : promptMinRows);
|
|
72
|
-
if (promptBudget < usefulPromptRows && modeBudget > modeMin) {
|
|
73
|
-
const shiftedRows = Math.min(usefulPromptRows - promptBudget, modeBudget - modeMin);
|
|
74
|
-
modeBudget -= shiftedRows;
|
|
75
|
-
promptBudget += shiftedRows;
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
else {
|
|
80
|
-
modeBudget = Math.min(modePreferredRows(input.mode), contentRows);
|
|
81
|
-
modeBudget = Math.max(Math.min(modeMinRows(input.mode), contentRows), modeBudget);
|
|
82
|
-
promptBudget = Math.max(0, contentRows - modeBudget);
|
|
83
|
-
if (promptBudget > 0 && modeBudget > 0) {
|
|
84
|
-
separatorRows = 1;
|
|
85
|
-
promptBudget = Math.max(0, promptBudget - separatorRows);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
return { bodyCapacity, helpBudget, contentRows, promptBudget, modeBudget, separatorRows };
|
|
89
|
-
}
|
package/dist/schema.d.ts
CHANGED
|
@@ -1,8 +1,5 @@
|
|
|
1
|
-
import { StringEnum } from "@earendil-works/pi-ai";
|
|
2
1
|
import type { QuestionOption } from "./single-select-layout";
|
|
3
|
-
export { StringEnum };
|
|
4
2
|
type AskOptionInput = QuestionOption | string;
|
|
5
|
-
export type AskDisplayMode = "overlay" | "inline";
|
|
6
3
|
interface BatchQuestionInput {
|
|
7
4
|
question: string;
|
|
8
5
|
context?: string;
|
|
@@ -17,7 +14,6 @@ export interface AskParams {
|
|
|
17
14
|
options?: AskOptionInput[];
|
|
18
15
|
allowMultiple?: boolean;
|
|
19
16
|
allowFreeform?: boolean;
|
|
20
|
-
displayMode?: AskDisplayMode;
|
|
21
17
|
timeout_ms?: number;
|
|
22
18
|
}
|
|
23
19
|
/** One validated entry of a `questions` batch. */
|
|
@@ -41,3 +37,4 @@ export declare function batchHeading(params: AskParams): string | undefined;
|
|
|
41
37
|
* correct the call.
|
|
42
38
|
*/
|
|
43
39
|
export declare function normalizeBatchQuestions(params: AskParams): BatchQuestion[];
|
|
40
|
+
export {};
|
package/dist/schema.js
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
import { StringEnum } from "@earendil-works/pi-ai";
|
|
2
1
|
import { isRecord } from "@hheei/pi-ext-core";
|
|
3
2
|
import { BATCH_MAX_QUESTIONS, BATCH_MIN_QUESTIONS } from "./constants";
|
|
4
|
-
export { StringEnum };
|
|
5
3
|
// Key aliases models fall back to when a schema-mangling proxy (Google
|
|
6
4
|
// function calling, Codex-style backends, cmux) strips the option shape and
|
|
7
5
|
// the model has to guess. See issue #22.
|
package/dist/shortcuts.d.ts
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import { type KeybindingsManager } from "@earendil-works/pi-tui";
|
|
2
2
|
export declare function matchesSelectUp(data: string, keybindings: KeybindingsManager): boolean;
|
|
3
3
|
export declare function matchesSelectDown(data: string, keybindings: KeybindingsManager): boolean;
|
|
4
|
-
/** PageUp/PageDown move by a viewport. The prompt pane owns these; lists own the arrows. */
|
|
5
|
-
export declare function pageDelta(data: string, pageRows: number): number;
|
|
6
4
|
/** A 1-9 digit key, including the CSI-u form the kitty keyboard protocol sends. */
|
|
7
5
|
export declare function digitKey(data: string): number | null;
|
package/dist/shortcuts.js
CHANGED
|
@@ -6,14 +6,6 @@ export function matchesSelectUp(data, keybindings) {
|
|
|
6
6
|
export function matchesSelectDown(data, keybindings) {
|
|
7
7
|
return keybindings.matches(data, "tui.select.down") || matchesKey(data, Key.tab);
|
|
8
8
|
}
|
|
9
|
-
/** PageUp/PageDown move by a viewport. The prompt pane owns these; lists own the arrows. */
|
|
10
|
-
export function pageDelta(data, pageRows) {
|
|
11
|
-
if (matchesKey(data, Key.pageUp))
|
|
12
|
-
return -pageRows;
|
|
13
|
-
if (matchesKey(data, Key.pageDown))
|
|
14
|
-
return pageRows;
|
|
15
|
-
return 0;
|
|
16
|
-
}
|
|
17
9
|
/** A 1-9 digit key, including the CSI-u form the kitty keyboard protocol sends. */
|
|
18
10
|
export function digitKey(data) {
|
|
19
11
|
const key = decodeKittyPrintable(data) ?? data;
|
package/dist/tool.js
CHANGED
|
@@ -2,7 +2,6 @@ import { Type } from "typebox";
|
|
|
2
2
|
import { BATCH_MAX_QUESTIONS, BATCH_MIN_QUESTIONS } from "./constants";
|
|
3
3
|
import { executeBatch, executeSingle } from "./execute";
|
|
4
4
|
import { createAskUserRenderers } from "./presentation";
|
|
5
|
-
import { StringEnum } from "./schema";
|
|
6
5
|
const ASK_DESCRIPTION = "Ask the user a question with optional multiple-choice answers, or 2-4 independent questions in one prompt. The turn blocks until the user answers.";
|
|
7
6
|
const ASK_PROMPT_SNIPPET = "Ask the user one focused question, or 2-4 independent ones together, with optional multiple-choice answers";
|
|
8
7
|
const ASK_PROMPT_GUIDELINES = [
|
|
@@ -21,7 +20,6 @@ const ASK_PARAMETER_DESCRIPTIONS = {
|
|
|
21
20
|
options: "Options for the user to choose from",
|
|
22
21
|
allowMultiple: "Allow selecting multiple options (default: false)",
|
|
23
22
|
allowFreeform: "Add a freeform text option (default: true)",
|
|
24
|
-
displayMode: "'overlay' shows a centered modal, 'inline' renders in place (default: displayMode in the askUser section of ext_settings.json, otherwise 'overlay')",
|
|
25
23
|
timeoutMs: "Auto-dismiss after N milliseconds; the result is cancelled",
|
|
26
24
|
};
|
|
27
25
|
// Flat object shape: union item schemas get stripped or rejected by several
|
|
@@ -63,9 +61,6 @@ export function registerAskUserTool(pi) {
|
|
|
63
61
|
options: Type.Optional(Type.Array(optionSchema, { description: ASK_PARAMETER_DESCRIPTIONS.options })),
|
|
64
62
|
allowMultiple: Type.Optional(Type.Boolean({ description: ASK_PARAMETER_DESCRIPTIONS.allowMultiple })),
|
|
65
63
|
allowFreeform: Type.Optional(Type.Boolean({ description: ASK_PARAMETER_DESCRIPTIONS.allowFreeform })),
|
|
66
|
-
displayMode: Type.Optional(StringEnum(["overlay", "inline"], {
|
|
67
|
-
description: ASK_PARAMETER_DESCRIPTIONS.displayMode,
|
|
68
|
-
})),
|
|
69
64
|
timeout_ms: Type.Optional(Type.Integer({
|
|
70
65
|
minimum: 1,
|
|
71
66
|
description: ASK_PARAMETER_DESCRIPTIONS.timeoutMs,
|
package/package.json
CHANGED
|
@@ -1,60 +1,57 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
"@earendil-works/pi-tui": "^1.4.0"
|
|
59
|
-
}
|
|
60
|
-
}
|
|
2
|
+
"name": "@hheei/pi-ask-user",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Interactive ask_user tool for pi-coding-agent: one question or 2-4 in one prompt, multiple choice, multi-select, and freeform answers.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./dist/index.js"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"dist",
|
|
13
|
+
"skills",
|
|
14
|
+
"README.md",
|
|
15
|
+
"LICENSE"
|
|
16
|
+
],
|
|
17
|
+
"pi": {
|
|
18
|
+
"extensions": [
|
|
19
|
+
"dist/index.js"
|
|
20
|
+
],
|
|
21
|
+
"skills": [
|
|
22
|
+
"./skills"
|
|
23
|
+
]
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"pi",
|
|
27
|
+
"pi-package",
|
|
28
|
+
"pi-extension",
|
|
29
|
+
"ask",
|
|
30
|
+
"ask_user",
|
|
31
|
+
"interactive"
|
|
32
|
+
],
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "git+https://github.com/hheei/pi-mono.git",
|
|
37
|
+
"directory": "packages/pi-ask-user"
|
|
38
|
+
},
|
|
39
|
+
"publishConfig": {
|
|
40
|
+
"access": "public"
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"@hheei/pi-ext-core": "^0.8.1"
|
|
44
|
+
},
|
|
45
|
+
"peerDependencies": {
|
|
46
|
+
"@earendil-works/pi-coding-agent": ">=1.5.0",
|
|
47
|
+
"@earendil-works/pi-tui": ">=1.5.0",
|
|
48
|
+
"typebox": "*"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@earendil-works/pi-coding-agent": "^1.5.0",
|
|
52
|
+
"@earendil-works/pi-tui": "^1.5.0"
|
|
53
|
+
},
|
|
54
|
+
"scripts": {
|
|
55
|
+
"build": "node -e \"import { rmSync } from 'node:fs'; rmSync('dist', { recursive: true, force: true })\" && tsc -p tsconfig.build.json"
|
|
56
|
+
}
|
|
57
|
+
}
|
package/skills/ask-user/SKILL.md
CHANGED
|
@@ -54,7 +54,6 @@ Call `ask_user` with one decision at a time:
|
|
|
54
54
|
- `options`: 2-5 clear choices when possible
|
|
55
55
|
- `allowMultiple`: `false` unless independent selections are genuinely needed
|
|
56
56
|
- `allowFreeform`: usually `true`
|
|
57
|
-
- `displayMode` *(optional)*: `"overlay"` (default) or `"inline"`. Use `"inline"` when preceding assistant context (summary, trade-offs, recommendation) is essential to the decision and should remain visible — overlays cover the conversation underneath. The user may set a personal default via `displayMode` in the `askUser` section of `ext_settings.json`; only pass this when you intentionally want to override it for one call.
|
|
58
57
|
- `timeout_ms` *(optional)*: auto-dismiss after N milliseconds; the result comes back cancelled. Omit unless the decision is genuinely time-boxed.
|
|
59
58
|
|
|
60
59
|
When 2-4 decisions at the same boundary are independent of each other and their prerequisites are settled, you may ask them together with `questions` instead of `question`. Each entry carries its own `question`, `context`, `options`, `allowMultiple`, and `allowFreeform`. A top-level `question` sent with `questions` is only a heading, never asked. Never batch a decision whose options depend on another answer; ask it in a later call once that answer is known.
|
|
@@ -66,19 +66,6 @@ Use this protocol whenever the trigger matrix says to ask.
|
|
|
66
66
|
}
|
|
67
67
|
```
|
|
68
68
|
|
|
69
|
-
### Display mode (optional)
|
|
70
|
-
|
|
71
|
-
The `ask_user` tool accepts an optional `displayMode` parameter:
|
|
72
|
-
|
|
73
|
-
- `"overlay"` *(default)*: centered modal; covers the conversation underneath.
|
|
74
|
-
- `"inline"`: rendered in the conversation flow; preceding messages stay visible.
|
|
75
|
-
|
|
76
|
-
Guidance:
|
|
77
|
-
|
|
78
|
-
- Omit `displayMode` to respect the user's configured preference (`displayMode` in the `askUser` section of `ext_settings.json`).
|
|
79
|
-
- Pass `"inline"` only when the immediately preceding assistant message (summary, trade-offs, recommendation) is the primary context for the decision and must remain visible.
|
|
80
|
-
- Pass `"overlay"` only to explicitly force the modal style (rare).
|
|
81
|
-
|
|
82
69
|
### Batches (optional)
|
|
83
70
|
|
|
84
71
|
`questions` replaces `question` with 2-4 entries, each with its own `question`, `context`, `options`, `allowMultiple`, and `allowFreeform`. The user answers them on separate pages and submits from a review page, where unanswered questions can be submitted as skipped.
|
package/dist/preferences.d.ts
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import { SETTINGS_SECTION } from "./constants";
|
|
2
|
-
import type { AskDisplayMode, AskParams } from "./schema";
|
|
3
|
-
export { SETTINGS_SECTION };
|
|
4
|
-
export interface PromptSettings {
|
|
5
|
-
displayMode: AskDisplayMode;
|
|
6
|
-
}
|
|
7
|
-
/** Resolve presentation preferences: call parameter, then the askUser section of ext_settings.json, then the field default. */
|
|
8
|
-
export declare function resolvePromptSettings(params: AskParams, signal?: AbortSignal): Promise<PromptSettings>;
|
package/dist/preferences.js
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import { defaultExtensionSettingsPaths, readMergedJsonSettingsSection, } from "@hheei/pi-ext-core";
|
|
2
|
-
import { SETTINGS_SECTION } from "./constants";
|
|
3
|
-
import { ASK_USER_FIELDS } from "./settings-provider";
|
|
4
|
-
export { SETTINGS_SECTION };
|
|
5
|
-
function pickEnum(raw, field) {
|
|
6
|
-
// ext-core types option values as SettingPrimitive; an enum field only ever lists its own T.
|
|
7
|
-
const matched = field.options?.find((option) => option.value === raw)?.value;
|
|
8
|
-
return matched ?? field.defaultValue;
|
|
9
|
-
}
|
|
10
|
-
/** Read on every ask_user call so edits apply without restarting Pi. */
|
|
11
|
-
async function readAskUserSettings(signal) {
|
|
12
|
-
const { merged } = await readMergedJsonSettingsSection({
|
|
13
|
-
paths: defaultExtensionSettingsPaths(),
|
|
14
|
-
section: SETTINGS_SECTION,
|
|
15
|
-
...(signal ? { signal } : {}),
|
|
16
|
-
});
|
|
17
|
-
return merged;
|
|
18
|
-
}
|
|
19
|
-
/** Resolve presentation preferences: call parameter, then the askUser section of ext_settings.json, then the field default. */
|
|
20
|
-
export async function resolvePromptSettings(params, signal) {
|
|
21
|
-
const raw = await readAskUserSettings(signal);
|
|
22
|
-
return {
|
|
23
|
-
displayMode: params.displayMode ?? pickEnum(raw.displayMode, ASK_USER_FIELDS.displayMode),
|
|
24
|
-
};
|
|
25
|
-
}
|
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
export declare function applyOverflowMarkers(theme: Theme, lines: string[], width: number, hasHiddenAbove: boolean, hasHiddenBelow: boolean): string[];
|
|
3
|
-
export declare function limitLines(theme: Theme, lines: string[], budget: number, width: number, showOverflowMarker: boolean): string[];
|
|
4
|
-
/** Editor rows keep their top/bottom borders and follow the cursor when clipped. */
|
|
5
|
-
export declare function limitEditorLines(theme: Theme, lines: string[], budget: number, width: number): string[];
|
package/dist/prompt-overflow.js
DELETED
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
import { CURSOR_MARKER, truncateToWidth } from "@earendil-works/pi-tui";
|
|
2
|
-
export function applyOverflowMarkers(theme, lines, width, hasHiddenAbove, hasHiddenBelow) {
|
|
3
|
-
if (lines.length === 0)
|
|
4
|
-
return lines;
|
|
5
|
-
const marker = (line, glyph) => truncateToWidth(`${theme.fg("dim", glyph)} ${line}`, width, "", true);
|
|
6
|
-
const marked = [...lines];
|
|
7
|
-
if (hasHiddenAbove && hasHiddenBelow && marked.length === 1) {
|
|
8
|
-
marked[0] = marker(marked[0] ?? "", "↕");
|
|
9
|
-
return marked;
|
|
10
|
-
}
|
|
11
|
-
if (hasHiddenAbove) {
|
|
12
|
-
marked[0] = marker(marked[0] ?? "", "↑");
|
|
13
|
-
}
|
|
14
|
-
if (hasHiddenBelow) {
|
|
15
|
-
const lastIndex = marked.length - 1;
|
|
16
|
-
marked[lastIndex] = marker(marked[lastIndex] ?? "", "↓");
|
|
17
|
-
}
|
|
18
|
-
return marked;
|
|
19
|
-
}
|
|
20
|
-
const truncateLines = (lines, width) => lines.map((line) => truncateToWidth(line, width, "", true));
|
|
21
|
-
export function limitLines(theme, lines, budget, width, showOverflowMarker) {
|
|
22
|
-
const safeBudget = Math.max(0, Math.floor(budget));
|
|
23
|
-
if (safeBudget <= 0)
|
|
24
|
-
return [];
|
|
25
|
-
if (lines.length <= safeBudget)
|
|
26
|
-
return truncateLines(lines, width);
|
|
27
|
-
if (!showOverflowMarker)
|
|
28
|
-
return truncateLines(lines.slice(0, safeBudget), width);
|
|
29
|
-
if (safeBudget === 1)
|
|
30
|
-
return [theme.fg("dim", "…")];
|
|
31
|
-
return [...truncateLines(lines.slice(0, safeBudget - 1), width), theme.fg("dim", "…")];
|
|
32
|
-
}
|
|
33
|
-
/** Editor rows keep their top/bottom borders and follow the cursor when clipped. */
|
|
34
|
-
export function limitEditorLines(theme, lines, budget, width) {
|
|
35
|
-
const safeBudget = Math.max(0, Math.floor(budget));
|
|
36
|
-
if (safeBudget <= 0)
|
|
37
|
-
return [];
|
|
38
|
-
if (lines.length <= safeBudget)
|
|
39
|
-
return truncateLines(lines, width);
|
|
40
|
-
if (safeBudget === 1)
|
|
41
|
-
return [theme.fg("dim", "…")];
|
|
42
|
-
const topBorder = truncateToWidth(lines[0] ?? "", width, "", true);
|
|
43
|
-
const bottomBorder = truncateToWidth(lines[lines.length - 1] ?? "", width, "", true);
|
|
44
|
-
if (safeBudget === 2)
|
|
45
|
-
return [topBorder, bottomBorder];
|
|
46
|
-
const contentLines = lines.slice(1, -1);
|
|
47
|
-
const contentBudget = safeBudget - 2;
|
|
48
|
-
// Locate the cursor row: prefer the zero-width CURSOR_MARKER the editor
|
|
49
|
-
// emits while focused (the same mechanism pi-tui core uses for hardware
|
|
50
|
-
// cursor placement), falling back to the inverse-video fake cursor.
|
|
51
|
-
const cursorLineIndex = contentLines.findIndex((line) => line.includes(CURSOR_MARKER) || line.includes("\x1b[7m"));
|
|
52
|
-
const maxStart = Math.max(0, contentLines.length - contentBudget);
|
|
53
|
-
const start = cursorLineIndex >= 0
|
|
54
|
-
? Math.max(0, Math.min(cursorLineIndex - contentBudget + 1, maxStart))
|
|
55
|
-
: maxStart;
|
|
56
|
-
const visibleContentLines = contentLines.slice(start, start + contentBudget);
|
|
57
|
-
const markedContentLines = applyOverflowMarkers(theme, visibleContentLines, width, start > 0, start + contentBudget < contentLines.length);
|
|
58
|
-
return [topBorder, ...markedContentLines, bottomBorder];
|
|
59
|
-
}
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { type SettingField, type SettingsProvider } from "@hheei/pi-ext-core";
|
|
3
|
-
import type { AskDisplayMode } from "./schema";
|
|
4
|
-
export declare const ASK_USER_SETTINGS_PROVIDER_ID = "pi-ask-user";
|
|
5
|
-
/**
|
|
6
|
-
* The `askUser` section, defined once: the settings UI renders these fields and the tool
|
|
7
|
-
* resolves its own defaults from them. Values land in the global layer; a project layer
|
|
8
|
-
* still wins when the tool resolves preferences.
|
|
9
|
-
*/
|
|
10
|
-
export declare const ASK_USER_FIELDS: {
|
|
11
|
-
displayMode: SettingField<AskDisplayMode>;
|
|
12
|
-
};
|
|
13
|
-
export declare function createAskUserSettingsProvider(): SettingsProvider;
|
|
14
|
-
/** Registers the provider in the runtime registry; returns the unregister handle. */
|
|
15
|
-
export declare function registerAskUserSettings(pi: ExtensionAPI): () => void;
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import { createJsonSettingsStorage, getRuntimeSettingsRegistry, registerSettings, } from "@hheei/pi-ext-core";
|
|
2
|
-
import { SETTINGS_SECTION } from "./constants";
|
|
3
|
-
export const ASK_USER_SETTINGS_PROVIDER_ID = "pi-ask-user";
|
|
4
|
-
function enumField(id, label, description, defaultValue, values) {
|
|
5
|
-
return {
|
|
6
|
-
id,
|
|
7
|
-
label,
|
|
8
|
-
type: "enum",
|
|
9
|
-
defaultValue,
|
|
10
|
-
description,
|
|
11
|
-
options: values.map((value) => ({ value, label: value })),
|
|
12
|
-
parse: (draft) => (values.includes(draft) ? draft : defaultValue),
|
|
13
|
-
};
|
|
14
|
-
}
|
|
15
|
-
/**
|
|
16
|
-
* The `askUser` section, defined once: the settings UI renders these fields and the tool
|
|
17
|
-
* resolves its own defaults from them. Values land in the global layer; a project layer
|
|
18
|
-
* still wins when the tool resolves preferences.
|
|
19
|
-
*/
|
|
20
|
-
export const ASK_USER_FIELDS = {
|
|
21
|
-
displayMode: enumField("displayMode", "display mode", "Overlay shows a centered modal; inline renders the prompt in place.", "overlay", ["overlay", "inline"]),
|
|
22
|
-
};
|
|
23
|
-
export function createAskUserSettingsProvider() {
|
|
24
|
-
return {
|
|
25
|
-
id: ASK_USER_SETTINGS_PROVIDER_ID,
|
|
26
|
-
title: "ask_user",
|
|
27
|
-
origin: "@hheei/pi-ask-user",
|
|
28
|
-
description: "Interactive prompts: where the question is drawn.",
|
|
29
|
-
groups: [
|
|
30
|
-
{
|
|
31
|
-
id: SETTINGS_SECTION,
|
|
32
|
-
title: "",
|
|
33
|
-
fields: Object.values(ASK_USER_FIELDS),
|
|
34
|
-
},
|
|
35
|
-
],
|
|
36
|
-
storage: createJsonSettingsStorage({ group: SETTINGS_SECTION }),
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
/** Registers the provider in the runtime registry; returns the unregister handle. */
|
|
40
|
-
export function registerAskUserSettings(pi) {
|
|
41
|
-
return registerSettings(createAskUserSettingsProvider(), getRuntimeSettingsRegistry(pi));
|
|
42
|
-
}
|