@hank-warren/pi-ask-user-question 0.3.0 → 0.4.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 +12 -2
- package/ask-user-question.ts +7 -1
- package/package.json +2 -2
- package/questionnaire.ts +8 -0
- package/tool/envelope.ts +6 -1
- package/tool/schema.ts +10 -1
- package/view/dialog.ts +60 -0
package/README.md
CHANGED
|
@@ -4,8 +4,9 @@ A structured questionnaire the model can put to you when it would otherwise
|
|
|
4
4
|
guess. Instead of a free-form "which do you prefer?" in chat, you get a dialog
|
|
5
5
|
with numbered options, digit hotkeys, a typed-answer escape, and Tab-to-comment.
|
|
6
6
|
|
|
7
|
-
> **v0.
|
|
8
|
-
> preview pane. See
|
|
7
|
+
> **v0.3** ships 1-4 questions per call as cycleable tabs, single-select, with
|
|
8
|
+
> an optional preview pane. See
|
|
9
|
+
> [the spec](../../docs/specs/pi-ask-user-question.md) §14.
|
|
9
10
|
|
|
10
11
|
## Install
|
|
11
12
|
|
|
@@ -33,6 +34,15 @@ answering one jumps to the next unanswered question, and the call returns once
|
|
|
33
34
|
every question has an answer. Cycling back and re-answering replaces that
|
|
34
35
|
question's answer rather than recording a second one.
|
|
35
36
|
|
|
37
|
+
## Previews
|
|
38
|
+
|
|
39
|
+
An option may carry a `preview` field — markdown shown in a pane below the
|
|
40
|
+
options while that option is highlighted. Use it for concrete artifacts worth
|
|
41
|
+
comparing (ASCII mockups, code snippets, configuration variations), not for
|
|
42
|
+
simple preference questions. The pane is stacked rather than side-by-side, and
|
|
43
|
+
long previews are clipped with a `… N more lines` marker so the options always
|
|
44
|
+
stay visible.
|
|
45
|
+
|
|
36
46
|
**Why `n` and not `Tab` for notes?** Tab cycles questions here, matching
|
|
37
47
|
`@juicesharp/rpiv-ask-user-question`. `pi-auto-permissions` approval prompts
|
|
38
48
|
keep `Tab` for notes — they are single-question and have no tabs to cycle.
|
package/ask-user-question.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* package replays cleanly. Only one of the two may be installed at a time.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import type
|
|
9
|
+
import { type ExtensionAPI, getMarkdownTheme } from "@earendil-works/pi-coding-agent";
|
|
10
10
|
import {
|
|
11
11
|
ASK_USER_BLOCKED_EVENT,
|
|
12
12
|
ASK_USER_PROMPT_EVENT,
|
|
@@ -41,6 +41,9 @@ const DESCRIPTION = `Ask the user one or more structured questions during execut
|
|
|
41
41
|
3. Get decisions on implementation choices as you work
|
|
42
42
|
4. Offer choices to the user about what direction to take
|
|
43
43
|
|
|
44
|
+
Preview feature:
|
|
45
|
+
Use the optional \`preview\` field on an option when presenting a concrete artifact the user needs to compare visually: an ASCII mockup, a code snippet, a diagram or configuration variation. It renders as markdown in a pane below the options while that option is highlighted. Do not use it for simple preference questions where the label and description already say enough.
|
|
46
|
+
|
|
44
47
|
Usage notes:
|
|
45
48
|
- The user can pick an option with the number keys, type a custom answer via the automatically appended "Type something." row, attach a note to their choice by pressing n, or press Esc to decline. Do NOT author "Other" or "Type something." labels yourself — reserved labels are rejected at runtime.
|
|
46
49
|
- If you recommend a specific option, make it the first option and add "(Recommended)" at the end of the label.
|
|
@@ -96,6 +99,9 @@ export function registerTool(pi: ExtensionAPI): void {
|
|
|
96
99
|
new QuestionnaireDialog({
|
|
97
100
|
session,
|
|
98
101
|
theme,
|
|
102
|
+
// Reuse pi's own markdown theme so previews match the
|
|
103
|
+
// transcript rather than inventing a second code style.
|
|
104
|
+
markdownTheme: getMarkdownTheme(),
|
|
99
105
|
done,
|
|
100
106
|
requestRender: () => tui.requestRender(),
|
|
101
107
|
}),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hank-warren/pi-ask-user-question",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Structured questionnaire tool for Pi with numbered options, digit hotkeys and Tab-to-comment, composed from the shared permission-selector component.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"LICENSE"
|
|
46
46
|
],
|
|
47
47
|
"dependencies": {
|
|
48
|
-
"@hank-warren/pi-permission-selector": "^0.3.
|
|
48
|
+
"@hank-warren/pi-permission-selector": "^0.3.2"
|
|
49
49
|
},
|
|
50
50
|
"peerDependencies": {
|
|
51
51
|
"@earendil-works/pi-coding-agent": "*",
|
package/questionnaire.ts
CHANGED
|
@@ -28,6 +28,8 @@ export interface SelectableRow {
|
|
|
28
28
|
value: string;
|
|
29
29
|
label: string;
|
|
30
30
|
description?: string;
|
|
31
|
+
/** Markdown shown in the preview pane while this row is highlighted. */
|
|
32
|
+
preview?: string;
|
|
31
33
|
}
|
|
32
34
|
|
|
33
35
|
export class QuestionnaireSession {
|
|
@@ -109,6 +111,7 @@ export class QuestionnaireSession {
|
|
|
109
111
|
value: option.label,
|
|
110
112
|
label: option.label,
|
|
111
113
|
description: option.description,
|
|
114
|
+
...(option.preview ? { preview: option.preview } : {}),
|
|
112
115
|
}));
|
|
113
116
|
rows.push({ value: CUSTOM_ANSWER_VALUE, label: CUSTOM_ANSWER_LABEL });
|
|
114
117
|
return rows;
|
|
@@ -127,12 +130,17 @@ export class QuestionnaireSession {
|
|
|
127
130
|
recordAnswer(answer: string, opts: { custom?: boolean; notes?: string } = {}): void {
|
|
128
131
|
const question = this.current;
|
|
129
132
|
if (!question) return;
|
|
133
|
+
// A custom typed answer never carries a preview; only a chosen option can.
|
|
134
|
+
const preview = opts.custom
|
|
135
|
+
? undefined
|
|
136
|
+
: question.options.find((option) => option.label === answer)?.preview;
|
|
130
137
|
this.answers[this.index] = {
|
|
131
138
|
questionIndex: this.index,
|
|
132
139
|
question: question.question,
|
|
133
140
|
answer,
|
|
134
141
|
custom: opts.custom === true,
|
|
135
142
|
...(opts.notes ? { notes: opts.notes } : {}),
|
|
143
|
+
...(preview ? { preview } : {}),
|
|
136
144
|
};
|
|
137
145
|
}
|
|
138
146
|
|
package/tool/envelope.ts
CHANGED
|
@@ -23,9 +23,14 @@ export function buildToolResult(text: string, details: QuestionnaireResult): Too
|
|
|
23
23
|
return { content: [{ type: "text" as const, text }], details };
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
/**
|
|
26
|
+
/**
|
|
27
|
+
* Format a single answer as `"question"="answer"`, with the chosen option's
|
|
28
|
+
* preview and the user's note appended when present. Order and wording match
|
|
29
|
+
* rpiv's envelope and are pinned by tests.
|
|
30
|
+
*/
|
|
27
31
|
export function buildAnswerSegment(a: QuestionAnswer): string {
|
|
28
32
|
const parts: string[] = [`"${a.question}"="${a.answer}"`];
|
|
33
|
+
if (a.preview && a.preview.length > 0) parts.push(`selected preview: ${a.preview}`);
|
|
29
34
|
if (a.notes && a.notes.length > 0) parts.push(`user notes: ${a.notes}`);
|
|
30
35
|
return `${parts.join(". ")}.`;
|
|
31
36
|
}
|
package/tool/schema.ts
CHANGED
|
@@ -39,6 +39,12 @@ export const OptionSchema = Type.Object({
|
|
|
39
39
|
description:
|
|
40
40
|
"Explanation of what this option means or what will happen if chosen. Useful for providing context about trade-offs or implications.",
|
|
41
41
|
}),
|
|
42
|
+
preview: Type.Optional(
|
|
43
|
+
Type.String({
|
|
44
|
+
description:
|
|
45
|
+
"Optional markdown shown in a pane below the options while this option is highlighted. Use for concrete artifacts the user needs to compare: ASCII mockups, code snippets, diagram or configuration variations. Do NOT use it for simple preference questions where the label and description already say enough.",
|
|
46
|
+
}),
|
|
47
|
+
),
|
|
42
48
|
});
|
|
43
49
|
|
|
44
50
|
export const QuestionSchema = Type.Object({
|
|
@@ -70,6 +76,7 @@ export const QuestionParamsSchema = Type.Object({
|
|
|
70
76
|
export interface OptionParams {
|
|
71
77
|
label: string;
|
|
72
78
|
description: string;
|
|
79
|
+
preview?: string;
|
|
73
80
|
}
|
|
74
81
|
|
|
75
82
|
export interface QuestionParams {
|
|
@@ -88,8 +95,10 @@ export interface QuestionAnswer {
|
|
|
88
95
|
question: string;
|
|
89
96
|
answer: string;
|
|
90
97
|
custom: boolean;
|
|
91
|
-
/** Trimmed
|
|
98
|
+
/** Trimmed note, when the user attached one. */
|
|
92
99
|
notes?: string;
|
|
100
|
+
/** Preview text of the chosen option, when it carried one. */
|
|
101
|
+
preview?: string;
|
|
93
102
|
}
|
|
94
103
|
|
|
95
104
|
export interface QuestionnaireResult {
|
package/view/dialog.ts
CHANGED
|
@@ -55,6 +55,7 @@ import {
|
|
|
55
55
|
visibleWidth,
|
|
56
56
|
wrapTextWithAnsi,
|
|
57
57
|
} from "@earendil-works/pi-tui";
|
|
58
|
+
import { Markdown, type MarkdownTheme } from "@earendil-works/pi-tui";
|
|
58
59
|
import type { QuestionnaireSession } from "../questionnaire.ts";
|
|
59
60
|
import type { QuestionnaireResult } from "../tool/schema.ts";
|
|
60
61
|
|
|
@@ -75,6 +76,12 @@ export interface DialogOptions {
|
|
|
75
76
|
* the transcript show through to its right.
|
|
76
77
|
*/
|
|
77
78
|
maxWidth?: number;
|
|
79
|
+
/**
|
|
80
|
+
* Markdown theme for the preview pane. Hosts pass pi's `getMarkdownTheme()`;
|
|
81
|
+
* when omitted the preview renders as plain wrapped text, which keeps this
|
|
82
|
+
* component unit-testable without constructing a 14-function theme.
|
|
83
|
+
*/
|
|
84
|
+
markdownTheme?: MarkdownTheme;
|
|
78
85
|
}
|
|
79
86
|
|
|
80
87
|
/** Left/right border plus one space of padding on each side. */
|
|
@@ -84,6 +91,8 @@ const FIELD_INDENT = " ";
|
|
|
84
91
|
/** Opens the note editor. Tab is taken by question cycling. */
|
|
85
92
|
const NOTE_KEY = "n";
|
|
86
93
|
const isNoteKey = isCharKey(NOTE_KEY);
|
|
94
|
+
/** Maximum rows the preview pane may occupy before it is clipped. */
|
|
95
|
+
const PREVIEW_MAX_ROWS = 12;
|
|
87
96
|
|
|
88
97
|
/** Per-question UI state, preserved while cycling between tabs. */
|
|
89
98
|
interface TabState {
|
|
@@ -232,12 +241,63 @@ export class QuestionnaireDialog {
|
|
|
232
241
|
}
|
|
233
242
|
|
|
234
243
|
lines.push(...tab.selector.render(inner));
|
|
244
|
+
lines.push(...this.previewLines(inner));
|
|
235
245
|
if (session.total > 1 && !tab.selector.isCommenting()) {
|
|
236
246
|
lines.push(this.style("dim", " tab next · shift+tab prev question"));
|
|
237
247
|
}
|
|
238
248
|
return lines;
|
|
239
249
|
}
|
|
240
250
|
|
|
251
|
+
/** Preview markdown for the highlighted row, if it carries any. */
|
|
252
|
+
private currentPreview(): string | undefined {
|
|
253
|
+
const selected = this.tab.selector.getSelected();
|
|
254
|
+
if (!selected) return undefined;
|
|
255
|
+
return this.session.rows().find((row) => row.value === selected.value)?.preview;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* The preview pane: markdown between two rules, below the options.
|
|
260
|
+
*
|
|
261
|
+
* Stacked, not side-by-side. rpiv spent ~712 lines largely on making a
|
|
262
|
+
* two-column layout behave at narrow widths; stacking needs none of it
|
|
263
|
+
* (docs/specs/pi-ask-user-question.md §6.2).
|
|
264
|
+
*/
|
|
265
|
+
private previewLines(inner: number): string[] {
|
|
266
|
+
const preview = this.currentPreview();
|
|
267
|
+
if (!preview) return [];
|
|
268
|
+
|
|
269
|
+
const rendered = this.renderMarkdown(preview, inner);
|
|
270
|
+
|
|
271
|
+
// Cap the pane so a long preview cannot push the options off screen.
|
|
272
|
+
const clipped = rendered.slice(0, PREVIEW_MAX_ROWS);
|
|
273
|
+
if (rendered.length > PREVIEW_MAX_ROWS) {
|
|
274
|
+
clipped.push(this.style("dim", `… ${rendered.length - PREVIEW_MAX_ROWS} more lines`));
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const rule = this.style("dim", "─".repeat(Math.max(1, inner)));
|
|
278
|
+
return ["", rule, ...clipped, rule];
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Render preview markdown, falling back to plain wrapped text.
|
|
283
|
+
*
|
|
284
|
+
* The fallback is not just for hosts that pass no theme. pi's
|
|
285
|
+
* `getMarkdownTheme()` returns lazily-bound functions that throw
|
|
286
|
+
* "Theme not initialized" until `initTheme()` has run, and a throw inside
|
|
287
|
+
* `render()` would take down the entire dialog rather than one pane. A
|
|
288
|
+
* preview is a nicety; it must never be able to do that.
|
|
289
|
+
*/
|
|
290
|
+
private renderMarkdown(preview: string, inner: number): string[] {
|
|
291
|
+
if (this.opts.markdownTheme) {
|
|
292
|
+
try {
|
|
293
|
+
return new Markdown(preview, 0, 0, this.opts.markdownTheme).render(inner);
|
|
294
|
+
} catch {
|
|
295
|
+
// fall through to plain text
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
return preview.split("\n").flatMap((line) => wrapTextWithAnsi(line, inner));
|
|
299
|
+
}
|
|
300
|
+
|
|
241
301
|
render(width: number): string[] {
|
|
242
302
|
const cap = this.opts.maxWidth ?? width;
|
|
243
303
|
const outer = Math.max(20, Math.min(width, cap));
|