pi-ask-user 0.2.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 +45 -0
- package/index.ts +97 -19
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -14,6 +14,11 @@ High-quality video: [ask-user-demo.mp4](./media/ask-user-demo.mp4)
|
|
|
14
14
|
- Multi-select option lists
|
|
15
15
|
- Optional freeform responses
|
|
16
16
|
- Context display support
|
|
17
|
+
- Overlay mode — dialog floats over conversation, preserving context
|
|
18
|
+
- Custom TUI rendering for tool calls and results
|
|
19
|
+
- System prompt integration via `promptSnippet` and `promptGuidelines`
|
|
20
|
+
- Optional timeout for auto-dismiss (fallback input mode)
|
|
21
|
+
- Structured `details` on all results for session state reconstruction
|
|
17
22
|
- Graceful fallback when interactive UI is unavailable
|
|
18
23
|
- Bundled `ask-user` skill for mandatory decision-gating in high-stakes or ambiguous tasks
|
|
19
24
|
|
|
@@ -46,6 +51,17 @@ The registered tool name is:
|
|
|
46
51
|
|
|
47
52
|
- `ask_user`
|
|
48
53
|
|
|
54
|
+
## Parameters
|
|
55
|
+
|
|
56
|
+
| Parameter | Type | Default | Description |
|
|
57
|
+
|-----------|------|---------|-------------|
|
|
58
|
+
| `question` | `string` | *required* | The question to ask the user |
|
|
59
|
+
| `context` | `string?` | — | Relevant context summary shown before the question |
|
|
60
|
+
| `options` | `(string \| {title, description?})[]?` | `[]` | Multiple-choice options |
|
|
61
|
+
| `allowMultiple` | `boolean?` | `false` | Enable multi-select mode |
|
|
62
|
+
| `allowFreeform` | `boolean?` | `true` | Add a "Type something" freeform option |
|
|
63
|
+
| `timeout` | `number?` | — | Auto-dismiss after N ms (applies to fallback input mode) |
|
|
64
|
+
|
|
49
65
|
## Example usage shape
|
|
50
66
|
|
|
51
67
|
```json
|
|
@@ -60,3 +76,32 @@ The registered tool name is:
|
|
|
60
76
|
"allowFreeform": true
|
|
61
77
|
}
|
|
62
78
|
```
|
|
79
|
+
|
|
80
|
+
## Result details
|
|
81
|
+
|
|
82
|
+
All tool results include a structured `details` object for rendering and session state reconstruction:
|
|
83
|
+
|
|
84
|
+
```typescript
|
|
85
|
+
interface AskToolDetails {
|
|
86
|
+
question: string;
|
|
87
|
+
context?: string;
|
|
88
|
+
options: QuestionOption[];
|
|
89
|
+
answer: string | null;
|
|
90
|
+
cancelled: boolean;
|
|
91
|
+
wasCustom?: boolean;
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Changelog
|
|
96
|
+
|
|
97
|
+
### 0.3.0
|
|
98
|
+
|
|
99
|
+
- Added `promptSnippet` and `promptGuidelines` for better LLM tool selection in the system prompt
|
|
100
|
+
- Added `renderCall` and `renderResult` for custom TUI rendering (compact tool call display, ✓/Cancelled result indicators)
|
|
101
|
+
- Added overlay mode — dialog now floats over the conversation instead of clearing the screen
|
|
102
|
+
- Added `timeout` parameter for auto-dismiss in fallback input mode (when no options are provided)
|
|
103
|
+
- Added structured `details` (`AskToolDetails`) to all result paths for session state reconstruction and branching support
|
|
104
|
+
|
|
105
|
+
### 0.2.1
|
|
106
|
+
|
|
107
|
+
- Initial public release
|
package/index.ts
CHANGED
|
@@ -37,6 +37,16 @@ interface AskParams {
|
|
|
37
37
|
options?: AskOptionInput[];
|
|
38
38
|
allowMultiple?: boolean;
|
|
39
39
|
allowFreeform?: boolean;
|
|
40
|
+
timeout?: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface AskToolDetails {
|
|
44
|
+
question: string;
|
|
45
|
+
context?: string;
|
|
46
|
+
options: QuestionOption[];
|
|
47
|
+
answer: string | null;
|
|
48
|
+
cancelled: boolean;
|
|
49
|
+
wasCustom?: boolean;
|
|
40
50
|
}
|
|
41
51
|
|
|
42
52
|
function normalizeOptions(options: AskOptionInput[]): QuestionOption[] {
|
|
@@ -534,6 +544,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
534
544
|
label: "Ask User",
|
|
535
545
|
description:
|
|
536
546
|
"Ask the user a question with optional multiple-choice answers. Use this to gather information interactively. Before calling, gather context with tools (read/exa/ref) and pass a short summary via the context field.",
|
|
547
|
+
promptSnippet:
|
|
548
|
+
"Ask the user a question with optional multiple-choice answers to gather information interactively",
|
|
549
|
+
promptGuidelines: [
|
|
550
|
+
"Before calling ask_user, gather context with tools (read/exa/ref) and pass a short summary via the context field.",
|
|
551
|
+
"Use ask_user when the user's intent is ambiguous, when a decision requires explicit user input, or when multiple valid options exist.",
|
|
552
|
+
],
|
|
537
553
|
parameters: Type.Object({
|
|
538
554
|
question: Type.String({ description: "The question to ask the user" }),
|
|
539
555
|
context: Type.Optional(
|
|
@@ -561,11 +577,17 @@ export default function (pi: ExtensionAPI) {
|
|
|
561
577
|
allowFreeform: Type.Optional(
|
|
562
578
|
Type.Boolean({ description: "Add a freeform text option. Default: true" }),
|
|
563
579
|
),
|
|
580
|
+
timeout: Type.Optional(
|
|
581
|
+
Type.Number({ description: "Auto-dismiss after N milliseconds (applies to fallback input mode when no options are provided)" }),
|
|
582
|
+
),
|
|
564
583
|
}),
|
|
565
584
|
|
|
566
585
|
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
567
586
|
if (signal?.aborted) {
|
|
568
|
-
return {
|
|
587
|
+
return {
|
|
588
|
+
content: [{ type: "text", text: "Cancelled" }],
|
|
589
|
+
details: { question: params.question, options: [], answer: null, cancelled: true } as AskToolDetails,
|
|
590
|
+
};
|
|
569
591
|
}
|
|
570
592
|
|
|
571
593
|
const {
|
|
@@ -574,6 +596,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
574
596
|
options: rawOptions = [],
|
|
575
597
|
allowMultiple = false,
|
|
576
598
|
allowFreeform = true,
|
|
599
|
+
timeout,
|
|
577
600
|
} = params as AskParams;
|
|
578
601
|
const options = normalizeOptions(rawOptions);
|
|
579
602
|
const normalizedContext = context?.trim() || undefined;
|
|
@@ -590,36 +613,45 @@ export default function (pi: ExtensionAPI) {
|
|
|
590
613
|
},
|
|
591
614
|
],
|
|
592
615
|
isError: true,
|
|
593
|
-
details: { question, context: normalizedContext, options },
|
|
616
|
+
details: { question, context: normalizedContext, options, answer: null, cancelled: true } as AskToolDetails,
|
|
594
617
|
};
|
|
595
618
|
}
|
|
596
619
|
|
|
597
620
|
// If no options provided, fall back to freeform input prompt.
|
|
598
621
|
if (options.length === 0) {
|
|
599
622
|
const prompt = normalizedContext ? `${question}\n\nContext:\n${normalizedContext}` : question;
|
|
600
|
-
const answer = await ctx.ui.input(prompt, "Type your answer...");
|
|
623
|
+
const answer = await ctx.ui.input(prompt, "Type your answer...", timeout ? { timeout } : undefined);
|
|
601
624
|
|
|
602
625
|
if (!answer) {
|
|
603
|
-
return {
|
|
626
|
+
return {
|
|
627
|
+
content: [{ type: "text", text: "User cancelled the question" }],
|
|
628
|
+
details: { question, context: normalizedContext, options, answer: null, cancelled: true } as AskToolDetails,
|
|
629
|
+
};
|
|
604
630
|
}
|
|
605
631
|
|
|
606
|
-
return {
|
|
632
|
+
return {
|
|
633
|
+
content: [{ type: "text", text: `User answered: ${answer}` }],
|
|
634
|
+
details: { question, context: normalizedContext, options, answer, cancelled: false, wasCustom: true } as AskToolDetails,
|
|
635
|
+
};
|
|
607
636
|
}
|
|
608
637
|
|
|
609
638
|
let result: string | null;
|
|
610
639
|
try {
|
|
611
|
-
result = await ctx.ui.custom<string | null>(
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
640
|
+
result = await ctx.ui.custom<string | null>(
|
|
641
|
+
(tui, theme, _kb, done) => {
|
|
642
|
+
return new AskComponent(
|
|
643
|
+
question,
|
|
644
|
+
normalizedContext,
|
|
645
|
+
options,
|
|
646
|
+
allowMultiple,
|
|
647
|
+
allowFreeform,
|
|
648
|
+
tui,
|
|
649
|
+
theme,
|
|
650
|
+
done,
|
|
651
|
+
);
|
|
652
|
+
},
|
|
653
|
+
{ overlay: true },
|
|
654
|
+
);
|
|
623
655
|
} catch (error) {
|
|
624
656
|
const message =
|
|
625
657
|
error instanceof Error ? `${error.message}\n${error.stack ?? ""}` : String(error);
|
|
@@ -631,10 +663,56 @@ export default function (pi: ExtensionAPI) {
|
|
|
631
663
|
}
|
|
632
664
|
|
|
633
665
|
if (result === null) {
|
|
634
|
-
return {
|
|
666
|
+
return {
|
|
667
|
+
content: [{ type: "text", text: "User cancelled the question" }],
|
|
668
|
+
details: { question, context: normalizedContext, options, answer: null, cancelled: true } as AskToolDetails,
|
|
669
|
+
};
|
|
635
670
|
}
|
|
636
671
|
|
|
637
|
-
return {
|
|
672
|
+
return {
|
|
673
|
+
content: [{ type: "text", text: `User answered: ${result}` }],
|
|
674
|
+
details: { question, context: normalizedContext, options, answer: result, cancelled: false } as AskToolDetails,
|
|
675
|
+
};
|
|
676
|
+
},
|
|
677
|
+
|
|
678
|
+
renderCall(args, theme) {
|
|
679
|
+
const question = (args.question as string) || "";
|
|
680
|
+
const rawOptions = Array.isArray(args.options) ? args.options : [];
|
|
681
|
+
let text = theme.fg("toolTitle", theme.bold("ask_user "));
|
|
682
|
+
text += theme.fg("muted", question);
|
|
683
|
+
if (rawOptions.length > 0) {
|
|
684
|
+
const labels = rawOptions.map((o: unknown) =>
|
|
685
|
+
typeof o === "string" ? o : (o as QuestionOption)?.title ?? "",
|
|
686
|
+
);
|
|
687
|
+
text += "\n" + theme.fg("dim", ` ${rawOptions.length} option(s): ${labels.join(", ")}`);
|
|
688
|
+
}
|
|
689
|
+
if (args.allowMultiple) {
|
|
690
|
+
text += theme.fg("dim", " [multi-select]");
|
|
691
|
+
}
|
|
692
|
+
return new Text(text, 0, 0);
|
|
693
|
+
},
|
|
694
|
+
|
|
695
|
+
renderResult(result, _options, theme) {
|
|
696
|
+
const details = result.details as (AskToolDetails & { error?: string }) | undefined;
|
|
697
|
+
|
|
698
|
+
// Error state
|
|
699
|
+
if (details?.error) {
|
|
700
|
+
return new Text(theme.fg("error", `✗ ${details.error}`), 0, 0);
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
// Cancelled / no details
|
|
704
|
+
if (!details || details.cancelled) {
|
|
705
|
+
return new Text(theme.fg("warning", "Cancelled"), 0, 0);
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
// Success
|
|
709
|
+
const answer = details.answer ?? "";
|
|
710
|
+
let text = theme.fg("success", "✓ ");
|
|
711
|
+
if (details.wasCustom) {
|
|
712
|
+
text += theme.fg("muted", "(wrote) ");
|
|
713
|
+
}
|
|
714
|
+
text += theme.fg("accent", answer);
|
|
715
|
+
return new Text(text, 0, 0);
|
|
638
716
|
},
|
|
639
717
|
});
|
|
640
718
|
}
|