pi-ask-user 0.6.1 → 0.7.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
@@ -16,7 +16,7 @@ High-quality video: [ask-user-demo.mp4](./media/ask-user-demo.mp4)
16
16
  - Optional freeform responses
17
17
  - User-toggleable extra context on structured selections
18
18
  - Context display support
19
- - Overlay mode dialog floats over conversation, preserving context
19
+ - Configurable display mode: `overlay` (modal, default) or `inline` (rendered directly in the flow)
20
20
  - Pi-TUI-aligned keybinding and editor behavior
21
21
  - Custom TUI rendering for tool calls and results
22
22
  - System prompt integration via `promptSnippet` and `promptGuidelines`
@@ -63,7 +63,8 @@ The registered tool name is:
63
63
  | `options` | `(string \| {title, description?})[]?` | `[]` | Multiple-choice options |
64
64
  | `allowMultiple` | `boolean?` | `false` | Enable multi-select mode |
65
65
  | `allowFreeform` | `boolean?` | `true` | Add a "Type something" freeform option |
66
- | `allowComment` | `boolean?` | `false` | Expose a user-toggleable extra-context option in the overlay (`ctrl+g` or the toggle row) and collect an optional comment in fallback dialogs |
66
+ | `allowComment` | `boolean?` | `false` | Expose a user-toggleable extra-context option in the custom UI (`ctrl+g` or the toggle row) and collect an optional comment in fallback dialogs |
67
+ | `displayMode` | `"overlay" \| "inline"?` | env var or `"overlay"` | Controls custom UI rendering: `overlay` shows the centered modal (current behavior), `inline` renders without overlay framing |
67
68
  | `timeout` | `number?` | — | Auto-dismiss after N ms and return `null` if the prompt times out |
68
69
 
69
70
  ## Example usage shape
@@ -78,10 +79,29 @@ The registered tool name is:
78
79
  ],
79
80
  "allowMultiple": false,
80
81
  "allowFreeform": true,
81
- "allowComment": true
82
+ "allowComment": true,
83
+ "displayMode": "inline"
82
84
  }
83
85
  ```
84
86
 
87
+ `displayMode: "inline"` uses the same interaction logic but skips overlay mode when calling `ctx.ui.custom(...)`. RPC/headless fallback behavior is unchanged.
88
+
89
+ ## Personal display mode preference
90
+
91
+ Set the `PI_ASK_USER_DISPLAY_MODE` environment variable to configure your preferred default globally. Add it to your shell profile (`~/.zshrc`, `~/.bash_profile`, etc.):
92
+
93
+ ```bash
94
+ export PI_ASK_USER_DISPLAY_MODE=inline
95
+ ```
96
+
97
+ Effective behavior order:
98
+
99
+ 1. Per-call `displayMode` parameter (if provided)
100
+ 2. `PI_ASK_USER_DISPLAY_MODE` environment variable (if set to `"overlay"` or `"inline"`)
101
+ 3. Fallback default: `"overlay"`
102
+
103
+ Unrecognised values are silently ignored and fall back to `"overlay"`.
104
+
85
105
  ## Result details
86
106
 
87
107
  All tool results include a structured `details` object for rendering and session state reconstruction:
package/index.ts CHANGED
@@ -7,7 +7,7 @@
7
7
 
8
8
  import type { ExtensionAPI, Theme } from "@mariozechner/pi-coding-agent";
9
9
  import { getMarkdownTheme } from "@mariozechner/pi-coding-agent";
10
- import { Type } from "@sinclair/typebox";
10
+ import { Type, type TUnsafe } from "@sinclair/typebox";
11
11
  import {
12
12
  Container,
13
13
  type Component,
@@ -33,8 +33,28 @@ import { createRequire } from "node:module";
33
33
  const _require = createRequire(import.meta.url);
34
34
  const ASK_USER_VERSION: string = (_require("./package.json") as { version: string }).version;
35
35
 
36
+ /**
37
+ * Emit a flat `{ type: "string", enum: [...] }` JSON Schema instead of the
38
+ * `anyOf`/`oneOf` shape that `Type.Union([Type.Literal()])` produces. Google's
39
+ * function-calling API rejects the union form. Local copy of pi-ai's StringEnum
40
+ * to avoid a peer dependency for one helper.
41
+ */
42
+ function StringEnum<const T extends readonly string[]>(
43
+ values: T,
44
+ options?: { description?: string; default?: T[number] },
45
+ ): TUnsafe<T[number]> {
46
+ return Type.Unsafe<T[number]>({
47
+ type: "string",
48
+ enum: [...values],
49
+ ...(options?.description ? { description: options.description } : {}),
50
+ ...(options?.default !== undefined ? { default: options.default } : {}),
51
+ });
52
+ }
53
+
36
54
  type AskOptionInput = QuestionOption | string;
37
55
 
56
+ type AskDisplayMode = "overlay" | "inline";
57
+
38
58
  interface AskParams {
39
59
  question: string;
40
60
  context?: string;
@@ -42,6 +62,7 @@ interface AskParams {
42
62
  allowMultiple?: boolean;
43
63
  allowFreeform?: boolean;
44
64
  allowComment?: boolean;
65
+ displayMode?: AskDisplayMode;
45
66
  timeout?: number;
46
67
  }
47
68
 
@@ -239,6 +260,38 @@ const SINGLE_SELECT_SPLIT_PANE_SEPARATOR = " │ ";
239
260
  const FREEFORM_SENTINEL = "\u270f\ufe0f Type custom response...";
240
261
  const COMMENT_TOGGLE_LABEL = "Add extra context after selection";
241
262
 
263
+ function buildCustomUIOptions(displayMode: AskDisplayMode) {
264
+ switch (displayMode) {
265
+ case "inline":
266
+ return undefined;
267
+ case "overlay":
268
+ return {
269
+ overlay: true,
270
+ overlayOptions: {
271
+ anchor: "center" as const,
272
+ width: ASK_OVERLAY_WIDTH,
273
+ minWidth: ASK_OVERLAY_MIN_WIDTH,
274
+ maxHeight: "85%",
275
+ margin: 1,
276
+ },
277
+ };
278
+ default: {
279
+ const _exhaustive: never = displayMode;
280
+ void _exhaustive;
281
+ return {
282
+ overlay: true,
283
+ overlayOptions: {
284
+ anchor: "center" as const,
285
+ width: ASK_OVERLAY_WIDTH,
286
+ minWidth: ASK_OVERLAY_MIN_WIDTH,
287
+ maxHeight: "85%",
288
+ margin: 1,
289
+ },
290
+ };
291
+ }
292
+ }
293
+ }
294
+
242
295
  class MultiSelectList implements Component {
243
296
  private options: QuestionOption[];
244
297
  private allowFreeform: boolean;
@@ -1331,6 +1384,11 @@ export default function(pi: ExtensionAPI) {
1331
1384
  allowComment: Type.Optional(
1332
1385
  Type.Boolean({ description: "Collect an optional comment after selecting one or more options. Default: false" }),
1333
1386
  ),
1387
+ displayMode: Type.Optional(
1388
+ StringEnum(["overlay", "inline"] as const, {
1389
+ description: "UI rendering mode. 'overlay' shows a centered modal, 'inline' renders in-place. Default: PI_ASK_USER_DISPLAY_MODE env var if set, otherwise 'overlay'. Omit to respect the user's configured preference.",
1390
+ }),
1391
+ ),
1334
1392
  timeout: Type.Optional(
1335
1393
  Type.Number({ description: "Auto-dismiss after N milliseconds. Returns null (cancelled) when expired." }),
1336
1394
  ),
@@ -1351,8 +1409,13 @@ export default function(pi: ExtensionAPI) {
1351
1409
  allowMultiple = false,
1352
1410
  allowFreeform = true,
1353
1411
  allowComment = false,
1412
+ displayMode,
1354
1413
  timeout,
1355
1414
  } = params as AskParams;
1415
+ const envMode = process.env.PI_ASK_USER_DISPLAY_MODE;
1416
+ const envDisplayMode: AskDisplayMode | undefined =
1417
+ envMode === "overlay" || envMode === "inline" ? envMode : undefined;
1418
+ const effectiveDisplayMode: AskDisplayMode = displayMode ?? envDisplayMode ?? "overlay";
1356
1419
  const options = normalizeOptions(rawOptions);
1357
1420
  const normalizedContext = context?.trim() || undefined;
1358
1421
 
@@ -1399,41 +1462,31 @@ export default function(pi: ExtensionAPI) {
1399
1462
 
1400
1463
  let result: AskUIResult | null;
1401
1464
  try {
1402
- const customResult = await ctx.ui.custom<AskUIResult | null>(
1403
- (tui, theme, keybindings, done) => {
1404
- if (signal) {
1405
- const onAbort = () => done(null);
1406
- signal.addEventListener("abort", onAbort, { once: true });
1407
- }
1408
-
1409
- if (timeout && timeout > 0) {
1410
- setTimeout(() => done(null), timeout);
1411
- }
1412
-
1413
- return new AskComponent(
1414
- question,
1415
- normalizedContext,
1416
- options,
1417
- allowMultiple,
1418
- allowFreeform,
1419
- allowComment,
1420
- tui,
1421
- theme,
1422
- keybindings,
1423
- done,
1424
- );
1425
- },
1426
- {
1427
- overlay: true,
1428
- overlayOptions: {
1429
- anchor: "center",
1430
- width: ASK_OVERLAY_WIDTH,
1431
- minWidth: ASK_OVERLAY_MIN_WIDTH,
1432
- maxHeight: "85%",
1433
- margin: 1,
1434
- },
1435
- },
1436
- );
1465
+ const customFactory = (tui: TUI, theme: Theme, keybindings: KeybindingsManager, done: (result: AskUIResult | null) => void) => {
1466
+ if (signal) {
1467
+ const onAbort = () => done(null);
1468
+ signal.addEventListener("abort", onAbort, { once: true });
1469
+ }
1470
+
1471
+ if (timeout && timeout > 0) {
1472
+ setTimeout(() => done(null), timeout);
1473
+ }
1474
+
1475
+ return new AskComponent(
1476
+ question,
1477
+ normalizedContext,
1478
+ options,
1479
+ allowMultiple,
1480
+ allowFreeform,
1481
+ allowComment,
1482
+ tui,
1483
+ theme,
1484
+ keybindings,
1485
+ done,
1486
+ );
1487
+ };
1488
+
1489
+ const customResult = await ctx.ui.custom<AskUIResult | null>(customFactory, buildCustomUIOptions(effectiveDisplayMode));
1437
1490
 
1438
1491
  if (customResult !== undefined) {
1439
1492
  result = customResult;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-ask-user",
3
- "version": "0.6.1",
3
+ "version": "0.7.0",
4
4
  "description": "Interactive ask_user tool for pi-coding-agent with searchable split-pane selection UI, multi-select, and freeform input",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -54,7 +54,7 @@ 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
-
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 the `PI_ASK_USER_DISPLAY_MODE` environment variable; only pass this when you intentionally want to override it for one call.
58
58
  ### 5) Commit the decision
59
59
  After response:
60
60
  - restate the decision in plain language
@@ -66,6 +66,19 @@ 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 (`PI_ASK_USER_DISPLAY_MODE` environment variable).
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
+
69
82
  ### Requirement-priority decision
70
83
 
71
84
  ```json