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 +23 -3
- package/index.ts +89 -36
- 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;
|
|
@@ -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
|
|
1403
|
-
(
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
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
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
|