pi-ask-user 0.6.0 → 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;
@@ -1292,12 +1345,14 @@ export default function(pi: ExtensionAPI) {
1292
1345
  name: "ask_user",
1293
1346
  label: "Ask User",
1294
1347
  description:
1295
- "Ask the user a question with optional multiple-choice answers. Use this to gather information interactively. Before calling, gather context with tools (read/web/ref) and pass a short summary via the context field.",
1348
+ "Ask the user a question with optional multiple-choice answers. Use this to gather information interactively. Ask exactly one focused question per call. Before calling, gather context with tools (read/web/ref) and pass a short summary via the context field.",
1296
1349
  promptSnippet:
1297
- "Ask the user a question with optional multiple-choice answers to gather information interactively",
1350
+ "Ask the user one focused question with optional multiple-choice answers to gather information interactively",
1298
1351
  promptGuidelines: [
1299
1352
  "Before calling ask_user, gather context with tools (read/web/ref) and pass a short summary via the context field.",
1300
1353
  "Use ask_user when the user's intent is ambiguous, when a decision requires explicit user input, or when multiple valid options exist.",
1354
+ "Ask exactly one focused question per ask_user call.",
1355
+ "Do not combine multiple numbered, multipart, or unrelated questions into one ask_user prompt.",
1301
1356
  ],
1302
1357
  parameters: Type.Object({
1303
1358
  question: Type.String({ description: "The question to ask the user" }),
@@ -1329,6 +1384,11 @@ export default function(pi: ExtensionAPI) {
1329
1384
  allowComment: Type.Optional(
1330
1385
  Type.Boolean({ description: "Collect an optional comment after selecting one or more options. Default: false" }),
1331
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
+ ),
1332
1392
  timeout: Type.Optional(
1333
1393
  Type.Number({ description: "Auto-dismiss after N milliseconds. Returns null (cancelled) when expired." }),
1334
1394
  ),
@@ -1349,8 +1409,13 @@ export default function(pi: ExtensionAPI) {
1349
1409
  allowMultiple = false,
1350
1410
  allowFreeform = true,
1351
1411
  allowComment = false,
1412
+ displayMode,
1352
1413
  timeout,
1353
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";
1354
1419
  const options = normalizeOptions(rawOptions);
1355
1420
  const normalizedContext = context?.trim() || undefined;
1356
1421
 
@@ -1397,41 +1462,31 @@ export default function(pi: ExtensionAPI) {
1397
1462
 
1398
1463
  let result: AskUIResult | null;
1399
1464
  try {
1400
- const customResult = await ctx.ui.custom<AskUIResult | null>(
1401
- (tui, theme, keybindings, done) => {
1402
- if (signal) {
1403
- const onAbort = () => done(null);
1404
- signal.addEventListener("abort", onAbort, { once: true });
1405
- }
1406
-
1407
- if (timeout && timeout > 0) {
1408
- setTimeout(() => done(null), timeout);
1409
- }
1410
-
1411
- return new AskComponent(
1412
- question,
1413
- normalizedContext,
1414
- options,
1415
- allowMultiple,
1416
- allowFreeform,
1417
- allowComment,
1418
- tui,
1419
- theme,
1420
- keybindings,
1421
- done,
1422
- );
1423
- },
1424
- {
1425
- overlay: true,
1426
- overlayOptions: {
1427
- anchor: "center",
1428
- width: ASK_OVERLAY_WIDTH,
1429
- minWidth: ASK_OVERLAY_MIN_WIDTH,
1430
- maxHeight: "85%",
1431
- margin: 1,
1432
- },
1433
- },
1434
- );
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));
1435
1490
 
1436
1491
  if (customResult !== undefined) {
1437
1492
  result = customResult;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-ask-user",
3
- "version": "0.6.0",
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