@hheei/pi-ask-user 0.1.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -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
- - Reads preferences through `@hheei/pi-ext-core` from the `askUser` section of `ext_settings.json`, so project `.pi/ext_settings.json` can override the global file. Upstream's own `ask-user.json` and the `PI_ASK_USER_*` environment layer are gone.
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 context, `PgUp`/`PgDn`/`Home`/`End` scroll (overlay) |
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
@@ -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, settings: PromptSettings, tui: TUI, theme: Theme, keybindings: KeybindingsManager, onDone: (result: AskUIResult | null) => void);
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
- private renderModeLines;
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; `promptScrollable` only applies where the pane can clip. */
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
  }
@@ -3,10 +3,7 @@ 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 { contextShouldCollapse, getOverlayMaxRenderLinesForRows, planOverlayBody, RAIL_ROWS, railRow, } from "./prompt-layout";
7
- import { applyOverflowMarkers, limitEditorLines, limitLines } from "./prompt-overflow";
8
6
  import { createFreeformResponse, createSelectionResponse } from "./response";
9
- import { pageDelta } from "./shortcuts";
10
7
  import { WrappedSingleSelectList } from "./single-select-list";
11
8
  function editorTheme(theme) {
12
9
  return {
@@ -26,7 +23,6 @@ function editorTheme(theme) {
26
23
  */
27
24
  export class AskComponent extends Container {
28
25
  prompt;
29
- settings;
30
26
  contextPane;
31
27
  tui;
32
28
  theme;
@@ -35,9 +31,6 @@ export class AskComponent extends Container {
35
31
  mode = "select";
36
32
  chrome = "";
37
33
  freeformDraft = "";
38
- promptScrollOffset = 0;
39
- promptMaxScrollOffset = 0;
40
- promptViewportRows = 0;
41
34
  get contextIsCollapsible() {
42
35
  return this.contextPane.collapsible;
43
36
  }
@@ -61,10 +54,9 @@ export class AskComponent extends Container {
61
54
  this.editor.focused = value;
62
55
  }
63
56
  }
64
- constructor(prompt, settings, tui, theme, keybindings, onDone) {
57
+ constructor(prompt, tui, theme, keybindings, onDone) {
65
58
  super();
66
59
  this.prompt = prompt;
67
- this.settings = settings;
68
60
  this.tui = tui;
69
61
  this.theme = theme;
70
62
  this.keybindings = keybindings;
@@ -94,8 +86,6 @@ export class AskComponent extends Container {
94
86
  this.updateStaticText();
95
87
  }
96
88
  render(width) {
97
- if (this.settings.displayMode === "overlay")
98
- return this.renderOverlayLayout(width);
99
89
  if (this.mode === "select")
100
90
  this.optionList.setMaxVisibleRows(12);
101
91
  return this.renderInlineLayout(width);
@@ -104,56 +94,12 @@ export class AskComponent extends Container {
104
94
  const fullContextLines = this.contextPane.fullLines(width);
105
95
  this.contextPane.setCollapsible(fullContextLines.length > INLINE_CONTEXT_MAX_ROWS);
106
96
  return [
107
- railRow(this.theme, width),
108
97
  ...this.buildPromptLines(width, fullContextLines),
109
98
  "",
110
99
  ...this.modeContainer.render(width),
111
- railRow(this.theme, width),
112
- ...this.helpLines(width, false),
100
+ ...this.helpLines(width),
113
101
  ];
114
102
  }
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
103
  buildPromptLines(width, fullContextLines) {
158
104
  const questionLines = this.questionText.render(width);
159
105
  const contextLines = this.contextPane.displayLines(fullContextLines, width);
@@ -163,46 +109,10 @@ export class AskComponent extends Container {
163
109
  ...(contextLines.length > 0 ? [...contextSeparator, ...contextLines] : []),
164
110
  ];
165
111
  }
166
- renderModeLines(width, budget) {
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. */
112
+ /** Header shown above the freeform editor. */
188
113
  editorHeader() {
189
114
  return this.theme.fg("accent", this.theme.bold("Custom response"));
190
115
  }
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
116
  updateStaticText() {
207
117
  const theme = this.theme;
208
118
  const question = theme.fg("text", theme.bold(this.prompt.question));
@@ -215,17 +125,15 @@ export class AskComponent extends Container {
215
125
  this.chrome = `${theme.fg("dim", frameTitle)} ${theme.fg("dim", navigationHint)}`;
216
126
  this.updateStaticText();
217
127
  }
218
- /** The hint row for the current mode; `promptScrollable` only applies where the pane can clip. */
219
- helpLines(width, promptScrollable) {
128
+ /** The hint row for the current mode. */
129
+ helpLines(width) {
220
130
  return wrapTextWithAnsi(buildHelpText({
221
131
  theme: this.theme,
222
132
  mode: this.mode,
223
- displayMode: this.settings.displayMode,
224
133
  allowMultiple: this.prompt.allowMultiple,
225
134
  hasOptions: this.prompt.options.length > 0,
226
135
  contextCollapsible: this.contextIsCollapsible,
227
136
  contextToggleKey: this.contextPane.toggleKey(),
228
- promptScrollable,
229
137
  keybindings: this.keybindings,
230
138
  }), width);
231
139
  }
@@ -291,51 +199,14 @@ export class AskComponent extends Container {
291
199
  return false;
292
200
  if (!this.contextPane.toggle())
293
201
  return false;
294
- this.promptScrollOffset = 0;
295
202
  this.invalidate();
296
203
  this.tui.requestRender();
297
204
  return true;
298
205
  }
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
206
  handleInput(data) {
332
207
  if (matchesKey(data, this.contextPane.toggleKey()) && this.toggleContext()) {
333
208
  return;
334
209
  }
335
- if (this.handlePromptScrollInput(data)) {
336
- this.tui.requestRender();
337
- return;
338
- }
339
210
  if (this.mode === "freeform") {
340
211
  if (matchesKey(data, Key.escape)) {
341
212
  // Without options there is no list to go back to, so esc cancels.
@@ -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[], settings: PromptSettings, tui: TUI, theme: Theme, keybindings: KeybindingsManager, onDone: (answers: BatchAnswer[] | null) => void, heading?: string | undefined);
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;
@@ -37,6 +35,6 @@ export declare class BatchAskComponent implements Component {
37
35
  private unansweredCount;
38
36
  private handleReviewInput;
39
37
  private renderReview;
40
- /** Rows the review page may use, rails included. */
38
+ /** Rows the review page may use. */
41
39
  private reviewLineCap;
42
40
  }
@@ -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 { getOverlayMaxRenderLinesForRows, railRow } from "./prompt-layout";
5
+ import { getMaxRenderLinesForRows } 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, settings, tui, theme, keybindings, onDone, heading) {
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, settings, tui, theme, keybindings, (result) => this.handlePageDone(index, result)));
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() {
@@ -168,25 +166,17 @@ export class BatchAskComponent {
168
166
  const footerLines = [...(warning ? wrap(warning) : []), ...wrap(theme.fg("dim", hints))];
169
167
  // The title and the footer are fixed; the answers take what is left and are
170
168
  // cut from the end, so a short prompt still shows the first ones.
171
- const budget = Math.max(1, this.reviewLineCap() - 3 - footerLines.length);
172
- return [
173
- railRow(theme, width),
174
- theme.fg("dim", this.title),
175
- ...answerLines.slice(0, budget),
176
- ...footerLines,
177
- railRow(theme, width),
178
- ];
169
+ const budget = Math.max(1, this.reviewLineCap() - 1 - footerLines.length);
170
+ return [theme.fg("dim", this.title), ...answerLines.slice(0, budget), ...footerLines];
179
171
  }
180
- /** Rows the review page may use, rails included. */
172
+ /** Rows the review page may use. */
181
173
  reviewLineCap() {
182
174
  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
175
  // Inline prompts sit in Pi's fullscreen input dock, which clips anything
187
176
  // taller than what remains after the transcript's minimum row, the
188
177
  // working status, and the 2-3 row footer. Components only learn their
189
178
  // width, so stay within that space instead of growing with the content.
190
- return Math.max(4, Math.min(overlayCap, rows - INLINE_DOCK_RESERVED_ROWS));
179
+ const cap = getMaxRenderLinesForRows(rows);
180
+ return Math.max(4, Math.min(cap, rows - INLINE_DOCK_RESERVED_ROWS));
191
181
  }
192
182
  }
@@ -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 ASK_OVERLAY_MAX_HEIGHT_RATIO = 0.85;
5
- export declare const ASK_OVERLAY_MIN_RENDER_LINES = 8;
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 ASK_OVERLAY_MAX_HEIGHT_RATIO = 0.85;
5
- export const ASK_OVERLAY_MIN_RENDER_LINES = 8;
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";
@@ -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. */
@@ -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, displayMode, createComponent } = request;
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
- ...(options === undefined ? {} : { overlay: true, overlayOptions: options }),
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, whileBlocked } from "./custom-prompt";
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 { settings, createComponent } = request;
24
- return await whileBlocked(pi, () => runCustomPrompt(pi, ctx, {
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, settings, tui, theme, keybindings, complete),
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, settings, tui, theme, keybindings, complete, heading),
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
@@ -1,6 +1,4 @@
1
- import { registerAskUserSettings } from "./settings-provider";
2
1
  import { registerAskUserTool } from "./tool";
3
2
  export default function askUser(pi) {
4
3
  registerAskUserTool(pi);
5
- registerAskUserSettings(pi);
6
4
  }
@@ -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
  /**
@@ -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
  : [
@@ -1,43 +1,2 @@
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
- /** The native separator row: one full-width `─` rule in `borderMuted`. */
7
- export declare function railRow(theme: Theme, width: number): string;
8
- /** Rows an overlay may draw for the terminal height: a ratio cap with a usable floor. */
9
- export declare function getOverlayMaxRenderLinesForRows(rows: number): number;
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;
1
+ /** Rows a prompt may draw for the terminal height: a ratio cap with a usable floor. */
2
+ export declare function getMaxRenderLinesForRows(rows: number): number;
@@ -1,89 +1,9 @@
1
- import { ASK_OVERLAY_MAX_HEIGHT_RATIO, ASK_OVERLAY_MIN_RENDER_LINES, } from "./constants";
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;
9
- /** The native separator row: one full-width `─` rule in `borderMuted`. */
10
- export function railRow(theme, width) {
11
- return theme.fg("borderMuted", "─".repeat(Math.max(0, width)));
12
- }
13
- /** Rows an overlay may draw for the terminal height: a ratio cap with a usable floor. */
14
- export function getOverlayMaxRenderLinesForRows(rows) {
1
+ import { ASK_MAX_RENDER_LINES_RATIO, ASK_MIN_RENDER_LINES } from "./constants";
2
+ /** Rows a prompt may draw for the terminal height: a ratio cap with a usable floor. */
3
+ export function getMaxRenderLinesForRows(rows) {
15
4
  const normalizedRows = Number.isFinite(rows) ? Math.max(1, Math.floor(rows)) : 24;
16
5
  const availableRows = Math.max(1, normalizedRows - 2);
17
- const ratioRows = Math.max(1, Math.floor(normalizedRows * ASK_OVERLAY_MAX_HEIGHT_RATIO));
18
- const minimumRows = Math.min(ASK_OVERLAY_MIN_RENDER_LINES, availableRows);
6
+ const ratioRows = Math.max(1, Math.floor(normalizedRows * ASK_MAX_RENDER_LINES_RATIO));
7
+ const minimumRows = Math.min(ASK_MIN_RENDER_LINES, availableRows);
19
8
  return Math.min(availableRows, Math.max(minimumRows, ratioRows));
20
9
  }
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.
@@ -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,6 +1,6 @@
1
1
  {
2
2
  "name": "@hheei/pi-ask-user",
3
- "version": "0.1.1",
3
+ "version": "0.3.0",
4
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
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -40,18 +40,16 @@
40
40
  "access": "public"
41
41
  },
42
42
  "dependencies": {
43
- "@hheei/pi-ext-core": "^0.8.0"
43
+ "@hheei/pi-ext-core": "^0.8.1"
44
44
  },
45
45
  "peerDependencies": {
46
- "@earendil-works/pi-ai": ">=1.2.0",
47
- "@earendil-works/pi-coding-agent": ">=1.2.0",
48
- "@earendil-works/pi-tui": ">=1.2.0",
46
+ "@earendil-works/pi-coding-agent": ">=1.5.0",
47
+ "@earendil-works/pi-tui": ">=1.5.0",
49
48
  "typebox": "*"
50
49
  },
51
50
  "devDependencies": {
52
- "@earendil-works/pi-ai": "^1.4.0",
53
- "@earendil-works/pi-coding-agent": "^1.4.0",
54
- "@earendil-works/pi-tui": "^1.4.0"
51
+ "@earendil-works/pi-coding-agent": "^1.5.0",
52
+ "@earendil-works/pi-tui": "^1.5.0"
55
53
  },
56
54
  "scripts": {
57
55
  "build": "node -e \"import { rmSync } from 'node:fs'; rmSync('dist', { recursive: true, force: true })\" && tsc -p tsconfig.build.json"
@@ -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.
@@ -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>;
@@ -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[];
@@ -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
- }