pi-ask-questions 0.0.0-stage → 0.2.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/CHANGELOG.md ADDED
@@ -0,0 +1,32 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.2.0] - 2026-10-15
9
+
10
+ ### Changed
11
+
12
+ - Removed decorative icons from tabs, option rows, the review tab, and tool output. Only the selection markers (`[x]` / `[ ]` / `(•)`) and the cursor remain.
13
+ - Option questions now hold at most one typed answer. The inline editor appears directly under the “Type something…” row with the options still visible, and moving onto that row starts editing immediately instead of opening a separate view.
14
+ - Free-form questions always resolve to a single typed answer; `multiple`, `min`, and `max` no longer apply when a question has no options.
15
+
16
+ ### Added
17
+
18
+ - `noWait` parameter: open the questionnaire in the background and return immediately, delivering the answers later as a follow-up message. Only one background questionnaire can be open at a time; it is cancelled safely on session shutdown.
19
+
20
+ ## [0.1.0] - 2026-10-15
21
+
22
+ ### Added
23
+
24
+ - `ask_user` tool with tabbed questionnaires, single-select and multi-select options, free-form typing, and multiple typed answers per question.
25
+ - Validated answers: required questions, `min`/`max` selection counts, optional questions that can be skipped, and preselected defaults.
26
+ - Review tab that summarizes all answers and allows jumping back to edit any question.
27
+ - Safe cancellation (Esc twice or Ctrl+C) and abort-signal support.
28
+ - Scrollable, width-aware rendering with option descriptions, placeholders, and status icons.
29
+ - Structured tool output (`structuredContent`) alongside a compact model-facing summary.
30
+ - RPC dialog fallback for modes without custom components, and a plain-text fallback for non-interactive modes.
31
+ - `/ask-demo` command to preview the questionnaire UI.
32
+ - Unit and interaction tests covering normalization, validation, formatting, rendering, and keyboard flows.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 thgilciffart
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,179 @@
1
- # Temporary Holding Version
1
+ # pi-ask-questions
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Interactive questionnaires for [Pi](https://pi.dev): the model runs an `ask_user` tool and you answer in a proper terminal UI — tabs, multi-select, free-form typing, validation, and a review step.
4
+
5
+ [![npm](https://img.shields.io/npm/v/pi-ask-questions.svg)](https://www.npmjs.com/package/pi-ask-questions)
6
+ [![ci](https://github.com/thgilciffart/pi-ask-questions/actions/workflows/ci.yml/badge.svg)](https://github.com/thgilciffart/pi-ask-questions/actions/workflows/ci.yml)
7
+
8
+ ```text
9
+ ────────────────────────────────────────────────────────────
10
+ ask_user demo
11
+ Scope Release Notes Submit
12
+ ────────────────────────────────────────────────────────────
13
+ Which parts of the project should this change touch?
14
+ Select one or more • Space toggles
15
+
16
+ ❯ [x] 1. Extension host
17
+ Tool registration and lifecycle code
18
+ [ ] 2. TUI component
19
+ The interactive questionnaire renderer
20
+ [ ] 3. Documentation
21
+ [x] 4. Tests
22
+ Type something…
23
+ ──────────────────────────────────────────────
24
+ custom answer
25
+ ──────────────────────────────────────────────
26
+
27
+ ↑↓ move • Space toggle • A all • C clear • 1-9 jump • Enter confirm
28
+ ────────────────────────────────────────────────────────────
29
+ ```
30
+
31
+ ## Install
32
+
33
+ ```bash
34
+ pi install npm:pi-ask-questions
35
+ ```
36
+
37
+ Try it for a single invocation without changing your settings:
38
+
39
+ ```bash
40
+ pi -e npm:pi-ask-questions
41
+ ```
42
+
43
+ Once installed, run `/ask-demo` to preview the UI, or just let the model call `ask_user` when it needs input.
44
+
45
+ ## Features
46
+
47
+ - **Tabs** — every question gets a tab, with answered state shown by color (green = answered, accent = partially answered, muted = untouched). Move with `Tab` / `Shift+Tab`.
48
+ - **Single-select** radio options with optional descriptions.
49
+ - **Multi-select** checkboxes (`multiple: true`), including `min` / `max` validation, `A` to select all, and `C` to clear.
50
+ - **Free-form typing** — a question without options is a plain text prompt. Option questions can also hold one typed answer unless `allowOther: false`.
51
+ - **Inline editing** — move the cursor onto “Type something…” and type; the editor appears directly under the row with the options still visible. No separate menu, and the answer can be edited again in place.
52
+ - **One typed answer per question** — typed answers are never duplicated; editing replaces the previous text.
53
+ - **Optional questions** (`required: false`) that can be skipped, plus preset answers (`selected: true`) and defaults.
54
+ - **Review tab** — see everything before submitting; jump back to any question to edit it.
55
+ - **Validation** — required answers and `min`/`max` selection counts are enforced before submit.
56
+ - **Background mode** — set `noWait: true` and the tool returns immediately while the questionnaire stays open; the answers arrive later as a follow-up message.
57
+ - **Safe cancel** — `Esc` arms cancellation, a second `Esc` or `Ctrl+C` cancels immediately.
58
+ - **Scrollable option lists** — long lists show “n more above / below” indicators.
59
+ - **Structured results** — the tool returns text for the model and a machine-readable `structuredContent` payload for codemode scripts.
60
+
61
+ ## How the model uses it
62
+
63
+ `ask_user` accepts one or more questions. A single-select question resolves immediately; everything else flows into tabs and a submit step.
64
+
65
+ ```json
66
+ {
67
+ "title": "Release planning",
68
+ "questions": [
69
+ {
70
+ "id": "scope",
71
+ "label": "Scope",
72
+ "question": "Which packages should the release include?",
73
+ "multiple": true,
74
+ "min": 1,
75
+ "options": [
76
+ { "value": "core", "label": "Core", "description": "Shared runtime code", "selected": true },
77
+ { "value": "cli", "label": "CLI" },
78
+ { "value": "docs", "label": "Docs" }
79
+ ]
80
+ },
81
+ {
82
+ "id": "version",
83
+ "label": "Version",
84
+ "question": "How should we version it?",
85
+ "options": [
86
+ { "value": "patch", "label": "Patch" },
87
+ { "value": "minor", "label": "Minor" },
88
+ { "value": "major", "label": "Major" }
89
+ ]
90
+ },
91
+ {
92
+ "id": "notes",
93
+ "label": "Notes",
94
+ "question": "Anything else I should know?",
95
+ "required": false,
96
+ "placeholder": "Type a note, or leave empty…"
97
+ }
98
+ ]
99
+ }
100
+ ```
101
+
102
+ The model receives a compact summary such as:
103
+
104
+ ```text
105
+ User answered:
106
+ Scope: selected "Core", "CLI", typed "shared types"
107
+ Version: selected "Minor"
108
+ Notes: (skipped)
109
+ ```
110
+
111
+ ### Parameters
112
+
113
+ | Field | Type | Description |
114
+ | --- | --- | --- |
115
+ | `title` | string | Optional title shown above the questionnaire. |
116
+ | `submitLabel` | string | Label of the submit entry. Default `Submit`. |
117
+ | `questions` | array | 1–12 questions, each rendered as a tab. |
118
+ | `questions[].id` | string | Stable id used in the result. Defaults to `q1`, `q2`, … |
119
+ | `questions[].label` | string | Short tab label. Defaults to `Q1`, `Q2`, … |
120
+ | `questions[].question` | string | The question text. |
121
+ | `questions[].header` | string | Optional context shown under the question. |
122
+ | `questions[].options` | array | Choices. Omit or leave empty for free-form text. |
123
+ | `questions[].options[].value` | string | Value returned when picked. Defaults to the label. |
124
+ | `questions[].options[].label` | string | Display label. |
125
+ | `questions[].options[].description` | string | Explanation shown below the label. |
126
+ | `questions[].options[].selected` | boolean | Preselect the option. |
127
+ | `questions[].multiple` | boolean | Allow more than one selection. |
128
+ | `questions[].allowOther` | boolean | Show the typed-answer entry. Default `true`. |
129
+ | `questions[].required` | boolean | Require an answer before submit. Default `true`. |
130
+ | `questions[].min` / `max` | integer | Selection bounds for multi-select. |
131
+ | `questions[].placeholder` | string | Placeholder for the free-form editor. |
132
+ | `questions[].otherLabel` | string | Custom label for the typed-answer entry. |
133
+ | `noWait` | boolean | Show the questionnaire in the background and return immediately (interactive TUI only). The answers arrive as a follow-up message. Only one background questionnaire can be open at a time. |
134
+
135
+ ### Keyboard reference
136
+
137
+ | Keys | Action |
138
+ | --- | --- |
139
+ | `Tab` / `Shift+Tab` | Switch tabs |
140
+ | `↑` / `↓`, `Home` / `End` | Move the option cursor |
141
+ | `Space` | Toggle a checkbox / select a radio option |
142
+ | `1`–`9` | Jump to an option (toggles it in multi-select) |
143
+ | `A` / `C` | Select all / clear (multi-select) |
144
+ | `Enter` | Confirm the question, or submit on the review tab |
145
+ | `Esc` | Arm cancel; `Esc` twice cancels |
146
+ | `Ctrl+C` | Cancel immediately |
147
+ | `\` + `Enter` | Newline in a free-form answer |
148
+
149
+ ## Modes
150
+
151
+ | Mode | Behavior |
152
+ | --- | --- |
153
+ | Interactive TUI | Full tabbed questionnaire. |
154
+ | Interactive TUI + `noWait` | Tool returns immediately; answers are delivered as a follow-up message. |
155
+ | RPC | Sequential fallback built from the dialog API (`select` / `input`). |
156
+ | Print / JSON | The tool reports that no UI is available and lists the questions so the model can ask them in plain text. |
157
+
158
+ ## Development
159
+
160
+ ```bash
161
+ npm install
162
+ npm run check # typecheck + unit tests
163
+ pi -e ./src/extension.ts
164
+ ```
165
+
166
+ The pure questionnaire model (`src/questions.ts`), the TUI component (`src/ui/ask-questionnaire.ts`), and the extension wiring are covered by unit and interaction tests that drive the component with raw key sequences.
167
+
168
+ ## Publishing
169
+
170
+ The package is published to npm as `pi-ask-questions` and carries the `pi-package` keyword, which makes it eligible for the [Pi package gallery](https://pi.dev/packages). `prepublishOnly` runs the full check suite.
171
+
172
+ ```bash
173
+ npm login
174
+ npm publish
175
+ ```
176
+
177
+ ## License
178
+
179
+ MIT
package/package.json CHANGED
@@ -1,6 +1,56 @@
1
1
  {
2
- "name": "pi-ask-questions",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
2
+ "name": "pi-ask-questions",
3
+ "version": "0.2.0",
4
+ "description": "Interactive questionnaires for Pi: tabs, multi-select, free-form typing, validation, and structured answers.",
5
+ "keywords": [
6
+ "pi-package",
7
+ "pi",
8
+ "pi-coding-agent",
9
+ "extension",
10
+ "questions",
11
+ "questionnaire",
12
+ "ask",
13
+ "multi-select",
14
+ "tui"
15
+ ],
16
+ "license": "MIT",
17
+ "author": "thgilciffart <73938431+thgilciffart@users.noreply.github.com>",
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/thgilciffart/pi-ask-questions.git"
21
+ },
22
+ "homepage": "https://github.com/thgilciffart/pi-ask-questions#readme",
23
+ "bugs": {
24
+ "url": "https://github.com/thgilciffart/pi-ask-questions/issues"
25
+ },
26
+ "type": "module",
27
+ "files": [
28
+ "src",
29
+ "README.md",
30
+ "CHANGELOG.md",
31
+ "LICENSE"
32
+ ],
33
+ "pi": {
34
+ "extensions": [
35
+ "./src/extension.ts"
36
+ ]
37
+ },
38
+ "scripts": {
39
+ "test": "node --test \"test/**/*.test.ts\"",
40
+ "typecheck": "tsc --noEmit",
41
+ "check": "npm run typecheck && npm test",
42
+ "prepublishOnly": "npm run check"
43
+ },
44
+ "peerDependencies": {
45
+ "@earendil-works/pi-coding-agent": "*",
46
+ "@earendil-works/pi-tui": "*",
47
+ "typebox": "*"
48
+ },
49
+ "devDependencies": {
50
+ "@earendil-works/pi-coding-agent": "^1.2.0",
51
+ "@earendil-works/pi-tui": "^1.2.0",
52
+ "@types/node": "^26.6.5",
53
+ "typebox": "^1.3.37",
54
+ "typescript": "^7.0.2"
55
+ }
56
+ }
@@ -0,0 +1,355 @@
1
+ /**
2
+ * pi-ask-questions
3
+ *
4
+ * Registers the `ask_user` tool: an interactive questionnaire with tabs,
5
+ * single- and multi-select options, free-form typing, validation, and
6
+ * structured answers. Also adds `/ask-demo` so the UI can be previewed
7
+ * without involving the model.
8
+ */
9
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
10
+ import { Text } from "@earendil-works/pi-tui";
11
+ import { runFallbackSurvey } from "./fallback.ts";
12
+ import {
13
+ type AskResult,
14
+ type NormalizedQuestion,
15
+ type QuestionAnswer,
16
+ DEFAULT_SUBMIT_LABEL,
17
+ buildResult,
18
+ formatAnswersText,
19
+ normalizeQuestions,
20
+ toStructuredOutput,
21
+ } from "./questions.ts";
22
+ import {
23
+ AskUserOutputSchema,
24
+ AskUserParamsSchema,
25
+ type AskUserParams,
26
+ } from "./schema.ts";
27
+ import { runQuestionnaire, type QuestionnaireContext } from "./ui/ask-questionnaire.ts";
28
+ /** Structured details stored with the tool result for rendering. */
29
+ export interface AskToolDetails {
30
+ cancelled: boolean;
31
+ /** True while a noWait questionnaire is still open in the background. */
32
+ pending?: boolean;
33
+ answers: QuestionAnswer[];
34
+ questions: NormalizedQuestion[];
35
+ }
36
+
37
+ const TOOL_DESCRIPTION = `Ask the user one or more questions and wait for their answers. Use it when you need a decision, preference, or piece of information that only the user can provide.
38
+
39
+ The interactive UI supports:
40
+ - Tabs: every question gets its own tab (Tab / Shift+Tab and arrow keys switch).
41
+ - Single-select and multi-select questions (set multiple: true).
42
+ - Free-form typing: a question with no options is a text prompt. Option questions can also hold one typed answer unless allowOther is false, and the answer is edited inline.
43
+ - Optional questions (required: false) that can be skipped, preselected options (selected: true), and min/max selection counts.
44
+ - Number keys 1-9 jump to options; A selects all; C clears a multi-select.
45
+ - Set noWait: true to open the questionnaire in the background: the tool returns immediately and the answers arrive later as a follow-up message.
46
+
47
+ Guidelines:
48
+ - Prefer this tool over asking questions in prose when you actually need the answer to proceed.
49
+ - Group related questions into one call (2-6 questions) instead of asking repeatedly.
50
+ - Provide concrete, labeled options whenever you can; use a free-form question only when the answer space is open.
51
+ - Put recommended options first.
52
+ - Use noWait only when you can keep working without the answer; otherwise omit it so the answers arrive in the same turn.`;
53
+
54
+ const HELP_TEXT = `Interactive UI is not available in this mode. Ask the user the following questions directly in your reply instead:
55
+ {{questions}}`;
56
+
57
+ /** Dispatch to the right UI for the current mode. */
58
+ async function askQuestions(
59
+ ctx: Pick<ExtensionContext, "ui" | "mode" | "hasUI">,
60
+ questions: NormalizedQuestion[],
61
+ options: { title?: string; submitLabel: string; signal?: AbortSignal },
62
+ ): Promise<AskResult> {
63
+ if (ctx.mode === "tui") {
64
+ return runQuestionnaire(ctx as QuestionnaireContext, questions, options);
65
+ }
66
+ if (ctx.hasUI) {
67
+ return runFallbackSurvey(ctx.ui, questions, options.signal);
68
+ }
69
+ throw new Error("no-ui");
70
+ }
71
+
72
+ /** Human-readable list used when no UI exists (print/JSON mode). */
73
+ function questionsAsText(questions: NormalizedQuestion[]): string {
74
+ return questions
75
+ .map((question, index) => {
76
+ const options =
77
+ question.options.length > 0
78
+ ? `\n Options: ${question.options.map((option) => option.label).join(", ")}`
79
+ : "";
80
+ const optional = question.required ? "" : " (optional)";
81
+ return `${index + 1}. ${question.question}${optional}${options}`;
82
+ })
83
+ .join("\n");
84
+ }
85
+
86
+ function resultContent(result: AskResult): string {
87
+ if (result.cancelled) {
88
+ return "The user cancelled the questionnaire before submitting.";
89
+ }
90
+ return `User answered:\n${formatAnswersText(result)}`;
91
+ }
92
+
93
+ /** Start the questionnaire and wait for the user. */
94
+ export default function askQuestionsExtension(pi: ExtensionAPI): void {
95
+ // A noWait questionnaire stays open after its tool call returns. It owns a
96
+ // session-scoped abort controller and reports its answers with a follow-up
97
+ // message instead of a tool result.
98
+ let backgroundController: AbortController | undefined;
99
+ let shuttingDown = false;
100
+
101
+ pi.on("session_start", () => {
102
+ shuttingDown = false;
103
+ });
104
+ pi.on("session_shutdown", () => {
105
+ shuttingDown = true;
106
+ backgroundController?.abort();
107
+ backgroundController = undefined;
108
+ });
109
+
110
+ pi.registerTool<typeof AskUserParamsSchema, AskToolDetails>({
111
+ name: "ask_user",
112
+ label: "Ask User",
113
+ description: TOOL_DESCRIPTION,
114
+ promptSnippet:
115
+ "ask_user: ask the user structured questions in an interactive questionnaire (tabs, multi-select, free-form).",
116
+ promptGuidelines: [
117
+ "Use ask_user when you need a decision or missing detail from the user; batch related questions into one call instead of asking repeatedly.",
118
+ "Give ask_user questions labeled options where possible, keep them self-contained, and put recommended options first.",
119
+ "Set noWait only when you can keep working without the answers, for example to ask while investigating something else.",
120
+ ],
121
+ parameters: AskUserParamsSchema,
122
+ outputSchema: AskUserOutputSchema,
123
+ executionMode: "sequential",
124
+ annotations: {
125
+ readOnlyHint: true,
126
+ destructiveHint: false,
127
+ idempotentHint: true,
128
+ openWorldHint: false,
129
+ },
130
+
131
+ async execute(_toolCallId, params, signal, _onUpdate, ctx) {
132
+ const questions = normalizeQuestions(params);
133
+ if (questions.length === 0) {
134
+ return {
135
+ content: [{ type: "text", text: "No questions were provided." }],
136
+ details: { cancelled: true, answers: [], questions },
137
+ isError: true,
138
+ };
139
+ }
140
+
141
+ const submitLabel = params.submitLabel?.trim() || DEFAULT_SUBMIT_LABEL;
142
+
143
+ if (params.noWait === true && ctx.mode === "tui") {
144
+ if (backgroundController) {
145
+ return {
146
+ content: [
147
+ {
148
+ type: "text",
149
+ text: "Another background questionnaire is already open. Wait for the user to answer it before asking again.",
150
+ },
151
+ ],
152
+ details: { cancelled: true, pending: true, answers: [], questions },
153
+ isError: true,
154
+ };
155
+ }
156
+
157
+ const controller = new AbortController();
158
+ backgroundController = controller;
159
+ void runQuestionnaire(ctx as QuestionnaireContext, questions, {
160
+ title: params.title?.trim() || undefined,
161
+ submitLabel,
162
+ signal: controller.signal,
163
+ })
164
+ .then((result) => {
165
+ if (shuttingDown || backgroundController !== controller) {
166
+ return;
167
+ }
168
+ pi.sendMessage(
169
+ {
170
+ customType: "ask_user:answers",
171
+ content: resultContent(result),
172
+ display: true,
173
+ details: { cancelled: result.cancelled, answers: result.answers, questions },
174
+ },
175
+ { triggerTurn: true, deliverAs: "followUp" },
176
+ );
177
+ })
178
+ .catch(() => {
179
+ // The questionnaire could not be shown; there is nothing to deliver.
180
+ })
181
+ .finally(() => {
182
+ if (backgroundController === controller) {
183
+ backgroundController = undefined;
184
+ }
185
+ });
186
+
187
+ return {
188
+ content: [
189
+ {
190
+ type: "text",
191
+ text: `Questionnaire shown to the user in the background. Continue with other work; the answers will arrive as a follow-up message.`,
192
+ },
193
+ ],
194
+ details: { cancelled: false, pending: true, answers: [], questions },
195
+ structuredContent: toStructuredOutput(buildResult(questions, new Map(), false, true)),
196
+ };
197
+ }
198
+ let result: AskResult;
199
+ try {
200
+ result = await askQuestions(ctx, questions, {
201
+ title: params.title?.trim() || undefined,
202
+ submitLabel,
203
+ signal: signal ?? undefined,
204
+ });
205
+ } catch (error) {
206
+ if (error instanceof Error && error.message === "no-ui") {
207
+ const text = HELP_TEXT.replace("{{questions}}", questionsAsText(questions));
208
+ return {
209
+ content: [{ type: "text", text }],
210
+ details: { cancelled: true, answers: [], questions },
211
+ isError: true,
212
+ };
213
+ }
214
+ throw error;
215
+ }
216
+
217
+ return {
218
+ content: [{ type: "text", text: resultContent(result) }],
219
+ details: {
220
+ cancelled: result.cancelled,
221
+ answers: result.answers,
222
+ questions,
223
+ },
224
+ structuredContent: toStructuredOutput(result),
225
+ };
226
+ },
227
+
228
+ renderCall(args, theme, _context) {
229
+ const questions = Array.isArray(args.questions) ? args.questions : [];
230
+ const count = questions.length;
231
+ let text =
232
+ theme.fg("toolTitle", theme.bold("ask_user ")) +
233
+ theme.fg("muted", `${count} question${count === 1 ? "" : "s"}`);
234
+ const labels = questions
235
+ .map((question, index) => question.label?.trim() || question.id?.trim() || `Q${index + 1}`)
236
+ .join(", ");
237
+ if (labels.length > 0) {
238
+ text += theme.fg("dim", ` (${labels})`);
239
+ }
240
+ const first = questions[0]?.question;
241
+ if (typeof first === "string" && first.length > 0) {
242
+ text += `\n${theme.fg("dim", ` ${first}`)}`;
243
+ }
244
+ return new Text(text, 0, 0);
245
+ },
246
+
247
+ renderResult(result, _options, theme, _context) {
248
+ const details = result.details;
249
+ if (!details) {
250
+ const content = result.content[0];
251
+ return new Text(content?.type === "text" ? content.text : "", 0, 0);
252
+ }
253
+ if (details.pending) {
254
+ if (result.isError) {
255
+ const content = result.content[0];
256
+ return new Text(content?.type === "text" ? content.text : "Background questionnaire busy", 0, 0);
257
+ }
258
+ return new Text(theme.fg("muted", "Waiting for the user in the background"), 0, 0);
259
+ }
260
+ if (details.cancelled) {
261
+ return new Text(theme.fg("warning", "Cancelled"), 0, 0);
262
+ }
263
+ const lines = details.answers.map((answer) => {
264
+ const label = theme.fg("accent", answer.label);
265
+ if (answer.skipped) {
266
+ return `${label}${theme.fg("dim", ": skipped")}`;
267
+ }
268
+ if (answer.entries.length === 0) {
269
+ return `${label}${theme.fg("dim", ": no answer")}`;
270
+ }
271
+ const values = answer.entries
272
+ .map((entry) =>
273
+ entry.custom
274
+ ? `${theme.fg("muted", "(typed) ")}${theme.fg("text", entry.value)}`
275
+ : theme.fg("text", entry.label),
276
+ )
277
+ .join(theme.fg("dim", ", "));
278
+ return `${label}${theme.fg("muted", ": ")}${values}`;
279
+ });
280
+ return new Text(lines.join("\n"), 0, 0);
281
+ },
282
+ });
283
+
284
+ pi.registerCommand("ask-demo", {
285
+ description: "Preview the ask_user questionnaire UI with a sample survey",
286
+ handler: async (_args, ctx) => {
287
+ if (!ctx.hasUI) {
288
+ ctx.ui.notify("The demo needs an interactive UI (TUI or RPC mode).", "error");
289
+ return;
290
+ }
291
+ const questions = normalizeQuestions(demoParams());
292
+ try {
293
+ const result = await askQuestions(ctx, questions, {
294
+ title: "ask_user demo",
295
+ submitLabel: DEFAULT_SUBMIT_LABEL,
296
+ signal: ctx.signal,
297
+ });
298
+ ctx.ui.notify(
299
+ result.cancelled
300
+ ? "Demo cancelled"
301
+ : `Demo finished — ${formatAnswersText(result).split("\n").length} answers`,
302
+ "info",
303
+ );
304
+ } catch (error) {
305
+ if (error instanceof Error && error.message === "no-ui") {
306
+ ctx.ui.notify("No UI available for the demo.", "error");
307
+ return;
308
+ }
309
+ throw error;
310
+ }
311
+ },
312
+ });
313
+ }
314
+
315
+ /** Sample questionnaire used by `/ask-demo` and the README. */
316
+ function demoParams(): AskUserParams {
317
+ return {
318
+ title: "ask_user demo",
319
+ questions: [
320
+ {
321
+ id: "scope",
322
+ label: "Scope",
323
+ question: "Which parts of the project should this change touch?",
324
+ multiple: true,
325
+ options: [
326
+ {
327
+ label: "Extension host",
328
+ description: "Tool registration and lifecycle code",
329
+ selected: true,
330
+ },
331
+ { label: "TUI component", description: "The interactive questionnaire renderer" },
332
+ { label: "Documentation" },
333
+ { label: "Tests", selected: true },
334
+ ],
335
+ },
336
+ {
337
+ id: "release",
338
+ label: "Release",
339
+ question: "How should the release be versioned?",
340
+ options: [
341
+ { label: "Patch", description: "Bug fixes only (0.0.x)" },
342
+ { label: "Minor", description: "Backwards-compatible features (0.x.0)" },
343
+ { label: "Major", description: "Breaking changes (x.0.0)" },
344
+ ],
345
+ },
346
+ {
347
+ id: "notes",
348
+ label: "Notes",
349
+ question: "Anything else I should know?",
350
+ required: false,
351
+ placeholder: "Type a note, or leave empty…",
352
+ },
353
+ ],
354
+ };
355
+ }