@d3ara1n/pi-ask-user 0.1.0 → 1.0.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 +23 -11
- package/package.json +1 -1
- package/src/index.ts +90 -78
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @d3ara1n/pi-ask-user
|
|
2
2
|
|
|
3
|
-
A collapsible **ask-user** tool for [pi](https://github.com/earendil-works/pi
|
|
3
|
+
A collapsible **ask-user** tool for [pi](https://github.com/earendil-works/pi).
|
|
4
4
|
|
|
5
5
|
## Why
|
|
6
6
|
|
|
@@ -30,6 +30,7 @@ This tool fixes that:
|
|
|
30
30
|
"questions": [
|
|
31
31
|
{
|
|
32
32
|
"header": "Which layout?",
|
|
33
|
+
"label": "layout",
|
|
33
34
|
"prompt": "Pick the layout for the new settings page.",
|
|
34
35
|
"options": [
|
|
35
36
|
{ "value": "sidebar", "label": "Sidebar", "description": "Nav on the left…" },
|
|
@@ -47,9 +48,8 @@ This tool fixes that:
|
|
|
47
48
|
| Field | Type | Required | Description |
|
|
48
49
|
|-------|------|----------|-------------|
|
|
49
50
|
| `header` | string | yes | Short title shown in the panel header |
|
|
51
|
+
| `label` | string | yes | Short keyword shown on the tab bar and returned to identify this question. Must be unique across all questions in one call |
|
|
50
52
|
| `options` | array | yes | 2–4 options |
|
|
51
|
-
| `id` | string | no | Unique id. Defaults to `q1`, `q2`, … |
|
|
52
|
-
| `label` | string | no | Tab label (multi-question). Defaults to `Q1`, … |
|
|
53
53
|
| `prompt` | string | no | Longer body text under the header |
|
|
54
54
|
| `allowOther` | boolean | no | Allow "Type something." custom input. Default `true` |
|
|
55
55
|
| `multiSelect` | boolean | no | Check multiple options. Default `false` |
|
|
@@ -61,8 +61,8 @@ This tool fixes that:
|
|
|
61
61
|
|-------|------|----------|-------------|
|
|
62
62
|
| `label` | string | yes | Display label |
|
|
63
63
|
| `value` | string | no | Returned value. Defaults to `label` if omitted |
|
|
64
|
-
| `description` | string | no |
|
|
65
|
-
| `preview` | string | no |
|
|
64
|
+
| `description` | string | no | Short explanation under the label (wraps). Add one when the label alone isn't self-explanatory |
|
|
65
|
+
| `preview` | string | no | Only add when `description` alone can't make the option clear — ASCII layout demo, code snippet, or detailed reasoning. Most options need only `description`. Triggers a side column when present |
|
|
66
66
|
|
|
67
67
|
### Icons
|
|
68
68
|
|
|
@@ -102,24 +102,27 @@ allowed so they can review/edit earlier questions.
|
|
|
102
102
|
|
|
103
103
|
### Rich previews
|
|
104
104
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
105
|
+
`description` is the default way to explain an option; reserve `preview` for
|
|
106
|
+
the rare case where a description can't fully convey it. If **any** option of
|
|
107
|
+
a question carries a `preview` field, that question renders in **two columns**:
|
|
108
|
+
option list on the left, the focused option's preview on the right. Moving the
|
|
109
|
+
cursor updates the right pane. Ideal for comparing ASCII layouts / code samples:
|
|
109
110
|
|
|
110
111
|
```jsonc
|
|
111
112
|
{
|
|
112
|
-
"id": "layout",
|
|
113
113
|
"header": "Which layout?",
|
|
114
|
+
"label": "layout",
|
|
114
115
|
"options": [
|
|
115
116
|
{
|
|
116
117
|
"label": "Sidebar",
|
|
117
118
|
"value": "sidebar",
|
|
119
|
+
"description": "Left-side navigation with the main content to its right.",
|
|
118
120
|
"preview": "┌──┬────────┐\n│导│ 正文 │\n│航│ │\n└──┴────────┘\n左侧固定导航"
|
|
119
121
|
},
|
|
120
122
|
{
|
|
121
123
|
"label": "Top bar",
|
|
122
124
|
"value": "topbar",
|
|
125
|
+
"description": "Top horizontal nav with the main content below.",
|
|
123
126
|
"preview": "┌──────────────┐\n│ 导航条 │\n├──────────────┤\n│ 正文 │\n└──────────────┘\n顶部横向导航"
|
|
124
127
|
}
|
|
125
128
|
]
|
|
@@ -133,9 +136,18 @@ contains a newline renders verbatim as a fixed-width block.
|
|
|
133
136
|
|
|
134
137
|
After the last question is answered, a **review screen** lists every question
|
|
135
138
|
and its answer (multi-select answers are comma-joined and truncated with `…`
|
|
136
|
-
when too long; skipped questions show `(skipped)`):
|
|
139
|
+
when too long; skipped questions show `(skipped)`). Each entry spans two rows:
|
|
140
|
+
the question header and, below it, the answer:
|
|
141
|
+
|
|
142
|
+
```
|
|
143
|
+
▸ Which layout?
|
|
144
|
+
Sidebar
|
|
145
|
+
Which database?
|
|
146
|
+
Postgres
|
|
147
|
+
```
|
|
137
148
|
|
|
138
149
|
- `↑`/`↓` — move the cursor between questions
|
|
150
|
+
- `PgUp`/`PgDn` — scroll by page (when there are more questions than rows)
|
|
139
151
|
- `Tab` — jump to the focused question to edit it (returns to the review after)
|
|
140
152
|
- `Enter` — confirm and submit all answers
|
|
141
153
|
- `Esc` — cancel
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -62,8 +62,7 @@ interface RenderOption extends QuestionOption {
|
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
interface Question {
|
|
65
|
-
|
|
66
|
-
label?: string;
|
|
65
|
+
label: string;
|
|
67
66
|
header: string;
|
|
68
67
|
prompt?: string;
|
|
69
68
|
options: QuestionOption[];
|
|
@@ -73,15 +72,15 @@ interface Question {
|
|
|
73
72
|
}
|
|
74
73
|
|
|
75
74
|
interface Answer {
|
|
76
|
-
|
|
75
|
+
label: string;
|
|
77
76
|
/** Single-select: the chosen value. Multi-select: empty string. */
|
|
78
77
|
value: string;
|
|
79
78
|
/** Multi-select: the chosen values. Single-select: absent. */
|
|
80
79
|
values?: string[];
|
|
81
|
-
/** Single-select label. */
|
|
82
|
-
|
|
83
|
-
/** Multi-select labels. */
|
|
84
|
-
|
|
80
|
+
/** Single-select answer label. */
|
|
81
|
+
answerLabel: string;
|
|
82
|
+
/** Multi-select answer labels. */
|
|
83
|
+
answerLabels?: string[];
|
|
85
84
|
wasCustom: boolean;
|
|
86
85
|
index?: number;
|
|
87
86
|
/** True for multi-select answers. */
|
|
@@ -125,37 +124,28 @@ const QuestionOptionSchema = Type.Object({
|
|
|
125
124
|
label: Type.String({ description: "Short display label for the option (shown on the selection row)" }),
|
|
126
125
|
description: Type.Optional(
|
|
127
126
|
Type.String({
|
|
128
|
-
description:
|
|
129
|
-
"Optional short explanation shown under the label (wraps). For ASCII diagrams/code use `preview`.",
|
|
127
|
+
description: "Short explanation shown under the label (wraps). Add one when the label alone isn't self-explanatory.",
|
|
130
128
|
}),
|
|
131
129
|
),
|
|
132
130
|
preview: Type.Optional(
|
|
133
131
|
Type.String({
|
|
134
132
|
description:
|
|
135
|
-
|
|
133
|
+
"Only add this when `description` alone can't make the option clear — e.g. an ASCII layout demo, a code snippet, or detailed reasoning. Omit it otherwise; most options need only `description`. Triggers a side column when present.",
|
|
136
134
|
}),
|
|
137
135
|
),
|
|
138
136
|
});
|
|
139
137
|
|
|
140
138
|
const QuestionSchema = Type.Object({
|
|
141
|
-
id: Type.Optional(
|
|
142
|
-
Type.String({
|
|
143
|
-
description: "Unique identifier for this question. Defaults to `q1`, `q2`, ... if omitted.",
|
|
144
|
-
}),
|
|
145
|
-
),
|
|
146
139
|
header: Type.String({
|
|
147
140
|
description: "Short question title shown in the panel header, e.g. 'Which layout?'",
|
|
148
141
|
}),
|
|
149
|
-
label: Type.
|
|
150
|
-
|
|
151
|
-
description: "Short tab label for multi-question mode (defaults to Q1, Q2, ...)",
|
|
152
|
-
}),
|
|
153
|
-
),
|
|
142
|
+
label: Type.String({
|
|
143
|
+
description: "Short keyword shown on the tab bar and returned to identify this question. Must be unique across all questions in one call." }),
|
|
154
144
|
prompt: Type.Optional(
|
|
155
145
|
Type.String({ description: "Optional longer body text shown under the header" }),
|
|
156
146
|
),
|
|
157
147
|
options: Type.Array(QuestionOptionSchema, {
|
|
158
|
-
description: "Available options. Pass 2-4; each
|
|
148
|
+
description: "Available options. Pass 2-4; each needs a short `label` + a `description`, and a `preview` only when a description can't fully convey the option.",
|
|
159
149
|
}),
|
|
160
150
|
allowOther: Type.Optional(
|
|
161
151
|
Type.Boolean({ description: "Allow a 'Type something.' custom-input option (default: true)" }),
|
|
@@ -277,6 +267,10 @@ class AskUserPanel implements Component, Focusable {
|
|
|
277
267
|
private reviewMode = false;
|
|
278
268
|
/** Cursor row in the review summary. */
|
|
279
269
|
private reviewCursor = 0;
|
|
270
|
+
/** Vertical scroll offset for the review viewport. */
|
|
271
|
+
private reviewScrollOffset = 0;
|
|
272
|
+
/** Visible review rows (recomputed each render). */
|
|
273
|
+
private reviewViewportH = 8;
|
|
280
274
|
/** True after jumping from review mode to edit a question; makes answering
|
|
281
275
|
* return to review instead of advancing to the next question. */
|
|
282
276
|
private editingFromReview = false;
|
|
@@ -321,10 +315,10 @@ class AskUserPanel implements Component, Focusable {
|
|
|
321
315
|
if (tabIndex === this.currentTab) this.invalidate();
|
|
322
316
|
return;
|
|
323
317
|
}
|
|
324
|
-
this.answers.set(q.
|
|
325
|
-
|
|
318
|
+
this.answers.set(q.label, {
|
|
319
|
+
label: q.label,
|
|
326
320
|
value: trimmed,
|
|
327
|
-
|
|
321
|
+
answerLabel: trimmed,
|
|
328
322
|
wasCustom: true,
|
|
329
323
|
});
|
|
330
324
|
st.inputMode = false;
|
|
@@ -355,6 +349,7 @@ class AskUserPanel implements Component, Focusable {
|
|
|
355
349
|
if (this.editingFromReview) {
|
|
356
350
|
this.editingFromReview = false;
|
|
357
351
|
this.reviewMode = true;
|
|
352
|
+
this.reviewScrollOffset = 0;
|
|
358
353
|
this.invalidate();
|
|
359
354
|
return;
|
|
360
355
|
}
|
|
@@ -365,6 +360,7 @@ class AskUserPanel implements Component, Focusable {
|
|
|
365
360
|
// Last question answered: enter review mode instead of submitting.
|
|
366
361
|
this.reviewMode = true;
|
|
367
362
|
this.reviewCursor = 0;
|
|
363
|
+
this.reviewScrollOffset = 0;
|
|
368
364
|
this.invalidate();
|
|
369
365
|
}
|
|
370
366
|
|
|
@@ -377,12 +373,12 @@ class AskUserPanel implements Component, Focusable {
|
|
|
377
373
|
private markSkippedIfNeeded(): boolean {
|
|
378
374
|
const q = this.currentQuestion();
|
|
379
375
|
if (!q) return true;
|
|
380
|
-
if (this.answers.has(q.
|
|
376
|
+
if (this.answers.has(q.label)) return true;
|
|
381
377
|
if (!canSkip(q)) return false; // required question: block
|
|
382
|
-
this.answers.set(q.
|
|
383
|
-
|
|
378
|
+
this.answers.set(q.label, {
|
|
379
|
+
label: q.label,
|
|
384
380
|
value: "",
|
|
385
|
-
|
|
381
|
+
answerLabel: "",
|
|
386
382
|
wasCustom: false,
|
|
387
383
|
skipped: true,
|
|
388
384
|
});
|
|
@@ -528,10 +524,10 @@ class AskUserPanel implements Component, Focusable {
|
|
|
528
524
|
const opt = opts[st.cursor];
|
|
529
525
|
if (opt && !opt.isOther) {
|
|
530
526
|
st.selectedSingle = st.cursor;
|
|
531
|
-
this.answers.set(q.
|
|
532
|
-
|
|
527
|
+
this.answers.set(q.label, {
|
|
528
|
+
label: q.label,
|
|
533
529
|
value: opt.value,
|
|
534
|
-
|
|
530
|
+
answerLabel: opt.label,
|
|
535
531
|
wasCustom: false,
|
|
536
532
|
index: st.cursor,
|
|
537
533
|
});
|
|
@@ -549,7 +545,7 @@ class AskUserPanel implements Component, Focusable {
|
|
|
549
545
|
// Prefill the editor with the committed custom text (if any) so the
|
|
550
546
|
// user can edit rather than retype. Per-tab editor keeps the text
|
|
551
547
|
// for Esc-discard semantics automatically.
|
|
552
|
-
const existing = this.answers.get(q.
|
|
548
|
+
const existing = this.answers.get(q.label);
|
|
553
549
|
if (existing?.wasCustom && existing.value) {
|
|
554
550
|
st.editor.setText(existing.value);
|
|
555
551
|
}
|
|
@@ -563,12 +559,12 @@ class AskUserPanel implements Component, Focusable {
|
|
|
563
559
|
.map((i) => opts[i])
|
|
564
560
|
.filter((o): o is RenderOption => !!o && !o.isOther);
|
|
565
561
|
if (picked.length === 0) return;
|
|
566
|
-
this.answers.set(q.
|
|
567
|
-
|
|
562
|
+
this.answers.set(q.label, {
|
|
563
|
+
label: q.label,
|
|
568
564
|
value: "",
|
|
569
565
|
values: picked.map((o) => o.value),
|
|
570
|
-
|
|
571
|
-
|
|
566
|
+
answerLabel: "",
|
|
567
|
+
answerLabels: picked.map((o) => o.label),
|
|
572
568
|
wasCustom: false,
|
|
573
569
|
multiSelect: true,
|
|
574
570
|
});
|
|
@@ -577,10 +573,10 @@ class AskUserPanel implements Component, Focusable {
|
|
|
577
573
|
}
|
|
578
574
|
// single-select: commit cursor position as the selection
|
|
579
575
|
st.selectedSingle = st.cursor;
|
|
580
|
-
this.answers.set(q.
|
|
581
|
-
|
|
576
|
+
this.answers.set(q.label, {
|
|
577
|
+
label: q.label,
|
|
582
578
|
value: opt.value,
|
|
583
|
-
|
|
579
|
+
answerLabel: opt.label,
|
|
584
580
|
wasCustom: false,
|
|
585
581
|
index: st.cursor,
|
|
586
582
|
});
|
|
@@ -624,6 +620,16 @@ class AskUserPanel implements Component, Focusable {
|
|
|
624
620
|
if (this.reviewCursor < n - 1) { this.reviewCursor++; this.invalidate(); }
|
|
625
621
|
return;
|
|
626
622
|
}
|
|
623
|
+
if (matchesKey(data, Key.pageUp)) {
|
|
624
|
+
this.reviewCursor = Math.max(0, this.reviewCursor - Math.max(1, this.reviewViewportH));
|
|
625
|
+
this.invalidate();
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
if (matchesKey(data, Key.pageDown)) {
|
|
629
|
+
this.reviewCursor = Math.min(n - 1, this.reviewCursor + Math.max(1, this.reviewViewportH));
|
|
630
|
+
this.invalidate();
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
627
633
|
}
|
|
628
634
|
|
|
629
635
|
// ── render ──
|
|
@@ -646,8 +652,8 @@ class AskUserPanel implements Component, Focusable {
|
|
|
646
652
|
const th = this.theme;
|
|
647
653
|
const tabsPart = this.questions
|
|
648
654
|
.map((q, i) => {
|
|
649
|
-
const label = q.label
|
|
650
|
-
const done = this.answers.has(q.
|
|
655
|
+
const label = q.label;
|
|
656
|
+
const done = this.answers.has(q.label);
|
|
651
657
|
return th.fg(done ? "success" : "dim", `${label}${done ? "✓" : "○"}`);
|
|
652
658
|
})
|
|
653
659
|
.join(th.fg("dim", " "));
|
|
@@ -672,9 +678,9 @@ class AskUserPanel implements Component, Focusable {
|
|
|
672
678
|
// ── Tab bar ──
|
|
673
679
|
if (this.questions.length > 1) {
|
|
674
680
|
const tabCells = this.questions.map((q, i) => {
|
|
675
|
-
const label = q.label
|
|
681
|
+
const label = q.label;
|
|
676
682
|
const active = i === this.currentTab;
|
|
677
|
-
const ans = this.answers.get(q.
|
|
683
|
+
const ans = this.answers.get(q.label);
|
|
678
684
|
let mark = " ";
|
|
679
685
|
let baseColor: import("@earendil-works/pi-coding-agent").ThemeColor = active ? "accent" : "muted";
|
|
680
686
|
if (ans?.skipped) { mark = "—"; baseColor = "warning"; }
|
|
@@ -752,8 +758,8 @@ class AskUserPanel implements Component, Focusable {
|
|
|
752
758
|
if (!ans) return th.fg("dim", "(no answer)");
|
|
753
759
|
if (ans.skipped) return th.fg("warning", "(skipped)");
|
|
754
760
|
let text: string;
|
|
755
|
-
if (ans.multiSelect) text = (ans.
|
|
756
|
-
else text = ans.wasCustom ? ans.value : (ans.
|
|
761
|
+
if (ans.multiSelect) text = (ans.answerLabels?.length ? ans.answerLabels : ans.values ?? []).join(", ");
|
|
762
|
+
else text = ans.wasCustom ? ans.value : (ans.answerLabel || ans.value);
|
|
757
763
|
const vw = visibleWidth(text);
|
|
758
764
|
if (vw <= maxW) return th.fg("text", text);
|
|
759
765
|
// truncate: keep prefix, append “…”
|
|
@@ -761,25 +767,46 @@ class AskUserPanel implements Component, Focusable {
|
|
|
761
767
|
return th.fg("text", cut) + th.fg("dim", "…");
|
|
762
768
|
}
|
|
763
769
|
|
|
764
|
-
/**
|
|
770
|
+
/** Clamp the review scroll offset so the cursor stays visible. */
|
|
771
|
+
private clampReviewScroll(): void {
|
|
772
|
+
const n = this.questions.length;
|
|
773
|
+
if (n === 0) return;
|
|
774
|
+
const viewH = this.reviewViewportH;
|
|
775
|
+
if (this.reviewCursor < this.reviewScrollOffset) this.reviewScrollOffset = this.reviewCursor;
|
|
776
|
+
else if (this.reviewCursor >= this.reviewScrollOffset + viewH)
|
|
777
|
+
this.reviewScrollOffset = this.reviewCursor - viewH + 1;
|
|
778
|
+
if (this.reviewScrollOffset < 0) this.reviewScrollOffset = 0;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
/** Review summary: one question per entry, header row + answer row, with
|
|
782
|
+
* viewport scrolling reusing the option-screen layout primitives. */
|
|
765
783
|
private renderReview(width: number, innerW: number, row: (s: string) => string, th: Theme): string[] {
|
|
766
784
|
const lines: string[] = [];
|
|
767
785
|
lines.push(th.fg("border", `╭${"─".repeat(innerW)}╮`));
|
|
768
786
|
lines.push(row(` ${th.fg("accent", th.bold("Review your answers"))}`));
|
|
769
787
|
lines.push(th.fg("border", `├${"─".repeat(innerW)}┤`));
|
|
770
788
|
const n = this.questions.length;
|
|
771
|
-
|
|
789
|
+
this.reviewViewportH = Math.max(3, Math.min(n, 10));
|
|
790
|
+
this.clampReviewScroll();
|
|
791
|
+
const start = this.reviewScrollOffset;
|
|
792
|
+
const end = Math.min(n, start + this.reviewViewportH);
|
|
793
|
+
for (let i = start; i < end; i++) {
|
|
772
794
|
const q = this.questions[i]!;
|
|
773
795
|
const isCursor = i === this.reviewCursor;
|
|
774
|
-
const ans = this.answers.get(q.
|
|
796
|
+
const ans = this.answers.get(q.label);
|
|
797
|
+
// Header row: cursor + label, same coloring as the option screen.
|
|
775
798
|
const prefix = isCursor ? `${th.fg("accent", ICON_CURSOR)} ` : " ";
|
|
776
|
-
const header = q.header;
|
|
777
|
-
const labelW = Math.min(20, innerW - 4);
|
|
778
|
-
const headerStr = truncateToWidth(header, labelW, "");
|
|
779
799
|
const headerColor = isCursor ? "accent" : "muted";
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
800
|
+
lines.push(row(` ${prefix}${th.fg(headerColor, q.header)}`));
|
|
801
|
+
// Answer row: reuse the description renderer's indent/wrap, fed the
|
|
802
|
+
// formatted answer text. Skipped/custom/multi-select all flow through
|
|
803
|
+
// formatAnswerText, so the coloring matches the option screen.
|
|
804
|
+
const maxW = innerW - 2 - " ".length;
|
|
805
|
+
const ansText = this.formatAnswerText(ans, maxW, th);
|
|
806
|
+
lines.push(row(` ${ansText}`));
|
|
807
|
+
}
|
|
808
|
+
if (n > this.reviewViewportH) {
|
|
809
|
+
lines.push(row(th.fg("dim", ` ↑↓/PgUp/PgDn scroll · ${start + 1}-${end}/${n}`)));
|
|
783
810
|
}
|
|
784
811
|
lines.push(th.fg("border", `├${"─".repeat(innerW)}┤`));
|
|
785
812
|
lines.push(row(th.fg("dim", " ↑↓ navigate · Tab edit selected · Enter confirm · Esc cancel")));
|
|
@@ -802,10 +829,10 @@ class AskUserPanel implements Component, Focusable {
|
|
|
802
829
|
const opt = opts[i]!;
|
|
803
830
|
const isCursor = i === st.cursor;
|
|
804
831
|
const prefix = isCursor ? `${th.fg("accent", ICON_CURSOR)} ` : " ";
|
|
805
|
-
const customAnswered = !multi && !!this.answers.get(q.
|
|
832
|
+
const customAnswered = !multi && !!this.answers.get(q.label)?.wasCustom;
|
|
806
833
|
const glyph = this.optionGlyph(opt, i, st, multi, th, isCursor, customAnswered);
|
|
807
834
|
// For "Type something.", show the committed text instead of the placeholder.
|
|
808
|
-
const displayLabel = opt.isOther && customAnswered ? this.answers.get(q.
|
|
835
|
+
const displayLabel = opt.isOther && customAnswered ? this.answers.get(q.label)!.value : opt.label;
|
|
809
836
|
const labelColor = isCursor ? "accent" : opt.isOther ? (customAnswered ? "text" : "dim") : "text";
|
|
810
837
|
const labelText = th.fg(labelColor, displayLabel);
|
|
811
838
|
out.push(row(` ${prefix}${glyph} ${labelText}`));
|
|
@@ -844,13 +871,13 @@ class AskUserPanel implements Component, Focusable {
|
|
|
844
871
|
|
|
845
872
|
// ── build left column lines (options) ──
|
|
846
873
|
const leftLines: string[] = [];
|
|
847
|
-
const customAnswered = !multi && !!this.answers.get(q.
|
|
874
|
+
const customAnswered = !multi && !!this.answers.get(q.label)?.wasCustom;
|
|
848
875
|
for (let i = start; i < end; i++) {
|
|
849
876
|
const opt = opts[i]!;
|
|
850
877
|
const isCursor = i === st.cursor;
|
|
851
878
|
const prefix = isCursor ? `${th.fg("accent", ICON_CURSOR)} ` : " ";
|
|
852
879
|
const glyph = this.optionGlyph(opt, i, st, multi, th, isCursor, customAnswered);
|
|
853
|
-
const displayLabel = opt.isOther && customAnswered ? this.answers.get(q.
|
|
880
|
+
const displayLabel = opt.isOther && customAnswered ? this.answers.get(q.label)!.value : opt.label;
|
|
854
881
|
const labelColor = isCursor ? "accent" : opt.isOther ? (customAnswered ? "text" : "dim") : "text";
|
|
855
882
|
const labelLine = `${prefix}${glyph} ${th.fg(labelColor, displayLabel)}`;
|
|
856
883
|
leftLines.push(truncateToWidth(labelLine, leftW - 1, ""));
|
|
@@ -867,11 +894,6 @@ class AskUserPanel implements Component, Focusable {
|
|
|
867
894
|
for (const ln of cursorOpt.preview.split("\n")) {
|
|
868
895
|
rightLines.push(th.fg("muted", truncateToWidth(ln, rightW - 1, "")));
|
|
869
896
|
}
|
|
870
|
-
} else if (cursorOpt?.description) {
|
|
871
|
-
// Fall back to wrapped description if no preview.
|
|
872
|
-
for (const ln of wrapTextWithAnsi(th.fg("muted", cursorOpt.description), rightW - 1)) {
|
|
873
|
-
rightLines.push(ln);
|
|
874
|
-
}
|
|
875
897
|
} else {
|
|
876
898
|
rightLines.push(th.fg("dim", truncateToWidth("(no preview)", rightW - 1, "")));
|
|
877
899
|
}
|
|
@@ -932,7 +954,7 @@ export default function askUserExtension(pi: ExtensionAPI) {
|
|
|
932
954
|
name: "ask_user",
|
|
933
955
|
label: "Ask User",
|
|
934
956
|
description:
|
|
935
|
-
"Ask the user one or more questions with options. Supports single-select (◎→◉) and multi-select (□→▣, space toggles), per-question 'Type something.' custom input with draft preserved across tab switches, and
|
|
957
|
+
"Ask the user one or more questions with options. Supports single-select (◎→◉) and multi-select (□→▣, space toggles), per-question 'Type something.' custom input with draft preserved across tab switches, and a focused side panel for extended detail (ASCII layouts, code, reasoning) when an option carries a `preview` field. Each option needs a short `label` + a `description` (shown beneath it); add a `preview` field only when a description can't fully convey the option. The panel is collapsible (Ctrl+\\). Use for clarifying requirements, getting preferences, or confirming decisions.",
|
|
936
958
|
parameters: AskUserParams,
|
|
937
959
|
|
|
938
960
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
@@ -943,18 +965,8 @@ export default function askUserExtension(pi: ExtensionAPI) {
|
|
|
943
965
|
return errorResult("Error: No questions provided");
|
|
944
966
|
}
|
|
945
967
|
|
|
946
|
-
|
|
947
|
-
// This keeps the result summary readable for both the LLM and the user
|
|
948
|
-
// (e.g. "颜色: 红色" instead of "q1: 红色").
|
|
949
|
-
const headerCounts = new Map<string, number>();
|
|
950
|
-
for (const q of params.questions) {
|
|
951
|
-
const h = q.id ? null : (q.header ?? "");
|
|
952
|
-
if (h) headerCounts.set(h, (headerCounts.get(h) ?? 0) + 1);
|
|
953
|
-
}
|
|
954
|
-
const questions: Question[] = params.questions.map((q, i) => ({
|
|
968
|
+
const questions: Question[] = params.questions.map((q) => ({
|
|
955
969
|
...q,
|
|
956
|
-
id: q.id || (q.header && (headerCounts.get(q.header) ?? 0) === 1 ? q.header : `q${i + 1}`),
|
|
957
|
-
label: q.label || `Q${i + 1}`,
|
|
958
970
|
allowOther: q.allowOther !== false,
|
|
959
971
|
options: q.options.map((o) => ({ ...o, value: o.value ?? o.label })),
|
|
960
972
|
}));
|
|
@@ -999,9 +1011,9 @@ export default function askUserExtension(pi: ExtensionAPI) {
|
|
|
999
1011
|
? `User cancelled the question(s). ${result.answers.length} question(s) were answered before cancellation.`
|
|
1000
1012
|
: result.answers
|
|
1001
1013
|
.map((a) => {
|
|
1002
|
-
if (a.skipped) return `${a.
|
|
1003
|
-
if (a.multiSelect) return `${a.
|
|
1004
|
-
return `${a.
|
|
1014
|
+
if (a.skipped) return `${a.label}: (skipped)`;
|
|
1015
|
+
if (a.multiSelect) return `${a.label}: ${a.answerLabels?.join(", ") ?? a.values?.join(", ") ?? ""}`;
|
|
1016
|
+
return `${a.label}: ${a.wasCustom ? "(custom) " : ""}${a.answerLabel || a.value}`;
|
|
1005
1017
|
})
|
|
1006
1018
|
.join("\n");
|
|
1007
1019
|
|