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.
Files changed (3) hide show
  1. package/README.md +45 -0
  2. package/index.ts +97 -19
  3. 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 { content: [{ type: "text", text: "Cancelled" }] };
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 { content: [{ type: "text", text: "User cancelled the question" }] };
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 { content: [{ type: "text", text: `User answered: ${answer}` }] };
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>((tui, theme, _kb, done) => {
612
- return new AskComponent(
613
- question,
614
- normalizedContext,
615
- options,
616
- allowMultiple,
617
- allowFreeform,
618
- tui,
619
- theme,
620
- done,
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 { content: [{ type: "text", text: "User cancelled the question" }] };
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 { content: [{ type: "text", text: `User answered: ${result}` }] };
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-ask-user",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "Interactive ask_user tool for pi-coding-agent with multi-select and freeform input UI",
5
5
  "type": "module",
6
6
  "keywords": [