pi-ask-user 0.2.0 → 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 CHANGED
@@ -2,12 +2,23 @@
2
2
 
3
3
  A Pi package that adds an interactive `ask_user` tool for collecting user decisions during an agent run.
4
4
 
5
+ ## Demo
6
+
7
+ ![ask_user demo](./media/ask-user-demo.gif)
8
+
9
+ High-quality video: [ask-user-demo.mp4](./media/ask-user-demo.mp4)
10
+
5
11
  ## Features
6
12
 
7
13
  - Single-select option lists
8
14
  - Multi-select option lists
9
15
  - Optional freeform responses
10
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
11
22
  - Graceful fallback when interactive UI is unavailable
12
23
  - Bundled `ask-user` skill for mandatory decision-gating in high-stakes or ambiguous tasks
13
24
 
@@ -28,12 +39,6 @@ The skill follows a "decision handshake" flow:
28
39
 
29
40
  See: `skills/ask-user/references/ask-user-skill-extension-spec.md`.
30
41
 
31
- ## Demo
32
-
33
- ![ask_user demo](./media/ask-user-demo.gif)
34
-
35
- High-quality video: [ask-user-demo.mp4](./media/ask-user-demo.mp4)
36
-
37
42
  ## Install
38
43
 
39
44
  ```bash
@@ -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
@@ -61,3 +77,31 @@ The registered tool name is:
61
77
  }
62
78
  ```
63
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.0",
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": [
@@ -2,72 +2,13 @@
2
2
 
3
3
  ## Purpose
4
4
 
5
- Define how the `ask-user` skill (prompt-time behavior) and the `ask_user` extension tool (runtime UI) cooperate to create reliable human-in-the-loop decisions.
5
+ This document defines a minimal decision-gating protocol for using the `ask-user` skill with the `ask_user` tool.
6
6
 
7
- This spec optimizes for:
8
- - explicit user control at decision boundaries
9
- - low-friction interactive UX
10
- - minimal context bloat (progressive disclosure)
11
- - deterministic behavior across high-stakes workflows
7
+ Goal: require explicit user decisions at high-impact or ambiguous boundaries before implementation continues.
12
8
 
13
9
  ---
14
10
 
15
- ## 1) 2026 Pattern Alignment
16
-
17
- This design intentionally follows three emerging patterns used by modern agent systems.
18
-
19
- ### A. Progressive Disclosure
20
-
21
- The skill uses layered loading:
22
-
23
- 1. **Metadata layer** (`name`, `description`) always in prompt.
24
- 2. **Core protocol layer** (`SKILL.md`) loaded only when relevant.
25
- 3. **Detailed reference layer** (`references/*.md`) loaded only when needed.
26
-
27
- Implication: keep `SKILL.md` concise and decision-focused; move large examples and policy details into references.
28
-
29
- ### B. Agent Protocol Handshakes
30
-
31
- High-impact actions require a handshake:
32
-
33
- `detect boundary -> summarize evidence -> ask -> explicit user choice -> commit -> execute`
34
-
35
- The handshake prevents implicit assumptions from silently becoming implementation decisions.
36
-
37
- ### C. Context-Aware Loading
38
-
39
- Questions must be formed from current evidence (repo state, docs, logs, external research), not from generic prompts.
40
-
41
- The `context` field in `ask_user` is the transport for this condensed evidence.
42
-
43
- ---
44
-
45
- ## 2) System Model
46
-
47
- ### Components
48
-
49
- 1. **Skill layer (`skills/ask-user/SKILL.md`)**
50
- - decides *when* user interaction is mandatory
51
- - enforces handshake behavior
52
-
53
- 2. **Extension layer (`index.ts`, tool `ask_user`)**
54
- - renders question UX (single-select, multi-select, freeform)
55
- - returns normalized answer text to the agent
56
-
57
- 3. **Model runtime**
58
- - interprets skill guidance
59
- - calls `ask_user` with structured payload
60
- - resumes execution after explicit user response
61
-
62
- ### Contract boundary
63
-
64
- - Skill controls policy and decision gating.
65
- - Extension controls interaction mechanics.
66
- - Model must not bypass skill policy for high-stakes/ambiguous decisions.
67
-
68
- ---
69
-
70
- ## 3) Trigger Matrix (When to Call `ask_user`)
11
+ ## 1) Trigger Matrix (When to Call `ask_user`)
71
12
 
72
13
  | Scenario | Must Ask? | Why |
73
14
  |---|---:|---|
@@ -82,122 +23,35 @@ The `context` field in `ask_user` is the transport for this condensed evidence.
82
23
 
83
24
  ---
84
25
 
85
- ## 4) Handshake State Machine
86
-
87
- ```text
88
- DISCOVER -> CLASSIFY -> (CLEAR -> EXECUTE)
89
- -> (AMBIGUOUS/HIGH_STAKES -> EVIDENCE -> ASK -> WAIT -> COMMIT -> EXECUTE)
90
- ```
91
-
92
- ### State definitions
26
+ ## 2) Decision Handshake
93
27
 
94
- - **DISCOVER**: inspect task and current project state.
95
- - **CLASSIFY**: decide whether decision gate is required.
96
- - **EVIDENCE**: gather and compress decision context.
97
- - **ASK**: invoke `ask_user` with a single focused decision.
98
- - **WAIT**: pause implementation until response arrives.
99
- - **COMMIT**: restate chosen option and intended next action.
100
- - **EXECUTE**: implement according to confirmed decision.
28
+ Use this protocol whenever the trigger matrix says to ask.
101
29
 
102
- ### Cancellation behavior
103
-
104
- If user cancels or response is unclear:
105
- - enforce a **max two-attempt budget** for the same decision boundary
106
- - attempt 1: normal structured question
107
- - attempt 2: narrower question with explicit recommendation + `Proceed with recommendation / Choose another / Stop`
108
-
109
- After attempt 2:
110
- - for `high_stakes` or `both`: do not continue; report blocked status
111
- - for `ambiguous` only: proceed only when user delegates choice (e.g., "your call"), using the most reversible default and explicit assumptions
112
-
113
- ---
114
-
115
- ## 5) `ask_user` Payload Design Standard
116
-
117
- ### Field mapping
118
-
119
- | Field | Required | Rule |
120
- |---|---:|---|
121
- | `question` | Yes | One decision only, concrete and action-bound |
122
- | `context` | Recommended | 3-7 bullets or short paragraph with evidence and trade-offs |
123
- | `options` | Optional | Prefer 2-5 choices when stable alternatives exist |
124
- | `allowMultiple` | Optional | `true` only for independent selections |
125
- | `allowFreeform` | Optional | Usually `true`; set `false` only when strict menu required |
30
+ 1. **Detect boundary**
31
+ - classify as `high_stakes`, `ambiguous`, `both`, or `clear`
32
+ 2. **Gather evidence**
33
+ - read code/docs/logs first; do not ask blindly
34
+ 3. **Summarize context**
35
+ - prepare concise trade-off context (3–7 bullets or short paragraph)
36
+ 4. **Ask one focused question**
37
+ - call `ask_user` for one decision at a time
38
+ 5. **Commit and proceed**
39
+ - restate chosen option and implement accordingly
126
40
 
127
- ### Style rules
41
+ ### Retry/cancel policy
128
42
 
129
- - Keep options concise, decision-oriented, and contrastive.
130
- - Include brief descriptions for non-obvious trade-offs.
131
- - Avoid stacking unrelated questions.
132
- - Ask after evidence gathering, not before.
43
+ - Max **2** `ask_user` attempts for the same decision boundary.
44
+ - Attempt 1: normal structured question.
45
+ - Attempt 2: narrower question with recommendation and explicit options.
46
+ - After attempt 2:
47
+ - `high_stakes` / `both`: stop and report blocked.
48
+ - `ambiguous` only: proceed only if user delegates (e.g., “your call”), using the most reversible default.
133
49
 
134
50
  ---
135
51
 
136
- ## 6) UX Guidance for Best Outcomes
52
+ ## 3) Example Payloads
137
53
 
138
- ### Good interaction shape
139
-
140
- 1. Agent summarizes known constraints.
141
- 2. Agent asks one clear question.
142
- 3. User selects an option quickly (or writes freeform).
143
- 4. Agent confirms and proceeds.
144
-
145
- ### Avoid
146
-
147
- - long speculative context dumps
148
- - “What do you want?” without options
149
- - repeated confirmation of unchanged decisions
150
- - more than two attempts for the same decision boundary
151
- - hidden assumptions after user response
152
-
153
- ### Recommended defaults
154
-
155
- - `allowFreeform: true`
156
- - `allowMultiple: false`
157
- - `options`: include concise titles + descriptions for trade-offs
158
-
159
- ---
160
-
161
- ## 7) Runtime and Fallback Semantics
162
-
163
- The extension already provides these behavior guarantees:
164
-
165
- 1. **Interactive mode with UI**
166
- - single-select list, multi-select list, or freeform editor
167
-
168
- 2. **No options provided**
169
- - freeform input prompt is used
170
-
171
- 3. **No interactive UI available**
172
- - tool returns an error-style textual fallback that includes question/context/options
173
-
174
- Design implication:
175
- - skill should prefer structured options when ambiguity is high
176
- - but always permit freeform for unanticipated requirements
177
-
178
- ---
179
-
180
- ## 8) Quality Rubric
181
-
182
- A decision-gated interaction is successful when all are true:
183
-
184
- - [ ] High-stakes or ambiguous boundary was detected
185
- - [ ] Context was gathered before asking
186
- - [ ] At most two decision questions were asked for the same boundary (normally one)
187
- - [ ] User response was explicit
188
- - [ ] Agent restated decision before execution
189
- - [ ] Implementation followed the selected path
190
-
191
- Failure signals:
192
- - agent made architectural choice without user decision
193
- - question lacked trade-off context
194
- - user answer ignored or overwritten
195
-
196
- ---
197
-
198
- ## 9) Example Protocol Templates
199
-
200
- ### Template: architecture fork
54
+ ### Architecture decision
201
55
 
202
56
  ```json
203
57
  {
@@ -212,7 +66,7 @@ Failure signals:
212
66
  }
213
67
  ```
214
68
 
215
- ### Template: ambiguity cleanup
69
+ ### Requirement-priority decision
216
70
 
217
71
  ```json
218
72
  {
@@ -227,24 +81,3 @@ Failure signals:
227
81
  "allowFreeform": true
228
82
  }
229
83
  ```
230
-
231
- ---
232
-
233
- ## 10) Packaging + Distribution Notes
234
-
235
- To ship this skill with the extension package:
236
-
237
- - include `skills/` in npm package files
238
- - register skills in `package.json` under `pi.skills`
239
-
240
- This ensures the skill is discoverable at startup and available for implicit invocation.
241
-
242
- ---
243
-
244
- ## 11) Future Enhancements (Optional)
245
-
246
- - **Decision memory log**: append chosen options to a lightweight session decision ledger.
247
- - **Adaptive option generation**: tailor options by repo language/framework context.
248
- - **Confidence thresholding**: auto-trigger ask gate when model confidence in requirements is low.
249
-
250
- These are optional and not required for initial rollout.