pi-ask-questions 0.0.0-stage → 0.1.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,20 @@
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.1.0] - 2026-10-15
9
+
10
+ ### Added
11
+
12
+ - `ask_user` tool with tabbed questionnaires, single-select and multi-select options, free-form typing, and multiple typed answers per question.
13
+ - Validated answers: required questions, `min`/`max` selection counts, optional questions that can be skipped, and preselected defaults.
14
+ - Review tab that summarizes all answers and allows jumping back to edit any question.
15
+ - Safe cancellation (Esc twice or Ctrl+C) and abort-signal support.
16
+ - Scrollable, width-aware rendering with option descriptions, placeholders, and status icons.
17
+ - Structured tool output (`structuredContent`) alongside a compact model-facing summary.
18
+ - RPC dialog fallback for modes without custom components, and a plain-text fallback for non-interactive modes.
19
+ - `/ask-demo` command to preview the questionnaire UI.
20
+ - 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,172 @@
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
+ ↑↓ move • Space toggle • A all • C clear • 1-9 jump • Enter confirm
25
+ ────────────────────────────────────────────────────────────
26
+ ```
27
+
28
+ ## Install
29
+
30
+ ```bash
31
+ pi install npm:pi-ask-questions
32
+ ```
33
+
34
+ Try it for a single invocation without changing your settings:
35
+
36
+ ```bash
37
+ pi -e npm:pi-ask-questions
38
+ ```
39
+
40
+ Once installed, run `/ask-demo` to preview the UI, or just let the model call `ask_user` when it needs input.
41
+
42
+ ## Features
43
+
44
+ - **Tabs** — every question gets a tab with live status (`○` unanswered, `◐` partially answered, `✓` answered, `·` skipped). Move with `Tab` / `Shift+Tab` or `←` / `→`.
45
+ - **Single-select** radio options with optional descriptions.
46
+ - **Multi-select** checkboxes (`multiple: true`), including `min` / `max` validation, `A` to select all, and `C` to clear.
47
+ - **Free-form typing** — a question without options is a plain text prompt. Typed answers are supported on option questions too via the built-in “Type something…” entry, and multiple-choice questions can collect several typed answers.
48
+ - **Multiple answers** — return any number of selections and typed entries per question.
49
+ - **Optional questions** (`required: false`) that can be skipped, plus preset answers (`selected: true`) and defaults.
50
+ - **Review tab** — see everything before submitting; jump back to any question to edit it.
51
+ - **Validation** — required answers and `min`/`max` selection counts are enforced before submit.
52
+ - **Safe cancel** — `Esc` arms cancellation, a second `Esc` or `Ctrl+C` cancels immediately.
53
+ - **Scrollable option lists** — long lists show `↑ n more` / `↓ n more` indicators.
54
+ - **Structured results** — the tool returns text for the model and a machine-readable `structuredContent` payload for codemode scripts.
55
+
56
+ ## How the model uses it
57
+
58
+ `ask_user` accepts one or more questions. A single-select question resolves immediately; everything else flows into tabs and a submit step.
59
+
60
+ ```json
61
+ {
62
+ "title": "Release planning",
63
+ "questions": [
64
+ {
65
+ "id": "scope",
66
+ "label": "Scope",
67
+ "question": "Which packages should the release include?",
68
+ "multiple": true,
69
+ "min": 1,
70
+ "options": [
71
+ { "value": "core", "label": "Core", "description": "Shared runtime code", "selected": true },
72
+ { "value": "cli", "label": "CLI" },
73
+ { "value": "docs", "label": "Docs" }
74
+ ]
75
+ },
76
+ {
77
+ "id": "version",
78
+ "label": "Version",
79
+ "question": "How should we version it?",
80
+ "options": [
81
+ { "value": "patch", "label": "Patch" },
82
+ { "value": "minor", "label": "Minor" },
83
+ { "value": "major", "label": "Major" }
84
+ ]
85
+ },
86
+ {
87
+ "id": "notes",
88
+ "label": "Notes",
89
+ "question": "Anything else I should know?",
90
+ "required": false,
91
+ "placeholder": "Type a note, or leave empty…"
92
+ }
93
+ ]
94
+ }
95
+ ```
96
+
97
+ The model receives a compact summary such as:
98
+
99
+ ```text
100
+ User answered:
101
+ Scope: selected "Core", "CLI", typed "shared types"
102
+ Version: selected "Minor"
103
+ Notes: (skipped)
104
+ ```
105
+
106
+ ### Parameters
107
+
108
+ | Field | Type | Description |
109
+ | --- | --- | --- |
110
+ | `title` | string | Optional title shown above the questionnaire. |
111
+ | `submitLabel` | string | Label of the submit entry. Default `Submit`. |
112
+ | `questions` | array | 1–12 questions, each rendered as a tab. |
113
+ | `questions[].id` | string | Stable id used in the result. Defaults to `q1`, `q2`, … |
114
+ | `questions[].label` | string | Short tab label. Defaults to `Q1`, `Q2`, … |
115
+ | `questions[].question` | string | The question text. |
116
+ | `questions[].header` | string | Optional context shown under the question. |
117
+ | `questions[].options` | array | Choices. Omit or leave empty for free-form text. |
118
+ | `questions[].options[].value` | string | Value returned when picked. Defaults to the label. |
119
+ | `questions[].options[].label` | string | Display label. |
120
+ | `questions[].options[].description` | string | Explanation shown below the label. |
121
+ | `questions[].options[].selected` | boolean | Preselect the option. |
122
+ | `questions[].multiple` | boolean | Allow more than one selection. |
123
+ | `questions[].allowOther` | boolean | Show the typed-answer entry. Default `true`. |
124
+ | `questions[].required` | boolean | Require an answer before submit. Default `true`. |
125
+ | `questions[].min` / `max` | integer | Selection bounds for multi-select. |
126
+ | `questions[].placeholder` | string | Placeholder for the free-form editor. |
127
+ | `questions[].otherLabel` | string | Custom label for the typed-answer entry. |
128
+
129
+ ### Keyboard reference
130
+
131
+ | Keys | Action |
132
+ | --- | --- |
133
+ | `Tab` / `Shift+Tab`, `←` / `→` | Switch tabs |
134
+ | `↑` / `↓`, `Home` / `End` | Move the option cursor |
135
+ | `Space` | Toggle a checkbox / select a radio option |
136
+ | `1`–`9` | Jump to an option (toggles it in multi-select) |
137
+ | `A` / `C` | Select all / clear (multi-select) |
138
+ | `Enter` | Confirm the question, open the editor, or submit |
139
+ | `Esc` | Leave the editor, arm cancel; `Esc` twice cancels |
140
+ | `Ctrl+C` | Cancel immediately |
141
+ | `\` + `Enter` | Newline in the free-form editor |
142
+
143
+ ## Modes
144
+
145
+ | Mode | Behavior |
146
+ | --- | --- |
147
+ | Interactive TUI | Full tabbed questionnaire. |
148
+ | RPC | Sequential fallback built from the dialog API (`select` / `input`). |
149
+ | Print / JSON | The tool reports that no UI is available and lists the questions so the model can ask them in plain text. |
150
+
151
+ ## Development
152
+
153
+ ```bash
154
+ npm install
155
+ npm run check # typecheck + unit tests
156
+ pi -e ./src/extension.ts
157
+ ```
158
+
159
+ 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.
160
+
161
+ ## Publishing
162
+
163
+ 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.
164
+
165
+ ```bash
166
+ npm login
167
+ npm publish
168
+ ```
169
+
170
+ ## License
171
+
172
+ 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.1.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,271 @@
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
+ formatAnswersText,
18
+ normalizeQuestions,
19
+ toStructuredOutput,
20
+ } from "./questions.ts";
21
+ import {
22
+ AskUserOutputSchema,
23
+ AskUserParamsSchema,
24
+ type AskUserParams,
25
+ } from "./schema.ts";
26
+ import { runQuestionnaire, type QuestionnaireContext } from "./ui/ask-questionnaire.ts";
27
+ /** Structured details stored with the tool result for rendering. */
28
+ export interface AskToolDetails {
29
+ cancelled: boolean;
30
+ answers: QuestionAnswer[];
31
+ questions: NormalizedQuestion[];
32
+ }
33
+
34
+ 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.
35
+
36
+ The interactive UI supports:
37
+ - Tabs: every question gets its own tab (Tab / Shift+Tab and arrow keys switch).
38
+ - Single-select and multi-select questions (set multiple: true).
39
+ - Free-form typing: the user can always type a custom answer unless allowOther is false; a question with no options is purely free-form.
40
+ - Optional questions (required: false) that can be skipped, preselected options (selected: true), and min/max selection counts.
41
+ - Number keys 1-9 jump to options; A selects all; C clears a multi-select.
42
+
43
+ Guidelines:
44
+ - Prefer this tool over asking questions in prose when you actually need the answer to proceed.
45
+ - Group related questions into one call (2-6 questions) instead of asking repeatedly.
46
+ - Provide concrete, labeled options whenever you can; use a free-form question only when the answer space is open.
47
+ - Put recommended options first.`;
48
+
49
+ const HELP_TEXT = `Interactive UI is not available in this mode. Ask the user the following questions directly in your reply instead:
50
+ {{questions}}`;
51
+
52
+ /** Dispatch to the right UI for the current mode. */
53
+ async function askQuestions(
54
+ ctx: Pick<ExtensionContext, "ui" | "mode" | "hasUI">,
55
+ questions: NormalizedQuestion[],
56
+ options: { title?: string; submitLabel: string; signal?: AbortSignal },
57
+ ): Promise<AskResult> {
58
+ if (ctx.mode === "tui") {
59
+ return runQuestionnaire(ctx as QuestionnaireContext, questions, options);
60
+ }
61
+ if (ctx.hasUI) {
62
+ return runFallbackSurvey(ctx.ui, questions, options.signal);
63
+ }
64
+ throw new Error("no-ui");
65
+ }
66
+
67
+ /** Human-readable list used when no UI exists (print/JSON mode). */
68
+ function questionsAsText(questions: NormalizedQuestion[]): string {
69
+ return questions
70
+ .map((question, index) => {
71
+ const options =
72
+ question.options.length > 0
73
+ ? `\n Options: ${question.options.map((option) => option.label).join(", ")}`
74
+ : "";
75
+ const optional = question.required ? "" : " (optional)";
76
+ return `${index + 1}. ${question.question}${optional}${options}`;
77
+ })
78
+ .join("\n");
79
+ }
80
+
81
+ function resultContent(result: AskResult): string {
82
+ if (result.cancelled) {
83
+ return "The user cancelled the questionnaire before submitting.";
84
+ }
85
+ return `User answered:\n${formatAnswersText(result)}`;
86
+ }
87
+
88
+ /** Start the questionnaire and wait for the user. */
89
+ export default function askQuestionsExtension(pi: ExtensionAPI): void {
90
+ pi.registerTool<typeof AskUserParamsSchema, AskToolDetails>({
91
+ name: "ask_user",
92
+ label: "Ask User",
93
+ description: TOOL_DESCRIPTION,
94
+ promptSnippet:
95
+ "ask_user: ask the user structured questions in an interactive questionnaire (tabs, multi-select, free-form).",
96
+ promptGuidelines: [
97
+ "Use ask_user when you need a decision or missing detail from the user; batch related questions into one call instead of asking repeatedly.",
98
+ "Give ask_user questions labeled options where possible, keep them self-contained, and put recommended options first.",
99
+ ],
100
+ parameters: AskUserParamsSchema,
101
+ outputSchema: AskUserOutputSchema,
102
+ executionMode: "sequential",
103
+ annotations: {
104
+ readOnlyHint: true,
105
+ destructiveHint: false,
106
+ idempotentHint: true,
107
+ openWorldHint: false,
108
+ },
109
+
110
+ async execute(_toolCallId, params, signal, _onUpdate, ctx) {
111
+ const questions = normalizeQuestions(params);
112
+ if (questions.length === 0) {
113
+ return {
114
+ content: [{ type: "text", text: "No questions were provided." }],
115
+ details: { cancelled: true, answers: [], questions },
116
+ isError: true,
117
+ };
118
+ }
119
+
120
+ const submitLabel = params.submitLabel?.trim() || DEFAULT_SUBMIT_LABEL;
121
+ let result: AskResult;
122
+ try {
123
+ result = await askQuestions(ctx, questions, {
124
+ title: params.title?.trim() || undefined,
125
+ submitLabel,
126
+ signal: signal ?? undefined,
127
+ });
128
+ } catch (error) {
129
+ if (error instanceof Error && error.message === "no-ui") {
130
+ const text = HELP_TEXT.replace("{{questions}}", questionsAsText(questions));
131
+ return {
132
+ content: [{ type: "text", text }],
133
+ details: { cancelled: true, answers: [], questions },
134
+ isError: true,
135
+ };
136
+ }
137
+ throw error;
138
+ }
139
+
140
+ return {
141
+ content: [{ type: "text", text: resultContent(result) }],
142
+ details: {
143
+ cancelled: result.cancelled,
144
+ answers: result.answers,
145
+ questions,
146
+ },
147
+ structuredContent: toStructuredOutput(result),
148
+ };
149
+ },
150
+
151
+ renderCall(args, theme, _context) {
152
+ const questions = Array.isArray(args.questions) ? args.questions : [];
153
+ const count = questions.length;
154
+ let text =
155
+ theme.fg("toolTitle", theme.bold("ask_user ")) +
156
+ theme.fg("muted", `${count} question${count === 1 ? "" : "s"}`);
157
+ const labels = questions
158
+ .map((question, index) => question.label?.trim() || question.id?.trim() || `Q${index + 1}`)
159
+ .join(", ");
160
+ if (labels.length > 0) {
161
+ text += theme.fg("dim", ` (${labels})`);
162
+ }
163
+ const first = questions[0]?.question;
164
+ if (typeof first === "string" && first.length > 0) {
165
+ text += `\n${theme.fg("dim", ` ${first}`)}`;
166
+ }
167
+ return new Text(text, 0, 0);
168
+ },
169
+
170
+ renderResult(result, _options, theme, _context) {
171
+ const details = result.details;
172
+ if (!details) {
173
+ const content = result.content[0];
174
+ return new Text(content?.type === "text" ? content.text : "", 0, 0);
175
+ }
176
+ if (details.cancelled) {
177
+ return new Text(theme.fg("warning", "Cancelled"), 0, 0);
178
+ }
179
+ const lines = details.answers.map((answer) => {
180
+ const label = theme.fg("accent", answer.label);
181
+ if (answer.skipped) {
182
+ return `${theme.fg("dim", "⏭ ")}${label}${theme.fg("dim", ": skipped")}`;
183
+ }
184
+ if (answer.entries.length === 0) {
185
+ return `${theme.fg("dim", "· ")}${label}${theme.fg("dim", ": no answer")}`;
186
+ }
187
+ const values = answer.entries
188
+ .map((entry) =>
189
+ entry.custom
190
+ ? `${theme.fg("muted", "(typed) ")}${theme.fg("text", entry.value)}`
191
+ : theme.fg("text", entry.label),
192
+ )
193
+ .join(theme.fg("dim", ", "));
194
+ return `${theme.fg("success", "✓ ")}${label}${theme.fg("muted", ": ")}${values}`;
195
+ });
196
+ return new Text(lines.join("\n"), 0, 0);
197
+ },
198
+ });
199
+
200
+ pi.registerCommand("ask-demo", {
201
+ description: "Preview the ask_user questionnaire UI with a sample survey",
202
+ handler: async (_args, ctx) => {
203
+ if (!ctx.hasUI) {
204
+ ctx.ui.notify("The demo needs an interactive UI (TUI or RPC mode).", "error");
205
+ return;
206
+ }
207
+ const questions = normalizeQuestions(demoParams());
208
+ try {
209
+ const result = await askQuestions(ctx, questions, {
210
+ title: "ask_user demo",
211
+ submitLabel: DEFAULT_SUBMIT_LABEL,
212
+ signal: ctx.signal,
213
+ });
214
+ ctx.ui.notify(
215
+ result.cancelled
216
+ ? "Demo cancelled"
217
+ : `Demo finished — ${formatAnswersText(result).split("\n").length} answers`,
218
+ "info",
219
+ );
220
+ } catch (error) {
221
+ if (error instanceof Error && error.message === "no-ui") {
222
+ ctx.ui.notify("No UI available for the demo.", "error");
223
+ return;
224
+ }
225
+ throw error;
226
+ }
227
+ },
228
+ });
229
+ }
230
+
231
+ /** Sample questionnaire used by `/ask-demo` and the README. */
232
+ function demoParams(): AskUserParams {
233
+ return {
234
+ title: "ask_user demo",
235
+ questions: [
236
+ {
237
+ id: "scope",
238
+ label: "Scope",
239
+ question: "Which parts of the project should this change touch?",
240
+ multiple: true,
241
+ options: [
242
+ {
243
+ label: "Extension host",
244
+ description: "Tool registration and lifecycle code",
245
+ selected: true,
246
+ },
247
+ { label: "TUI component", description: "The interactive questionnaire renderer" },
248
+ { label: "Documentation" },
249
+ { label: "Tests", selected: true },
250
+ ],
251
+ },
252
+ {
253
+ id: "release",
254
+ label: "Release",
255
+ question: "How should the release be versioned?",
256
+ options: [
257
+ { label: "Patch", description: "Bug fixes only (0.0.x)" },
258
+ { label: "Minor", description: "Backwards-compatible features (0.x.0)" },
259
+ { label: "Major", description: "Breaking changes (x.0.0)" },
260
+ ],
261
+ },
262
+ {
263
+ id: "notes",
264
+ label: "Notes",
265
+ question: "Anything else I should know?",
266
+ required: false,
267
+ placeholder: "Type a note, or leave empty…",
268
+ },
269
+ ],
270
+ };
271
+ }