@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.
Files changed (3) hide show
  1. package/README.md +23 -11
  2. package/package.json +1 -1
  3. 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-mono).
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 | Explanation under the label (wraps). For ASCII diagrams/code use `preview` |
65
- | `preview` | string | no | Rich preview shown in a right-hand column when focused. Triggers two-column layout if any option has it |
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
- If **any** option of a question carries a `preview` field, that question
106
- renders in **two columns**: option list on the left, the focused option's
107
- preview on the right. Moving the cursor updates the right pane. Ideal for
108
- comparing ASCII layouts / code samples:
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@d3ara1n/pi-ask-user",
3
- "version": "0.1.0",
3
+ "version": "1.0.0",
4
4
  "description": "Ask-user tool for pi — collapsible question overlay that releases focus so you can scroll the transcript while deciding",
5
5
  "main": "src/index.ts",
6
6
  "keywords": [
package/src/index.ts CHANGED
@@ -62,8 +62,7 @@ interface RenderOption extends QuestionOption {
62
62
  }
63
63
 
64
64
  interface Question {
65
- id: string;
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
- id: string;
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
- label: string;
83
- /** Multi-select labels. */
84
- labels?: string[];
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
- "Optional rich preview (ASCII diagram, code sample, etc.) shown in a dedicated right-hand column when this option is focused. Triggers two-column layout for the whole question if any option has it.",
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.Optional(
150
- Type.String({
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 may carry a description and/or preview.",
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.id, {
325
- id: q.id,
318
+ this.answers.set(q.label, {
319
+ label: q.label,
326
320
  value: trimmed,
327
- label: trimmed,
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.id)) return true;
376
+ if (this.answers.has(q.label)) return true;
381
377
  if (!canSkip(q)) return false; // required question: block
382
- this.answers.set(q.id, {
383
- id: q.id,
378
+ this.answers.set(q.label, {
379
+ label: q.label,
384
380
  value: "",
385
- label: "",
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.id, {
532
- id: q.id,
527
+ this.answers.set(q.label, {
528
+ label: q.label,
533
529
  value: opt.value,
534
- label: opt.label,
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.id);
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.id, {
567
- id: q.id,
562
+ this.answers.set(q.label, {
563
+ label: q.label,
568
564
  value: "",
569
565
  values: picked.map((o) => o.value),
570
- label: "",
571
- labels: picked.map((o) => o.label),
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.id, {
581
- id: q.id,
576
+ this.answers.set(q.label, {
577
+ label: q.label,
582
578
  value: opt.value,
583
- label: opt.label,
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 || `Q${i + 1}`;
650
- const done = this.answers.has(q.id);
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 || `Q${i + 1}`;
681
+ const label = q.label;
676
682
  const active = i === this.currentTab;
677
- const ans = this.answers.get(q.id);
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.labels?.length ? ans.labels : ans.values ?? []).join(", ");
756
- else text = ans.wasCustom ? ans.value : (ans.label || ans.value);
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
- /** Review summary: one row per question showing its answer. */
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
- for (let i = 0; i < n; i++) {
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.id);
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
- const ansW = innerW - labelW - 5; // prefix + ": " + answer + padding
781
- const ansStr = this.formatAnswerText(ans, Math.max(10, ansW), th);
782
- lines.push(row(` ${prefix}${th.fg(headerColor, headerStr)}${th.fg("dim", ": ")}${ansStr}`));
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.id)?.wasCustom;
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.id)!.value : opt.label;
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.id)?.wasCustom;
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.id)!.value : opt.label;
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 rich per-option previews (ASCII diagrams render verbatim in a side column when any option has a `preview` field). The panel is collapsible (Ctrl+\\). Use for clarifying requirements, getting preferences, or confirming decisions.",
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
- // Default id: use `header` when unique, else fall back to `q{n}`.
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.id}: (skipped)`;
1003
- if (a.multiSelect) return `${a.id}: ${a.labels?.join(", ") ?? a.values?.join(", ") ?? ""}`;
1004
- return `${a.id}: ${a.wasCustom ? "(custom) " : ""}${a.label || a.value}`;
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