@xynogen/pix-ask 0.2.5 → 0.2.7
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/package.json +1 -1
- package/src/ask.test.ts +2 -9
- package/src/components.ts +1 -4
- package/src/frame.ts +1 -2
- package/src/glyphs.test.ts +4 -12
- package/src/glyphs.ts +5 -7
- package/src/helpers.ts +4 -13
- package/src/index.ts +8 -25
- package/src/questionnaire.ts +21 -74
- package/src/rpc.ts +3 -6
- package/src/schema.ts +3 -6
package/package.json
CHANGED
package/src/ask.test.ts
CHANGED
|
@@ -17,11 +17,7 @@ import {
|
|
|
17
17
|
|
|
18
18
|
// ── Fixtures ──────────────────────────────────────────────────────────
|
|
19
19
|
|
|
20
|
-
const opt = (
|
|
21
|
-
label: string,
|
|
22
|
-
description = "Test option",
|
|
23
|
-
preview?: string,
|
|
24
|
-
): OptionData => ({
|
|
20
|
+
const opt = (label: string, description = "Test option", preview?: string): OptionData => ({
|
|
25
21
|
label,
|
|
26
22
|
description,
|
|
27
23
|
...(preview ? { preview } : {}),
|
|
@@ -30,10 +26,7 @@ const opt = (
|
|
|
30
26
|
const qSingle: QuestionData = {
|
|
31
27
|
question: "Which approach?",
|
|
32
28
|
header: "Approach",
|
|
33
|
-
options: [
|
|
34
|
-
opt("REST", "Traditional REST API"),
|
|
35
|
-
opt("GraphQL", "Query language for APIs"),
|
|
36
|
-
],
|
|
29
|
+
options: [opt("REST", "Traditional REST API"), opt("GraphQL", "Query language for APIs")],
|
|
37
30
|
};
|
|
38
31
|
|
|
39
32
|
const qMulti: QuestionData = {
|
package/src/components.ts
CHANGED
|
@@ -43,10 +43,7 @@ export class TabBar implements Component {
|
|
|
43
43
|
truncateToWidth(
|
|
44
44
|
t.fg("accent", "╭─") +
|
|
45
45
|
line +
|
|
46
|
-
t.fg(
|
|
47
|
-
"accent",
|
|
48
|
-
`${"─".repeat(Math.max(0, inner - line.length - 1))}╮`,
|
|
49
|
-
),
|
|
46
|
+
t.fg("accent", `${"─".repeat(Math.max(0, inner - line.length - 1))}╮`),
|
|
50
47
|
width,
|
|
51
48
|
"",
|
|
52
49
|
),
|
package/src/frame.ts
CHANGED
|
@@ -60,8 +60,7 @@ export function frameLines(opts: FrameOptions): string[] {
|
|
|
60
60
|
|
|
61
61
|
const row = (content: string): string => {
|
|
62
62
|
const pad = inner - visibleWidth(content);
|
|
63
|
-
const padded =
|
|
64
|
-
pad > 0 ? content + " ".repeat(pad) : truncateToWidth(content, inner);
|
|
63
|
+
const padded = pad > 0 ? content + " ".repeat(pad) : truncateToWidth(content, inner);
|
|
65
64
|
return bg(`${color("│")} ${reassert(padded)} ${color("│")}`);
|
|
66
65
|
};
|
|
67
66
|
|
package/src/glyphs.test.ts
CHANGED
|
@@ -4,36 +4,28 @@ import { checkboxGlyphs, selectionGlyph } from "./glyphs.js";
|
|
|
4
4
|
|
|
5
5
|
describe("selectionGlyph", () => {
|
|
6
6
|
test("multi checked → ▣ / success", () => {
|
|
7
|
-
expect(
|
|
8
|
-
selectionGlyph({ multi: true, selected: false, checked: true }),
|
|
9
|
-
).toEqual({
|
|
7
|
+
expect(selectionGlyph({ multi: true, selected: false, checked: true })).toEqual({
|
|
10
8
|
glyph: "▣",
|
|
11
9
|
color: "success",
|
|
12
10
|
});
|
|
13
11
|
});
|
|
14
12
|
|
|
15
13
|
test("multi unchecked → ☐ / dim", () => {
|
|
16
|
-
expect(
|
|
17
|
-
selectionGlyph({ multi: true, selected: true, checked: false }),
|
|
18
|
-
).toEqual({
|
|
14
|
+
expect(selectionGlyph({ multi: true, selected: true, checked: false })).toEqual({
|
|
19
15
|
glyph: "☐",
|
|
20
16
|
color: "dim",
|
|
21
17
|
});
|
|
22
18
|
});
|
|
23
19
|
|
|
24
20
|
test("radio selected → ◉ / accent", () => {
|
|
25
|
-
expect(
|
|
26
|
-
selectionGlyph({ multi: false, selected: true, checked: false }),
|
|
27
|
-
).toEqual({
|
|
21
|
+
expect(selectionGlyph({ multi: false, selected: true, checked: false })).toEqual({
|
|
28
22
|
glyph: "◉",
|
|
29
23
|
color: "accent",
|
|
30
24
|
});
|
|
31
25
|
});
|
|
32
26
|
|
|
33
27
|
test("radio unselected → ○ / dim", () => {
|
|
34
|
-
expect(
|
|
35
|
-
selectionGlyph({ multi: false, selected: false, checked: false }),
|
|
36
|
-
).toEqual({
|
|
28
|
+
expect(selectionGlyph({ multi: false, selected: false, checked: false })).toEqual({
|
|
37
29
|
glyph: "○",
|
|
38
30
|
color: "dim",
|
|
39
31
|
});
|
package/src/glyphs.ts
CHANGED
|
@@ -6,11 +6,10 @@ const RADIO_SELECTED = "◉";
|
|
|
6
6
|
const RADIO_UNSELECTED = "○";
|
|
7
7
|
|
|
8
8
|
/** Glyph + theme color token for a selection row. Caller applies theme.fg(color, glyph). */
|
|
9
|
-
export function selectionGlyph(opts: {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
}): { glyph: string; color: string } {
|
|
9
|
+
export function selectionGlyph(opts: { multi: boolean; selected: boolean; checked: boolean }): {
|
|
10
|
+
glyph: string;
|
|
11
|
+
color: string;
|
|
12
|
+
} {
|
|
14
13
|
if (opts.multi) {
|
|
15
14
|
return opts.checked
|
|
16
15
|
? { glyph: CHECKBOX_CHECKED, color: "success" }
|
|
@@ -28,8 +27,7 @@ export function selectionGlyph(opts: {
|
|
|
28
27
|
*/
|
|
29
28
|
export function checkboxGlyphs(): { checked: string; unchecked: string } {
|
|
30
29
|
const unicodeSafe =
|
|
31
|
-
visibleWidth(CHECKBOX_CHECKED) === 1 &&
|
|
32
|
-
visibleWidth(CHECKBOX_UNCHECKED) === 1;
|
|
30
|
+
visibleWidth(CHECKBOX_CHECKED) === 1 && visibleWidth(CHECKBOX_UNCHECKED) === 1;
|
|
33
31
|
return unicodeSafe
|
|
34
32
|
? { checked: CHECKBOX_CHECKED, unchecked: CHECKBOX_UNCHECKED }
|
|
35
33
|
: { checked: "[x]", unchecked: "[ ]" };
|
package/src/helpers.ts
CHANGED
|
@@ -20,15 +20,11 @@ export function safeMarkdownTheme(): MarkdownTheme | undefined {
|
|
|
20
20
|
// ── Option / question helpers ──────────────────────────────────────────
|
|
21
21
|
|
|
22
22
|
export function hasAnyPreview(q: QuestionData): boolean {
|
|
23
|
-
return q.options.some(
|
|
24
|
-
(o) => typeof o.preview === "string" && o.preview.length > 0,
|
|
25
|
-
);
|
|
23
|
+
return q.options.some((o) => typeof o.preview === "string" && o.preview.length > 0);
|
|
26
24
|
}
|
|
27
25
|
|
|
28
26
|
/** Which sentinel rows are auto-appended for a question. */
|
|
29
|
-
export function sentinelsFor(
|
|
30
|
-
q: QuestionData,
|
|
31
|
-
): Array<{ kind: string; label: string }> {
|
|
27
|
+
export function sentinelsFor(q: QuestionData): Array<{ kind: string; label: string }> {
|
|
32
28
|
const out: Array<{ kind: string; label: string }> = [];
|
|
33
29
|
if (q.multiSelect) {
|
|
34
30
|
out.push({ kind: "next", label: SENTINEL_NEXT });
|
|
@@ -47,10 +43,7 @@ export function formatAnswerScalar(a: QuestionAnswer): string {
|
|
|
47
43
|
return a.answer ?? "(selected)";
|
|
48
44
|
}
|
|
49
45
|
|
|
50
|
-
export function buildResponseText(
|
|
51
|
-
answers: QuestionAnswer[],
|
|
52
|
-
questions: QuestionData[],
|
|
53
|
-
): string {
|
|
46
|
+
export function buildResponseText(answers: QuestionAnswer[], questions: QuestionData[]): string {
|
|
54
47
|
const segs: string[] = [];
|
|
55
48
|
for (const a of answers) {
|
|
56
49
|
const q = questions[a.questionIndex]?.question ?? `Q${a.questionIndex + 1}`;
|
|
@@ -58,9 +51,7 @@ export function buildResponseText(
|
|
|
58
51
|
if (a.preview) s += `. selected preview: ${a.preview}`;
|
|
59
52
|
segs.push(s);
|
|
60
53
|
}
|
|
61
|
-
return segs.length
|
|
62
|
-
? `User answered: ${segs.join(". ")}.`
|
|
63
|
-
: "User declined to answer questions.";
|
|
54
|
+
return segs.length ? `User answered: ${segs.join(". ")}.` : "User declined to answer questions.";
|
|
64
55
|
}
|
|
65
56
|
|
|
66
57
|
// ── Scroll indicator ───────────────────────────────────────────────────
|
package/src/index.ts
CHANGED
|
@@ -6,13 +6,7 @@ import { once } from "./once.ts";
|
|
|
6
6
|
import { AskQuestionnaire } from "./questionnaire.js";
|
|
7
7
|
import { rpcFallback } from "./rpc.js";
|
|
8
8
|
import type { Params } from "./schema.js";
|
|
9
|
-
import {
|
|
10
|
-
MAX_OPTIONS,
|
|
11
|
-
MAX_QUESTIONS,
|
|
12
|
-
MIN_OPTIONS,
|
|
13
|
-
ParamsSchema,
|
|
14
|
-
SENTINEL_FREEFORM,
|
|
15
|
-
} from "./schema.js";
|
|
9
|
+
import { MAX_OPTIONS, MAX_QUESTIONS, MIN_OPTIONS, ParamsSchema } from "./schema.js";
|
|
16
10
|
import type { QuestionAnswer, QuestionnaireResult } from "./types.js";
|
|
17
11
|
|
|
18
12
|
// ── Re-exports (consumed by tests and single-select-layout) ───────────
|
|
@@ -41,8 +35,6 @@ export default function registerAsk(pi: ExtensionAPI): void {
|
|
|
41
35
|
promptSnippet: `Ask the user up to ${MAX_QUESTIONS} structured questions (${MIN_OPTIONS}-${MAX_OPTIONS} options each) when requirements are ambiguous`,
|
|
42
36
|
promptGuidelines: [
|
|
43
37
|
`Use ask whenever the user's request is underspecified and you cannot proceed without concrete decisions — you can ask up to ${MAX_QUESTIONS} questions per invocation.`,
|
|
44
|
-
`Each question MUST have ${MIN_OPTIONS}-${MAX_OPTIONS} options. Every option requires a concise label (1-5 words) and a description explaining what the choice means or its trade-offs. The user can additionally type a custom answer ("${SENTINEL_FREEFORM}" row is appended automatically to single-select questions).`,
|
|
45
|
-
`Set multiSelect: true when multiple answers are valid; this suppresses the "${SENTINEL_FREEFORM}" row. Provide an options[].preview markdown string when an option benefits from richer side-by-side context (mockups, code snippets, diagrams, configs) — single-select only. NOTE: any non-empty preview on a single-select question ALSO suppresses the "${SENTINEL_FREEFORM}" row (no room in the side-by-side layout). If you recommend a specific option, make it the first option and append "(Recommended)" to its label.`,
|
|
46
38
|
"Do not stack multiple ask calls back-to-back — group all clarifying questions into one invocation.",
|
|
47
39
|
],
|
|
48
40
|
executionMode: "sequential",
|
|
@@ -60,9 +52,7 @@ export default function registerAsk(pi: ExtensionAPI): void {
|
|
|
60
52
|
|
|
61
53
|
if (!Array.isArray(typed.questions) || typed.questions.length === 0) {
|
|
62
54
|
return {
|
|
63
|
-
content: [
|
|
64
|
-
{ type: "text", text: "At least one question is required." },
|
|
65
|
-
],
|
|
55
|
+
content: [{ type: "text", text: "At least one question is required." }],
|
|
66
56
|
isError: true,
|
|
67
57
|
details: { answers: [], cancelled: true },
|
|
68
58
|
};
|
|
@@ -79,11 +69,9 @@ export default function registerAsk(pi: ExtensionAPI): void {
|
|
|
79
69
|
const result = await ctx.ui.custom<QuestionnaireResult | null>(
|
|
80
70
|
(tui, theme, keybindings, done) => {
|
|
81
71
|
if (signal) {
|
|
82
|
-
signal.addEventListener(
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
{ once: true },
|
|
86
|
-
);
|
|
72
|
+
signal.addEventListener("abort", () => done({ answers: [], cancelled: true }), {
|
|
73
|
+
once: true,
|
|
74
|
+
});
|
|
87
75
|
}
|
|
88
76
|
return new AskQuestionnaire(typed, tui, theme, keybindings, done);
|
|
89
77
|
},
|
|
@@ -92,9 +80,7 @@ export default function registerAsk(pi: ExtensionAPI): void {
|
|
|
92
80
|
|
|
93
81
|
if (!result || result.cancelled) {
|
|
94
82
|
return {
|
|
95
|
-
content: [
|
|
96
|
-
{ type: "text", text: "User cancelled the questionnaire" },
|
|
97
|
-
],
|
|
83
|
+
content: [{ type: "text", text: "User cancelled the questionnaire" }],
|
|
98
84
|
details: result ?? { answers: [], cancelled: true },
|
|
99
85
|
};
|
|
100
86
|
}
|
|
@@ -106,7 +92,7 @@ export default function registerAsk(pi: ExtensionAPI): void {
|
|
|
106
92
|
renderCall(args, theme) {
|
|
107
93
|
const questions = Array.isArray(args.questions) ? args.questions : [];
|
|
108
94
|
const count = questions.length;
|
|
109
|
-
const firstQ =
|
|
95
|
+
const firstQ = questions[0]?.question ?? "";
|
|
110
96
|
let text = theme.fg("toolTitle", theme.bold(`ask (${count}) `));
|
|
111
97
|
text += theme.fg("muted", firstQ);
|
|
112
98
|
if (count > 1) text += theme.fg("dim", ` +${count - 1} more`);
|
|
@@ -124,10 +110,7 @@ export default function registerAsk(pi: ExtensionAPI): void {
|
|
|
124
110
|
return new Text(theme.fg("warning", "Cancelled"), 0, 0);
|
|
125
111
|
}
|
|
126
112
|
const texts = details.answers.map((a) => {
|
|
127
|
-
const v =
|
|
128
|
-
a.kind === "multi"
|
|
129
|
-
? (a.selected ?? []).join(", ")
|
|
130
|
-
: (a.answer ?? "");
|
|
113
|
+
const v = a.kind === "multi" ? (a.selected ?? []).join(", ") : (a.answer ?? "");
|
|
131
114
|
return `${a.questionIndex + 1}: ${v}`;
|
|
132
115
|
});
|
|
133
116
|
return new Text(theme.fg("success", `✓ ${texts.join(" • ")}`), 0, 0);
|
package/src/questionnaire.ts
CHANGED
|
@@ -17,17 +17,8 @@ import { frameLines, modalWidth } from "./frame.js";
|
|
|
17
17
|
import { checkboxGlyphs, selectionGlyph } from "./glyphs.js";
|
|
18
18
|
import { safeMarkdownTheme, sentinelsFor } from "./helpers.js";
|
|
19
19
|
import type { OptionData, Params, QuestionData } from "./schema.js";
|
|
20
|
-
import {
|
|
21
|
-
|
|
22
|
-
SENTINEL_NEXT,
|
|
23
|
-
SEPARATOR,
|
|
24
|
-
SPLIT_PANE_MIN_WIDTH,
|
|
25
|
-
} from "./schema.js";
|
|
26
|
-
import type {
|
|
27
|
-
AnswerKind,
|
|
28
|
-
QuestionAnswer,
|
|
29
|
-
QuestionnaireResult,
|
|
30
|
-
} from "./types.js";
|
|
20
|
+
import { SENTINEL_FREEFORM, SENTINEL_NEXT, SEPARATOR, SPLIT_PANE_MIN_WIDTH } from "./schema.js";
|
|
21
|
+
import type { AnswerKind, QuestionAnswer, QuestionnaireResult } from "./types.js";
|
|
31
22
|
|
|
32
23
|
// ── AskQuestionnaire ───────────────────────────────────────────────────
|
|
33
24
|
|
|
@@ -84,8 +75,7 @@ export class AskQuestionnaire extends Container {
|
|
|
84
75
|
label?: string;
|
|
85
76
|
option?: OptionData;
|
|
86
77
|
}> {
|
|
87
|
-
const items: Array<{ kind: string; label?: string; option?: OptionData }> =
|
|
88
|
-
[];
|
|
78
|
+
const items: Array<{ kind: string; label?: string; option?: OptionData }> = [];
|
|
89
79
|
for (const o of this.filteredOptions) {
|
|
90
80
|
items.push({ kind: "option", option: o });
|
|
91
81
|
}
|
|
@@ -137,9 +127,7 @@ export class AskQuestionnaire extends Container {
|
|
|
137
127
|
selected?: string[],
|
|
138
128
|
preview?: string,
|
|
139
129
|
): void {
|
|
140
|
-
this.answers = this.answers.filter(
|
|
141
|
-
(a) => a.questionIndex !== this.currentIndex,
|
|
142
|
-
);
|
|
130
|
+
this.answers = this.answers.filter((a) => a.questionIndex !== this.currentIndex);
|
|
143
131
|
this.answers.push({
|
|
144
132
|
questionIndex: this.currentIndex,
|
|
145
133
|
question: this.currentQ.question,
|
|
@@ -158,12 +146,7 @@ export class AskQuestionnaire extends Container {
|
|
|
158
146
|
}
|
|
159
147
|
|
|
160
148
|
if (item.kind === "option" && item.option) {
|
|
161
|
-
this.recordAnswer(
|
|
162
|
-
"option",
|
|
163
|
-
item.option.label,
|
|
164
|
-
undefined,
|
|
165
|
-
item.option.preview,
|
|
166
|
-
);
|
|
149
|
+
this.recordAnswer("option", item.option.label, undefined, item.option.preview);
|
|
167
150
|
this.nextQuestion();
|
|
168
151
|
} else if (item.kind === "other") {
|
|
169
152
|
this.inputMode = true;
|
|
@@ -172,7 +155,7 @@ export class AskQuestionnaire extends Container {
|
|
|
172
155
|
} else if (item.kind === "next") {
|
|
173
156
|
const selected = Array.from(this.multiChecked)
|
|
174
157
|
.sort((a, b) => a - b)
|
|
175
|
-
.map((i) => this.currentQ.options[i]?.label);
|
|
158
|
+
.map((i) => this.currentQ.options[i]?.label ?? "");
|
|
176
159
|
if (selected.length === 0) {
|
|
177
160
|
this.cancel();
|
|
178
161
|
return;
|
|
@@ -204,9 +187,7 @@ export class AskQuestionnaire extends Container {
|
|
|
204
187
|
}
|
|
205
188
|
|
|
206
189
|
private restoreAnswerState(): void {
|
|
207
|
-
const prev = this.answers.find(
|
|
208
|
-
(a) => a.questionIndex === this.currentIndex,
|
|
209
|
-
);
|
|
190
|
+
const prev = this.answers.find((a) => a.questionIndex === this.currentIndex);
|
|
210
191
|
if (!prev) return;
|
|
211
192
|
const q = this.currentQ;
|
|
212
193
|
if (prev.kind === "multi") {
|
|
@@ -287,8 +268,7 @@ export class AskQuestionnaire extends Container {
|
|
|
287
268
|
matchesKey(data, Key.ctrl("k"))
|
|
288
269
|
) {
|
|
289
270
|
if (total > 0) {
|
|
290
|
-
this.selectedOptionIndex =
|
|
291
|
-
(this.selectedOptionIndex - 1 + total) % total;
|
|
271
|
+
this.selectedOptionIndex = (this.selectedOptionIndex - 1 + total) % total;
|
|
292
272
|
this.refresh();
|
|
293
273
|
}
|
|
294
274
|
return;
|
|
@@ -423,10 +403,7 @@ export class AskQuestionnaire extends Container {
|
|
|
423
403
|
const maxVisible = Math.min(total, 12);
|
|
424
404
|
const start = Math.max(
|
|
425
405
|
0,
|
|
426
|
-
Math.min(
|
|
427
|
-
this.selectedOptionIndex - Math.floor(maxVisible / 2),
|
|
428
|
-
total - maxVisible,
|
|
429
|
-
),
|
|
406
|
+
Math.min(this.selectedOptionIndex - Math.floor(maxVisible / 2), total - maxVisible),
|
|
430
407
|
);
|
|
431
408
|
const end = Math.min(start + maxVisible, total);
|
|
432
409
|
|
|
@@ -462,24 +439,18 @@ export class AskQuestionnaire extends Container {
|
|
|
462
439
|
const label = sel
|
|
463
440
|
? t.fg("accent", t.bold(SENTINEL_FREEFORM))
|
|
464
441
|
: t.fg("text", t.bold(SENTINEL_FREEFORM));
|
|
465
|
-
lines.push(
|
|
466
|
-
truncateToWidth(`${ptr} ${t.fg("dim", "✎")} ${label}`, inner, ""),
|
|
467
|
-
);
|
|
442
|
+
lines.push(truncateToWidth(`${ptr} ${t.fg("dim", "✎")} ${label}`, inner, ""));
|
|
468
443
|
} else if (item.kind === "next") {
|
|
469
444
|
const label = sel
|
|
470
445
|
? t.fg("accent", t.bold(SENTINEL_NEXT))
|
|
471
446
|
: t.fg("text", t.bold(SENTINEL_NEXT));
|
|
472
|
-
lines.push(
|
|
473
|
-
truncateToWidth(`${ptr} ${t.fg("dim", "→")} ${label}`, inner, ""),
|
|
474
|
-
);
|
|
447
|
+
lines.push(truncateToWidth(`${ptr} ${t.fg("dim", "→")} ${label}`, inner, ""));
|
|
475
448
|
}
|
|
476
449
|
}
|
|
477
450
|
|
|
478
451
|
if (start > 0 || end < total) {
|
|
479
452
|
const count =
|
|
480
|
-
this.filteredOptions.length > 0
|
|
481
|
-
? `${this.selectedOptionIndex + 1}/${total}`
|
|
482
|
-
: `${total}`;
|
|
453
|
+
this.filteredOptions.length > 0 ? `${this.selectedOptionIndex + 1}/${total}` : `${total}`;
|
|
483
454
|
lines.push(t.fg("dim", truncateToWidth(` ${count}`, inner, "")));
|
|
484
455
|
}
|
|
485
456
|
|
|
@@ -496,19 +467,12 @@ export class AskQuestionnaire extends Container {
|
|
|
496
467
|
const mdWidth = Math.max(10, width);
|
|
497
468
|
|
|
498
469
|
if (this.mdTheme) {
|
|
499
|
-
const md = new Markdown(
|
|
500
|
-
`## ${item.option.label}\n\n${mdText}`,
|
|
501
|
-
0,
|
|
502
|
-
0,
|
|
503
|
-
this.mdTheme,
|
|
504
|
-
);
|
|
470
|
+
const md = new Markdown(`## ${item.option.label}\n\n${mdText}`, 0, 0, this.mdTheme);
|
|
505
471
|
return md.render(mdWidth);
|
|
506
472
|
}
|
|
507
473
|
|
|
508
474
|
const lines = wrapTextWithAnsi(mdText, mdWidth);
|
|
509
|
-
return lines.map((l) =>
|
|
510
|
-
truncateToWidth(this.theme.fg("muted", l), mdWidth, ""),
|
|
511
|
-
);
|
|
475
|
+
return lines.map((l) => truncateToWidth(this.theme.fg("muted", l), mdWidth, ""));
|
|
512
476
|
}
|
|
513
477
|
|
|
514
478
|
override render(termWidth: number): string[] {
|
|
@@ -520,9 +484,7 @@ export class AskQuestionnaire extends Container {
|
|
|
520
484
|
const t = this.theme;
|
|
521
485
|
const isMulti = !!this.currentQ.multiSelect;
|
|
522
486
|
const hasPreview =
|
|
523
|
-
!isMulti &&
|
|
524
|
-
this.selectedItem?.kind === "option" &&
|
|
525
|
-
!!this.selectedItem?.option?.preview;
|
|
487
|
+
!isMulti && this.selectedItem?.kind === "option" && !!this.selectedItem?.option?.preview;
|
|
526
488
|
|
|
527
489
|
const useSplit = hasPreview && width >= SPLIT_PANE_MIN_WIDTH;
|
|
528
490
|
const leftWidth = useSplit ? Math.floor((width - 2) * 0.45) : inner;
|
|
@@ -530,8 +492,7 @@ export class AskQuestionnaire extends Container {
|
|
|
530
492
|
|
|
531
493
|
const lines: string[] = [];
|
|
532
494
|
|
|
533
|
-
const row = (content: string): string =>
|
|
534
|
-
truncateToWidth(content, width, "");
|
|
495
|
+
const row = (content: string): string => truncateToWidth(content, width, "");
|
|
535
496
|
|
|
536
497
|
// Tab bar — rendered as the framed top edge (frameLines `top`).
|
|
537
498
|
let top: string | undefined;
|
|
@@ -554,10 +515,7 @@ export class AskQuestionnaire extends Container {
|
|
|
554
515
|
lines.push(row(`${chip}${prog}`));
|
|
555
516
|
|
|
556
517
|
// Question text
|
|
557
|
-
for (const w of wrapTextWithAnsi(
|
|
558
|
-
this.currentQ.question,
|
|
559
|
-
Math.max(10, inner),
|
|
560
|
-
)) {
|
|
518
|
+
for (const w of wrapTextWithAnsi(this.currentQ.question, Math.max(10, inner))) {
|
|
561
519
|
lines.push(row(t.fg("text", t.bold(w))));
|
|
562
520
|
}
|
|
563
521
|
|
|
@@ -601,28 +559,17 @@ export class AskQuestionnaire extends Container {
|
|
|
601
559
|
}
|
|
602
560
|
|
|
603
561
|
// Footer hints
|
|
604
|
-
const navHint =
|
|
605
|
-
this.params.questions.length > 1 ? "↑↓ nav • ←→ question" : "↑↓ nav";
|
|
562
|
+
const navHint = this.params.questions.length > 1 ? "↑↓ nav • ←→ question" : "↑↓ nav";
|
|
606
563
|
const hintParts = isMulti
|
|
607
|
-
? [
|
|
608
|
-
|
|
609
|
-
"ctrl+c cancel",
|
|
610
|
-
]
|
|
611
|
-
: [
|
|
612
|
-
`${navHint} • type filter • enter select • esc clear`,
|
|
613
|
-
"ctrl+c cancel",
|
|
614
|
-
];
|
|
564
|
+
? [`${navHint} • space toggle • enter commit • esc clear`, "ctrl+c cancel"]
|
|
565
|
+
: [`${navHint} • type filter • enter select • esc clear`, "ctrl+c cancel"];
|
|
615
566
|
lines.push(row(dim(t)(hintParts.join(" • "))));
|
|
616
567
|
|
|
617
568
|
return this.frame(mw, lines, top);
|
|
618
569
|
}
|
|
619
570
|
|
|
620
571
|
/** Wrap body lines in the rounded modal border at the given outer width. */
|
|
621
|
-
private frame(
|
|
622
|
-
outerWidth: number,
|
|
623
|
-
lines: string[],
|
|
624
|
-
top: string | undefined,
|
|
625
|
-
): string[] {
|
|
572
|
+
private frame(outerWidth: number, lines: string[], top: string | undefined): string[] {
|
|
626
573
|
const t = this.theme;
|
|
627
574
|
return frameLines({
|
|
628
575
|
width: outerWidth,
|
package/src/rpc.ts
CHANGED
|
@@ -21,9 +21,7 @@ export async function rpcFallback(
|
|
|
21
21
|
const header = q.header;
|
|
22
22
|
|
|
23
23
|
if (q.multiSelect) {
|
|
24
|
-
const lines = q.options.map(
|
|
25
|
-
(o, idx) => `${idx + 1}. ${o.label} — ${o.description}`,
|
|
26
|
-
);
|
|
24
|
+
const lines = q.options.map((o, idx) => `${idx + 1}. ${o.label} — ${o.description}`);
|
|
27
25
|
const raw = await ui.input(
|
|
28
26
|
`${header}: ${q.question}\n\n${lines.join("\n")}\n\nEnter numbers separated by commas:`,
|
|
29
27
|
"e.g. 1,3",
|
|
@@ -36,7 +34,7 @@ export async function rpcFallback(
|
|
|
36
34
|
.split(",")
|
|
37
35
|
.map((s) => Number(s.trim()))
|
|
38
36
|
.filter((n) => n >= 1 && n <= q.options.length);
|
|
39
|
-
const selected = indices.map((n) => q.options[n - 1]?.label);
|
|
37
|
+
const selected = indices.map((n) => q.options[n - 1]?.label ?? "");
|
|
40
38
|
if (selected.length > 0) {
|
|
41
39
|
answers.push({
|
|
42
40
|
questionIndex: i,
|
|
@@ -71,8 +69,7 @@ export async function rpcFallback(
|
|
|
71
69
|
});
|
|
72
70
|
} else {
|
|
73
71
|
const opt = q.options.find(
|
|
74
|
-
(o) =>
|
|
75
|
-
chosen === o.label || `${o.label} — ${o.description}` === chosen,
|
|
72
|
+
(o) => chosen === o.label || `${o.label} — ${o.description}` === chosen,
|
|
76
73
|
);
|
|
77
74
|
answers.push({
|
|
78
75
|
questionIndex: i,
|
package/src/schema.ts
CHANGED
|
@@ -27,8 +27,7 @@ export const OptionSchema = Type.Object({
|
|
|
27
27
|
}),
|
|
28
28
|
preview: Type.Optional(
|
|
29
29
|
Type.String({
|
|
30
|
-
description:
|
|
31
|
-
"Optional markdown preview for side-by-side layout (single-select only).",
|
|
30
|
+
description: "Optional markdown preview for side-by-side layout (single-select only).",
|
|
32
31
|
}),
|
|
33
32
|
),
|
|
34
33
|
});
|
|
@@ -44,14 +43,12 @@ export const QuestionSchema = Type.Object({
|
|
|
44
43
|
options: Type.Array(OptionSchema, {
|
|
45
44
|
minItems: MIN_OPTIONS,
|
|
46
45
|
maxItems: MAX_OPTIONS,
|
|
47
|
-
description:
|
|
48
|
-
"2-4 options. 'Type something.' is auto-appended for single-select.",
|
|
46
|
+
description: "2-4 options. 'Type something.' is auto-appended for single-select.",
|
|
49
47
|
}),
|
|
50
48
|
multiSelect: Type.Optional(
|
|
51
49
|
Type.Boolean({
|
|
52
50
|
default: false,
|
|
53
|
-
description:
|
|
54
|
-
"Allow multiple selections. Suppresses 'Type something.' row.",
|
|
51
|
+
description: "Allow multiple selections. Suppresses 'Type something.' row.",
|
|
55
52
|
}),
|
|
56
53
|
),
|
|
57
54
|
});
|