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 +23 -3
- package/index.ts +93 -38
- package/package.json +1 -1
- package/skills/ask-user/SKILL.md +1 -1
- package/skills/ask-user/references/ask-user-skill-extension-spec.md +13 -0
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
|
-
-
|
|
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
|
|
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
|
|
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
|
|
1401
|
-
(
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
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
package/skills/ask-user/SKILL.md
CHANGED
|
@@ -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
|