pi-ask-user 0.5.2 → 0.6.1

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 CHANGED
@@ -14,6 +14,7 @@ High-quality video: [ask-user-demo.mp4](./media/ask-user-demo.mp4)
14
14
  - Responsive split-pane details preview on wide terminals with single-column fallback on narrow terminals
15
15
  - Multi-select option lists
16
16
  - Optional freeform responses
17
+ - User-toggleable extra context on structured selections
17
18
  - Context display support
18
19
  - Overlay mode — dialog floats over conversation, preserving context
19
20
  - Pi-TUI-aligned keybinding and editor behavior
@@ -62,6 +63,7 @@ The registered tool name is:
62
63
  | `options` | `(string \| {title, description?})[]?` | `[]` | Multiple-choice options |
63
64
  | `allowMultiple` | `boolean?` | `false` | Enable multi-select mode |
64
65
  | `allowFreeform` | `boolean?` | `true` | Add a "Type something" freeform option |
66
+ | `allowComment` | `boolean?` | `false` | Expose a user-toggleable extra-context option in the overlay (`ctrl+g` or the toggle row) and collect an optional comment in fallback dialogs |
65
67
  | `timeout` | `number?` | — | Auto-dismiss after N ms and return `null` if the prompt times out |
66
68
 
67
69
  ## Example usage shape
@@ -75,7 +77,8 @@ The registered tool name is:
75
77
  { "title": "production", "description": "Customer-facing" }
76
78
  ],
77
79
  "allowMultiple": false,
78
- "allowFreeform": true
80
+ "allowFreeform": true,
81
+ "allowComment": true
79
82
  }
80
83
  ```
81
84
 
@@ -84,13 +87,16 @@ The registered tool name is:
84
87
  All tool results include a structured `details` object for rendering and session state reconstruction:
85
88
 
86
89
  ```typescript
90
+ type AskResponse =
91
+ | { kind: "selection"; selections: string[]; comment?: string }
92
+ | { kind: "freeform"; text: string };
93
+
87
94
  interface AskToolDetails {
88
95
  question: string;
89
96
  context?: string;
90
97
  options: QuestionOption[];
91
- answer: string | null;
98
+ response: AskResponse | null;
92
99
  cancelled: boolean;
93
- wasCustom?: boolean;
94
100
  }
95
101
  ```
96
102
 
package/index.ts CHANGED
@@ -41,22 +41,30 @@ interface AskParams {
41
41
  options?: AskOptionInput[];
42
42
  allowMultiple?: boolean;
43
43
  allowFreeform?: boolean;
44
+ allowComment?: boolean;
44
45
  timeout?: number;
45
46
  }
46
47
 
48
+ type AskResponse =
49
+ | {
50
+ kind: "selection";
51
+ selections: string[];
52
+ comment?: string;
53
+ }
54
+ | {
55
+ kind: "freeform";
56
+ text: string;
57
+ };
58
+
47
59
  interface AskToolDetails {
48
60
  question: string;
49
61
  context?: string;
50
62
  options: QuestionOption[];
51
- answer: string | null;
63
+ response: AskResponse | null;
52
64
  cancelled: boolean;
53
- wasCustom?: boolean;
54
65
  }
55
66
 
56
- interface AskUIResult {
57
- answer: string;
58
- wasCustom: boolean;
59
- }
67
+ type AskUIResult = AskResponse;
60
68
 
61
69
  function normalizeOptions(options: AskOptionInput[]): QuestionOption[] {
62
70
  return options
@@ -81,6 +89,54 @@ function formatOptionsForMessage(options: QuestionOption[]): string {
81
89
  .join("\n");
82
90
  }
83
91
 
92
+ function normalizeOptionalComment(text: string | null | undefined): string | undefined {
93
+ const trimmed = text?.trim();
94
+ return trimmed ? trimmed : undefined;
95
+ }
96
+
97
+ function createFreeformResponse(text: string | null | undefined): AskResponse | null {
98
+ const trimmed = text?.trim();
99
+ return trimmed ? { kind: "freeform", text: trimmed } : null;
100
+ }
101
+
102
+ function createSelectionResponse(selections: string[], comment?: string | null): AskResponse | null {
103
+ const normalizedSelections = selections.map((selection) => selection.trim()).filter(Boolean);
104
+ if (normalizedSelections.length === 0) return null;
105
+
106
+ const normalizedComment = normalizeOptionalComment(comment);
107
+ return normalizedComment
108
+ ? { kind: "selection", selections: normalizedSelections, comment: normalizedComment }
109
+ : { kind: "selection", selections: normalizedSelections };
110
+ }
111
+
112
+ function formatResponseSummary(response: AskResponse): string {
113
+ if (response.kind === "freeform") return response.text;
114
+
115
+ const selections = response.selections.join(", ");
116
+ return response.comment ? `${selections} — ${response.comment}` : selections;
117
+ }
118
+
119
+ function buildCommentPrompt(prompt: string, selections: string[]): string {
120
+ const label = selections.length === 1 ? "Selected option" : "Selected options";
121
+ const lines = selections.map((selection) => `- ${selection}`).join("\n");
122
+ return `${prompt}\n\n${label}:\n${lines}`;
123
+ }
124
+
125
+ function parseDialogSelections(input: string): string[] {
126
+ return input
127
+ .split(",")
128
+ .map((selection) => selection.trim())
129
+ .filter(Boolean);
130
+ }
131
+
132
+ function isCancelledInput(value: unknown): value is null | undefined {
133
+ return value === null || value === undefined;
134
+ }
135
+
136
+ function isSelectionResponse(response: AskResponse): response is Extract<AskResponse, { kind: "selection" }> {
137
+ return response.kind === "selection";
138
+ }
139
+
84
140
  function createSelectListTheme(theme: Theme) {
85
141
  return {
86
142
  selectedPrefix: (t: string) => theme.fg("accent", t),
@@ -167,7 +223,11 @@ function literalHint(theme: Theme, key: string, description: string): string {
167
223
  return `${theme.fg("dim", key)}${theme.fg("muted", ` ${description}`)}`;
168
224
  }
169
225
 
170
- type AskMode = "select" | "freeform";
226
+ function isCommentToggleKey(data: string): boolean {
227
+ return matchesKey(data, Key.ctrl("g"));
228
+ }
229
+
230
+ type AskMode = "select" | "freeform" | "comment";
171
231
 
172
232
  const ASK_OVERLAY_MAX_HEIGHT_RATIO = 0.85;
173
233
  const ASK_OVERLAY_WIDTH = "92%";
@@ -177,39 +237,66 @@ const SINGLE_SELECT_SPLIT_PANE_LEFT_MIN_WIDTH = 32;
177
237
  const SINGLE_SELECT_SPLIT_PANE_RIGHT_MIN_WIDTH = 28;
178
238
  const SINGLE_SELECT_SPLIT_PANE_SEPARATOR = " │ ";
179
239
  const FREEFORM_SENTINEL = "\u270f\ufe0f Type custom response...";
240
+ const COMMENT_TOGGLE_LABEL = "Add extra context after selection";
180
241
 
181
242
  class MultiSelectList implements Component {
182
243
  private options: QuestionOption[];
183
244
  private allowFreeform: boolean;
245
+ private allowComment: boolean;
184
246
  private theme: Theme;
185
247
  private keybindings: KeybindingsManager;
186
248
  private selectedIndex = 0;
187
249
  private checked = new Set<number>();
250
+ private commentEnabled = false;
188
251
  private cachedWidth?: number;
189
252
  private cachedLines?: string[];
190
253
 
191
254
  public onCancel?: () => void;
192
- public onSubmit?: (result: string) => void;
255
+ public onSubmit?: (result: string[]) => void;
193
256
  public onEnterFreeform?: () => void;
194
257
 
195
- constructor(options: QuestionOption[], allowFreeform: boolean, theme: Theme, keybindings: KeybindingsManager) {
258
+ constructor(
259
+ options: QuestionOption[],
260
+ allowFreeform: boolean,
261
+ allowComment: boolean,
262
+ theme: Theme,
263
+ keybindings: KeybindingsManager,
264
+ ) {
196
265
  this.options = options;
197
266
  this.allowFreeform = allowFreeform;
267
+ this.allowComment = allowComment;
198
268
  this.theme = theme;
199
269
  this.keybindings = keybindings;
200
270
  }
201
271
 
272
+ public isCommentEnabled(): boolean {
273
+ return this.commentEnabled;
274
+ }
275
+
202
276
  invalidate(): void {
203
277
  this.cachedWidth = undefined;
204
278
  this.cachedLines = undefined;
205
279
  }
206
280
 
207
281
  private getItemCount(): number {
208
- return this.options.length + (this.allowFreeform ? 1 : 0);
282
+ return this.options.length + (this.allowComment ? 1 : 0) + (this.allowFreeform ? 1 : 0);
283
+ }
284
+
285
+ private getCommentToggleIndex(): number | null {
286
+ return this.allowComment ? this.options.length : null;
287
+ }
288
+
289
+ private getFreeformIndex(): number {
290
+ return this.options.length + (this.allowComment ? 1 : 0);
291
+ }
292
+
293
+ private isCommentToggleRow(index: number): boolean {
294
+ const toggleIndex = this.getCommentToggleIndex();
295
+ return toggleIndex !== null && index === toggleIndex;
209
296
  }
210
297
 
211
298
  private isFreeformRow(index: number): boolean {
212
- return this.allowFreeform && index === this.options.length;
299
+ return this.allowFreeform && index === this.getFreeformIndex();
213
300
  }
214
301
 
215
302
  private toggle(index: number): void {
@@ -218,6 +305,12 @@ class MultiSelectList implements Component {
218
305
  else this.checked.add(index);
219
306
  }
220
307
 
308
+ private toggleComment(): void {
309
+ if (!this.allowComment) return;
310
+ this.commentEnabled = !this.commentEnabled;
311
+ this.invalidate();
312
+ }
313
+
221
314
  handleInput(data: string): void {
222
315
  if (this.keybindings.matches(data, "tui.select.cancel")) {
223
316
  this.onCancel?.();
@@ -230,6 +323,11 @@ class MultiSelectList implements Component {
230
323
  return;
231
324
  }
232
325
 
326
+ if (this.allowComment && isCommentToggleKey(data)) {
327
+ this.toggleComment();
328
+ return;
329
+ }
330
+
233
331
  if (this.keybindings.matches(data, "tui.select.up") || matchesKey(data, Key.shift("tab"))) {
234
332
  this.selectedIndex = this.selectedIndex === 0 ? count - 1 : this.selectedIndex - 1;
235
333
  this.invalidate();
@@ -242,7 +340,6 @@ class MultiSelectList implements Component {
242
340
  return;
243
341
  }
244
342
 
245
- // Number keys (1-9) toggle checkboxes for normal items
246
343
  const numMatch = data.match(/^[1-9]$/);
247
344
  if (numMatch) {
248
345
  const idx = Number.parseInt(numMatch[0], 10) - 1;
@@ -255,6 +352,10 @@ class MultiSelectList implements Component {
255
352
  }
256
353
 
257
354
  if (matchesKey(data, Key.space)) {
355
+ if (this.isCommentToggleRow(this.selectedIndex)) {
356
+ this.toggleComment();
357
+ return;
358
+ }
258
359
  if (this.isFreeformRow(this.selectedIndex)) {
259
360
  this.onEnterFreeform?.();
260
361
  return;
@@ -265,6 +366,10 @@ class MultiSelectList implements Component {
265
366
  }
266
367
 
267
368
  if (this.keybindings.matches(data, "tui.select.confirm")) {
369
+ if (this.isCommentToggleRow(this.selectedIndex)) {
370
+ this.toggleComment();
371
+ return;
372
+ }
268
373
  if (this.isFreeformRow(this.selectedIndex)) {
269
374
  this.onEnterFreeform?.();
270
375
  return;
@@ -275,11 +380,10 @@ class MultiSelectList implements Component {
275
380
  .map((i) => this.options[i]?.title)
276
381
  .filter((t): t is string => !!t);
277
382
 
278
- // If nothing checked, fall back to current row
279
383
  const fallback = this.options[this.selectedIndex]?.title;
280
- const result = selectedTitles.length > 0 ? selectedTitles.join(", ") : fallback;
384
+ const result = selectedTitles.length > 0 ? selectedTitles : fallback ? [fallback] : [];
281
385
 
282
- if (result) this.onSubmit?.(result);
386
+ if (result.length > 0) this.onSubmit?.(result);
283
387
  else this.onCancel?.();
284
388
  }
285
389
  }
@@ -308,6 +412,15 @@ class MultiSelectList implements Component {
308
412
  const isSelected = i === this.selectedIndex;
309
413
  const prefix = isSelected ? theme.fg("accent", "→") : " ";
310
414
 
415
+ if (this.isCommentToggleRow(i)) {
416
+ const checkbox = this.commentEnabled ? theme.fg("success", "[✓]") : theme.fg("dim", "[ ]");
417
+ const label = isSelected
418
+ ? theme.fg("accent", theme.bold(COMMENT_TOGGLE_LABEL))
419
+ : theme.fg("text", theme.bold(COMMENT_TOGGLE_LABEL));
420
+ lines.push(truncateToWidth(`${prefix} ${checkbox} ${label}`, width, ""));
421
+ continue;
422
+ }
423
+
311
424
  if (this.isFreeformRow(i)) {
312
425
  const label = theme.fg("text", theme.bold("Type something."));
313
426
  const desc = theme.fg("muted", "Enter a custom response");
@@ -338,7 +451,6 @@ class MultiSelectList implements Component {
338
451
  }
339
452
  }
340
453
 
341
- // Scroll indicator
342
454
  if (startIndex > 0 || endIndex < count) {
343
455
  lines.push(theme.fg("dim", truncateToWidth(` (${this.selectedIndex + 1}/${count})`, width, "")));
344
456
  }
@@ -352,10 +464,12 @@ class MultiSelectList implements Component {
352
464
  class WrappedSingleSelectList implements Component {
353
465
  private options: QuestionOption[];
354
466
  private allowFreeform: boolean;
467
+ private allowComment: boolean;
355
468
  private theme: Theme;
356
469
  private keybindings: KeybindingsManager;
357
470
  private selectedIndex = 0;
358
471
  private searchQuery = "";
472
+ private commentEnabled = false;
359
473
  private maxVisibleRows = 12;
360
474
  private cachedWidth?: number;
361
475
  private cachedLines?: string[];
@@ -364,13 +478,24 @@ class WrappedSingleSelectList implements Component {
364
478
  public onSubmit?: (result: string) => void;
365
479
  public onEnterFreeform?: () => void;
366
480
 
367
- constructor(options: QuestionOption[], allowFreeform: boolean, theme: Theme, keybindings: KeybindingsManager) {
481
+ constructor(
482
+ options: QuestionOption[],
483
+ allowFreeform: boolean,
484
+ allowComment: boolean,
485
+ theme: Theme,
486
+ keybindings: KeybindingsManager,
487
+ ) {
368
488
  this.options = options;
369
489
  this.allowFreeform = allowFreeform;
490
+ this.allowComment = allowComment;
370
491
  this.theme = theme;
371
492
  this.keybindings = keybindings;
372
493
  }
373
494
 
495
+ public isCommentEnabled(): boolean {
496
+ return this.commentEnabled;
497
+ }
498
+
374
499
  setMaxVisibleRows(rows: number): void {
375
500
  const next = Math.max(1, Math.floor(rows));
376
501
  if (next !== this.maxVisibleRows) {
@@ -389,11 +514,21 @@ class WrappedSingleSelectList implements Component {
389
514
  }
390
515
 
391
516
  private getItemCount(filteredOptions: QuestionOption[]): number {
392
- return filteredOptions.length + (this.allowFreeform ? 1 : 0);
517
+ return filteredOptions.length + (this.allowComment ? 1 : 0) + (this.allowFreeform ? 1 : 0);
518
+ }
519
+
520
+ private isCommentToggleRow(index: number, filteredOptions: QuestionOption[]): boolean {
521
+ return this.allowComment && index === filteredOptions.length;
393
522
  }
394
523
 
395
524
  private isFreeformRow(index: number, filteredOptions: QuestionOption[]): boolean {
396
- return this.allowFreeform && index === filteredOptions.length;
525
+ return this.allowFreeform && index === filteredOptions.length + (this.allowComment ? 1 : 0);
526
+ }
527
+
528
+ private toggleComment(): void {
529
+ if (!this.allowComment) return;
530
+ this.commentEnabled = !this.commentEnabled;
531
+ this.invalidate();
397
532
  }
398
533
 
399
534
  private setSearchQuery(query: string): void {
@@ -491,6 +626,8 @@ class WrappedSingleSelectList implements Component {
491
626
  selectedIndex: this.selectedIndex,
492
627
  width,
493
628
  allowFreeform: this.allowFreeform,
629
+ allowComment: this.allowComment,
630
+ commentEnabled: this.commentEnabled,
494
631
  maxRows,
495
632
  hideDescriptions,
496
633
  });
@@ -508,10 +645,13 @@ class WrappedSingleSelectList implements Component {
508
645
  mdTheme = getMarkdownTheme();
509
646
  } catch { }
510
647
 
511
- // Build a markdown string for the preview content
512
648
  let md = "";
513
649
 
514
- if (this.isFreeformRow(this.selectedIndex, filteredOptions)) {
650
+ if (this.isCommentToggleRow(this.selectedIndex, filteredOptions)) {
651
+ md += "## Additional context\n\n";
652
+ md += `Currently: **${this.commentEnabled ? "Enabled" : "Disabled"}**\n\n`;
653
+ md += "Turn this on when the selected option needs extra explanation before the tool submits.\n";
654
+ } else if (this.isFreeformRow(this.selectedIndex, filteredOptions)) {
515
655
  md += "## Custom response\n\n";
516
656
  md += "Open the editor to write **any** answer.\n\n";
517
657
  md += "*Use this when none of the listed options fit.*\n";
@@ -536,7 +676,6 @@ class WrappedSingleSelectList implements Component {
536
676
  }
537
677
  }
538
678
 
539
- // Render via Markdown component if theme is available, otherwise fall back to plain text
540
679
  let lines: string[];
541
680
  if (mdTheme) {
542
681
  const mdComponent = new Markdown(md.trim(), 0, 0, mdTheme);
@@ -548,7 +687,6 @@ class WrappedSingleSelectList implements Component {
548
687
  }
549
688
  }
550
689
 
551
- // Trim trailing blanks
552
690
  while (lines.length > 0 && lines[lines.length - 1]?.trim() === "") {
553
691
  lines.pop();
554
692
  }
@@ -572,6 +710,11 @@ class WrappedSingleSelectList implements Component {
572
710
  return;
573
711
  }
574
712
 
713
+ if (this.allowComment && isCommentToggleKey(data)) {
714
+ this.toggleComment();
715
+ return;
716
+ }
717
+
575
718
  const filteredOptions = this.getFilteredOptions();
576
719
  const count = this.getItemCount(filteredOptions);
577
720
 
@@ -588,16 +731,25 @@ class WrappedSingleSelectList implements Component {
588
731
  }
589
732
 
590
733
  const numMatch = data.match(/^[1-9]$/);
591
- if (numMatch && count > 0) {
734
+ if (numMatch && filteredOptions.length > 0) {
592
735
  const idx = Number.parseInt(numMatch[0], 10) - 1;
593
- if (idx >= 0 && idx < count) {
736
+ if (idx >= 0 && idx < filteredOptions.length) {
594
737
  this.selectedIndex = idx;
595
738
  this.invalidate();
596
739
  return;
597
740
  }
598
741
  }
599
742
 
743
+ if (matchesKey(data, Key.space) && count > 0 && this.isCommentToggleRow(this.selectedIndex, filteredOptions)) {
744
+ this.toggleComment();
745
+ return;
746
+ }
747
+
600
748
  if (this.keybindings.matches(data, "tui.select.confirm") && count > 0) {
749
+ if (this.isCommentToggleRow(this.selectedIndex, filteredOptions)) {
750
+ this.toggleComment();
751
+ return;
752
+ }
601
753
  if (this.isFreeformRow(this.selectedIndex, filteredOptions)) {
602
754
  this.onEnterFreeform?.();
603
755
  return;
@@ -662,12 +814,16 @@ class AskComponent extends Container {
662
814
  private options: QuestionOption[];
663
815
  private allowMultiple: boolean;
664
816
  private allowFreeform: boolean;
817
+ private allowComment: boolean;
665
818
  private tui: TUI;
666
819
  private theme: Theme;
667
820
  private keybindings: KeybindingsManager;
668
821
  private onDone: (result: AskUIResult | null) => void;
669
822
 
670
823
  private mode: AskMode = "select";
824
+ private pendingSelections: string[] = [];
825
+ private freeformDraft = "";
826
+ private commentDraft = "";
671
827
 
672
828
  // Static layout components
673
829
  private titleText: Text;
@@ -688,7 +844,7 @@ class AskComponent extends Container {
688
844
  }
689
845
  set focused(value: boolean) {
690
846
  this._focused = value;
691
- if (this.editor && this.mode === "freeform") {
847
+ if (this.editor && (this.mode === "freeform" || this.mode === "comment")) {
692
848
  (this.editor as any).focused = value;
693
849
  }
694
850
  }
@@ -699,6 +855,7 @@ class AskComponent extends Container {
699
855
  options: QuestionOption[],
700
856
  allowMultiple: boolean,
701
857
  allowFreeform: boolean,
858
+ allowComment: boolean,
702
859
  tui: TUI,
703
860
  theme: Theme,
704
861
  keybindings: KeybindingsManager,
@@ -711,6 +868,7 @@ class AskComponent extends Container {
711
868
  this.options = options;
712
869
  this.allowMultiple = allowMultiple;
713
870
  this.allowFreeform = allowFreeform;
871
+ this.allowComment = allowComment;
714
872
  this.tui = tui;
715
873
  this.theme = theme;
716
874
  this.keybindings = keybindings;
@@ -815,7 +973,8 @@ class AskComponent extends Container {
815
973
 
816
974
  private updateStaticText(): void {
817
975
  const theme = this.theme;
818
- this.titleText.setText(theme.fg("accent", theme.bold("Question")));
976
+ const title = this.mode === "comment" ? "Optional comment" : "Question";
977
+ this.titleText.setText(theme.fg("accent", theme.bold(title)));
819
978
  this.questionText.setText(theme.fg("text", theme.bold(this.question)));
820
979
  if (this.contextComponent && this.context) {
821
980
  if (this.contextComponent instanceof Markdown) {
@@ -832,12 +991,12 @@ class AskComponent extends Container {
832
991
 
833
992
  private updateHelpText(): void {
834
993
  const theme = this.theme;
835
- if (this.mode === "freeform") {
994
+ if (this.mode === "freeform" || this.mode === "comment") {
836
995
  const alternateCancelKeys = this.keybindings
837
996
  .getKeys("tui.select.cancel")
838
997
  .filter((key) => key !== "escape" && key !== "esc");
839
998
  const hints = [
840
- keybindingHint(theme, this.keybindings, "tui.input.submit", "submit"),
999
+ keybindingHint(theme, this.keybindings, "tui.input.submit", this.mode === "comment" ? "submit/skip" : "submit"),
841
1000
  keybindingHint(theme, this.keybindings, "tui.input.newLine", "newline"),
842
1001
  literalHint(theme, "esc", "back"),
843
1002
  alternateCancelKeys.length > 0 ? literalHint(theme, formatKeyList(alternateCancelKeys), "cancel") : null,
@@ -852,9 +1011,12 @@ class AskComponent extends Container {
852
1011
  const hints = [
853
1012
  literalHint(theme, "↑↓", "navigate"),
854
1013
  literalHint(theme, "space", "toggle"),
1014
+ this.allowComment ? literalHint(theme, "ctrl+g", "toggle context") : null,
855
1015
  keybindingHint(theme, this.keybindings, "tui.select.confirm", "submit"),
856
1016
  keybindingHint(theme, this.keybindings, "tui.select.cancel", "cancel"),
857
- ].join(" • ");
1017
+ ]
1018
+ .filter((hint): hint is string => !!hint)
1019
+ .join(" • ");
858
1020
  this.helpText.setText(theme.fg("dim", hints));
859
1021
  } else {
860
1022
  const alternateCancelKeys = this.keybindings
@@ -864,6 +1026,7 @@ class AskComponent extends Container {
864
1026
  literalHint(theme, "type", "filter"),
865
1027
  keybindingHint(theme, this.keybindings, "tui.editor.deleteCharBackward", "erase"),
866
1028
  literalHint(theme, "↑↓", "navigate"),
1029
+ this.allowComment ? literalHint(theme, "ctrl+g", "toggle context") : null,
867
1030
  keybindingHint(theme, this.keybindings, "tui.select.confirm", "select"),
868
1031
  literalHint(theme, "esc", "clear/cancel"),
869
1032
  alternateCancelKeys.length > 0
@@ -879,8 +1042,14 @@ class AskComponent extends Container {
879
1042
  private ensureSingleSelectList(): WrappedSingleSelectList {
880
1043
  if (this.singleSelectList) return this.singleSelectList;
881
1044
 
882
- const list = new WrappedSingleSelectList(this.options, this.allowFreeform, this.theme, this.keybindings);
883
- list.onSubmit = (result) => this.onDone({ answer: result, wasCustom: false });
1045
+ const list = new WrappedSingleSelectList(
1046
+ this.options,
1047
+ this.allowFreeform,
1048
+ this.allowComment,
1049
+ this.theme,
1050
+ this.keybindings,
1051
+ );
1052
+ list.onSubmit = (result) => this.handleSelectionSubmit([result], list.isCommentEnabled());
884
1053
  list.onCancel = () => this.onDone(null);
885
1054
  list.onEnterFreeform = () => this.showFreeformMode();
886
1055
 
@@ -891,9 +1060,15 @@ class AskComponent extends Container {
891
1060
  private ensureMultiSelectList(): MultiSelectList {
892
1061
  if (this.multiSelectList) return this.multiSelectList;
893
1062
 
894
- const list = new MultiSelectList(this.options, this.allowFreeform, this.theme, this.keybindings);
1063
+ const list = new MultiSelectList(
1064
+ this.options,
1065
+ this.allowFreeform,
1066
+ this.allowComment,
1067
+ this.theme,
1068
+ this.keybindings,
1069
+ );
895
1070
  list.onCancel = () => this.onDone(null);
896
- list.onSubmit = (result) => this.onDone({ answer: result, wasCustom: false });
1071
+ list.onSubmit = (result) => this.handleSelectionSubmit(result, list.isCommentEnabled());
897
1072
  list.onEnterFreeform = () => this.showFreeformMode();
898
1073
 
899
1074
  this.multiSelectList = list;
@@ -905,15 +1080,63 @@ class AskComponent extends Container {
905
1080
  const editor = new Editor(this.tui, createEditorTheme(this.theme));
906
1081
  editor.disableSubmit = false;
907
1082
  editor.onSubmit = (text: string) => {
908
- const trimmed = text.trim();
909
- this.onDone(trimmed ? { answer: trimmed, wasCustom: true } : null);
1083
+ this.handleEditorSubmit(text);
910
1084
  };
911
1085
  this.editor = editor;
912
1086
  return editor;
913
1087
  }
914
1088
 
1089
+ private saveEditorDraft(): void {
1090
+ if (!this.editor) return;
1091
+ const getText = (this.editor as any).getText;
1092
+ if (typeof getText !== "function") return;
1093
+
1094
+ const currentText = String(getText.call(this.editor) ?? "");
1095
+ if (this.mode === "freeform") {
1096
+ this.freeformDraft = currentText;
1097
+ } else if (this.mode === "comment") {
1098
+ this.commentDraft = currentText;
1099
+ }
1100
+ }
1101
+
1102
+ private setEditorText(text: string): void {
1103
+ const editor = this.ensureEditor();
1104
+ const setText = (editor as any).setText;
1105
+ if (typeof setText === "function") {
1106
+ setText.call(editor, text);
1107
+ }
1108
+ }
1109
+
1110
+ private handleSelectionSubmit(selections: string[], wantsComment: boolean): void {
1111
+ if (this.allowComment && wantsComment) {
1112
+ this.pendingSelections = selections;
1113
+ this.commentDraft = "";
1114
+ this.showCommentMode();
1115
+ return;
1116
+ }
1117
+
1118
+ this.onDone(createSelectionResponse(selections));
1119
+ }
1120
+
1121
+ private handleEditorSubmit(text: string): void {
1122
+ if (this.mode === "freeform") {
1123
+ this.onDone(createFreeformResponse(text));
1124
+ return;
1125
+ }
1126
+
1127
+ if (this.mode === "comment") {
1128
+ this.commentDraft = text;
1129
+ this.onDone(createSelectionResponse(this.pendingSelections, text));
1130
+ }
1131
+ }
1132
+
915
1133
  private showSelectMode(): void {
1134
+ if (this.mode === "freeform" || this.mode === "comment") {
1135
+ this.saveEditorDraft();
1136
+ }
1137
+
916
1138
  this.mode = "select";
1139
+ this.pendingSelections = [];
917
1140
  this.modeContainer.clear();
918
1141
 
919
1142
  if (this.allowMultiple) {
@@ -928,10 +1151,15 @@ class AskComponent extends Container {
928
1151
  }
929
1152
 
930
1153
  private showFreeformMode(): void {
1154
+ if (this.mode === "comment") {
1155
+ this.saveEditorDraft();
1156
+ }
1157
+
931
1158
  this.mode = "freeform";
932
1159
  this.modeContainer.clear();
933
1160
 
934
1161
  const editor = this.ensureEditor();
1162
+ this.setEditorText(this.freeformDraft);
935
1163
  (editor as any).focused = this._focused;
936
1164
 
937
1165
  this.modeContainer.addChild(new Text(this.theme.fg("accent", this.theme.bold("Custom response")), 1, 0));
@@ -943,8 +1171,31 @@ class AskComponent extends Container {
943
1171
  this.tui.requestRender();
944
1172
  }
945
1173
 
946
- handleInput(data: string): void {
1174
+ private showCommentMode(): void {
947
1175
  if (this.mode === "freeform") {
1176
+ this.saveEditorDraft();
1177
+ }
1178
+
1179
+ this.mode = "comment";
1180
+ this.modeContainer.clear();
1181
+
1182
+ const editor = this.ensureEditor();
1183
+ this.setEditorText(this.commentDraft);
1184
+ (editor as any).focused = this._focused;
1185
+
1186
+ const selectedLabel = this.pendingSelections.length === 1 ? "Selected option:" : "Selected options:";
1187
+ this.modeContainer.addChild(new Text(this.theme.fg("accent", this.theme.bold(selectedLabel)), 1, 0));
1188
+ this.modeContainer.addChild(new Text(this.theme.fg("text", this.pendingSelections.join(", ")), 1, 0));
1189
+ this.modeContainer.addChild(new Spacer(1));
1190
+ this.modeContainer.addChild(editor);
1191
+
1192
+ this.updateHelpText();
1193
+ this.invalidate();
1194
+ this.tui.requestRender();
1195
+ }
1196
+
1197
+ handleInput(data: string): void {
1198
+ if (this.mode === "freeform" || this.mode === "comment") {
948
1199
  if (matchesKey(data, Key.escape)) {
949
1200
  this.showSelectMode();
950
1201
  return;
@@ -960,7 +1211,6 @@ class AskComponent extends Container {
960
1211
  return;
961
1212
  }
962
1213
 
963
- // Selection mode
964
1214
  if (this.allowMultiple) {
965
1215
  this.ensureMultiSelectList().handleInput?.(data);
966
1216
  this.tui.requestRender();
@@ -983,38 +1233,58 @@ async function askViaDialogs(
983
1233
  options: QuestionOption[],
984
1234
  allowMultiple: boolean,
985
1235
  allowFreeform: boolean,
1236
+ allowComment: boolean,
986
1237
  timeout?: number,
987
1238
  ): Promise<AskUIResult | null> {
988
1239
  const dialogOpts = timeout ? { timeout } : undefined;
989
1240
  const prompt = context ? `${question}\n\nContext:\n${context}` : question;
990
1241
 
991
1242
  if (allowMultiple) {
992
- // Multi-select degrades to a freeform input with options listed in the prompt.
993
- // RPC's select() is single-choice only; input() lets the user type freely.
994
1243
  const optionList = formatOptionsForMessage(options);
995
- const answer = await ui.input(
1244
+ const rawSelections = await ui.input(
996
1245
  `${prompt}\n\nOptions (select one or more):\n${optionList}`,
997
1246
  "Type your selection(s)...",
998
1247
  dialogOpts,
999
1248
  ) as string | undefined;
1000
- if (!answer) return null;
1001
- return { answer: answer.trim(), wasCustom: true };
1249
+ if (isCancelledInput(rawSelections)) return null;
1250
+
1251
+ const selections = parseDialogSelections(rawSelections);
1252
+ if (selections.length === 0) return null;
1253
+
1254
+ if (!allowComment) {
1255
+ return createSelectionResponse(selections);
1256
+ }
1257
+
1258
+ const comment = await ui.input(
1259
+ buildCommentPrompt(prompt, selections),
1260
+ "Optional comment (press Enter to skip)...",
1261
+ dialogOpts,
1262
+ ) as string | undefined;
1263
+ return createSelectionResponse(selections, comment);
1002
1264
  }
1003
1265
 
1004
- // Single-select: present options via ctx.ui.select()
1005
1266
  const selectOptions = options.map((o) => o.title);
1006
1267
  if (allowFreeform) selectOptions.push(FREEFORM_SENTINEL);
1007
1268
 
1008
1269
  const selected = await ui.select(prompt, selectOptions, dialogOpts) as string | undefined;
1009
- if (!selected) return null;
1270
+ if (isCancelledInput(selected)) return null;
1010
1271
 
1011
1272
  if (selected === FREEFORM_SENTINEL) {
1012
1273
  const answer = await ui.input(prompt, "Type your answer...", dialogOpts) as string | undefined;
1013
- if (!answer) return null;
1014
- return { answer: answer.trim(), wasCustom: true };
1274
+ if (isCancelledInput(answer)) return null;
1275
+ return createFreeformResponse(answer);
1276
+ }
1277
+
1278
+ if (!allowComment) {
1279
+ return createSelectionResponse([selected]);
1015
1280
  }
1016
1281
 
1017
- return { answer: selected, wasCustom: false };
1282
+ const comment = await ui.input(
1283
+ buildCommentPrompt(prompt, [selected]),
1284
+ "Optional comment (press Enter to skip)...",
1285
+ dialogOpts,
1286
+ ) as string | undefined;
1287
+ return createSelectionResponse([selected], comment);
1018
1288
  }
1019
1289
 
1020
1290
  export default function(pi: ExtensionAPI) {
@@ -1022,12 +1292,14 @@ export default function(pi: ExtensionAPI) {
1022
1292
  name: "ask_user",
1023
1293
  label: "Ask User",
1024
1294
  description:
1025
- "Ask the user a question with optional multiple-choice answers. Use this to gather information interactively. Before calling, gather context with tools (read/web/ref) and pass a short summary via the context field.",
1295
+ "Ask the user a question with optional multiple-choice answers. Use this to gather information interactively. Ask exactly one focused question per call. Before calling, gather context with tools (read/web/ref) and pass a short summary via the context field.",
1026
1296
  promptSnippet:
1027
- "Ask the user a question with optional multiple-choice answers to gather information interactively",
1297
+ "Ask the user one focused question with optional multiple-choice answers to gather information interactively",
1028
1298
  promptGuidelines: [
1029
1299
  "Before calling ask_user, gather context with tools (read/web/ref) and pass a short summary via the context field.",
1030
1300
  "Use ask_user when the user's intent is ambiguous, when a decision requires explicit user input, or when multiple valid options exist.",
1301
+ "Ask exactly one focused question per ask_user call.",
1302
+ "Do not combine multiple numbered, multipart, or unrelated questions into one ask_user prompt.",
1031
1303
  ],
1032
1304
  parameters: Type.Object({
1033
1305
  question: Type.String({ description: "The question to ask the user" }),
@@ -1056,6 +1328,9 @@ export default function(pi: ExtensionAPI) {
1056
1328
  allowFreeform: Type.Optional(
1057
1329
  Type.Boolean({ description: "Add a freeform text option. Default: true" }),
1058
1330
  ),
1331
+ allowComment: Type.Optional(
1332
+ Type.Boolean({ description: "Collect an optional comment after selecting one or more options. Default: false" }),
1333
+ ),
1059
1334
  timeout: Type.Optional(
1060
1335
  Type.Number({ description: "Auto-dismiss after N milliseconds. Returns null (cancelled) when expired." }),
1061
1336
  ),
@@ -1065,7 +1340,7 @@ export default function(pi: ExtensionAPI) {
1065
1340
  if (signal?.aborted) {
1066
1341
  return {
1067
1342
  content: [{ type: "text", text: "Cancelled" }],
1068
- details: { question: params.question, options: [], answer: null, cancelled: true } as AskToolDetails,
1343
+ details: { question: params.question, options: [], response: null, cancelled: true } as AskToolDetails,
1069
1344
  };
1070
1345
  }
1071
1346
 
@@ -1075,6 +1350,7 @@ export default function(pi: ExtensionAPI) {
1075
1350
  options: rawOptions = [],
1076
1351
  allowMultiple = false,
1077
1352
  allowFreeform = true,
1353
+ allowComment = false,
1078
1354
  timeout,
1079
1355
  } = params as AskParams;
1080
1356
  const options = normalizeOptions(rawOptions);
@@ -1083,55 +1359,53 @@ export default function(pi: ExtensionAPI) {
1083
1359
  if (!ctx.hasUI || !ctx.ui) {
1084
1360
  const optionText = options.length > 0 ? `\n\nOptions:\n${formatOptionsForMessage(options)}` : "";
1085
1361
  const freeformHint = allowFreeform ? "\n\nYou can also answer freely." : "";
1362
+ const commentHint = allowComment ? "\n\nAfter choosing an option, you may add an optional comment." : "";
1086
1363
  const contextText = normalizedContext ? `\n\nContext:\n${normalizedContext}` : "";
1087
1364
  return {
1088
1365
  content: [
1089
1366
  {
1090
1367
  type: "text",
1091
- text: `Ask requires interactive mode. Please answer:\n\n${question}${contextText}${optionText}${freeformHint}`,
1368
+ text: `Ask requires interactive mode. Please answer:\n\n${question}${contextText}${optionText}${freeformHint}${commentHint}`,
1092
1369
  },
1093
1370
  ],
1094
1371
  isError: true,
1095
- details: { question, context: normalizedContext, options, answer: null, cancelled: true } as AskToolDetails,
1372
+ details: { question, context: normalizedContext, options, response: null, cancelled: true } as AskToolDetails,
1096
1373
  };
1097
1374
  }
1098
1375
 
1099
- // If no options provided, fall back to freeform input prompt.
1100
1376
  if (options.length === 0) {
1101
1377
  const prompt = normalizedContext ? `${question}\n\nContext:\n${normalizedContext}` : question;
1102
1378
  const answer = await ctx.ui.input(prompt, "Type your answer...", timeout ? { timeout } : undefined);
1379
+ const response = createFreeformResponse(answer);
1103
1380
 
1104
- if (!answer) {
1381
+ if (!response) {
1105
1382
  return {
1106
1383
  content: [{ type: "text", text: "User cancelled the question" }],
1107
- details: { question, context: normalizedContext, options, answer: null, cancelled: true } as AskToolDetails,
1384
+ details: { question, context: normalizedContext, options, response: null, cancelled: true } as AskToolDetails,
1108
1385
  };
1109
1386
  }
1110
1387
 
1111
- pi.events.emit("ask:answered", { question, context: normalizedContext, answer, wasCustom: true });
1388
+ pi.events.emit("ask:answered", { question, context: normalizedContext, response });
1112
1389
  return {
1113
- content: [{ type: "text", text: `User answered: ${answer}` }],
1114
- details: { question, context: normalizedContext, options, answer, cancelled: false, wasCustom: true } as AskToolDetails,
1390
+ content: [{ type: "text", text: `User answered: ${formatResponseSummary(response)}` }],
1391
+ details: { question, context: normalizedContext, options, response, cancelled: false } as AskToolDetails,
1115
1392
  };
1116
1393
  }
1117
1394
 
1118
1395
  onUpdate?.({
1119
1396
  content: [{ type: "text", text: "Waiting for user input..." }],
1120
- details: { question, context: normalizedContext, options, answer: null, cancelled: false },
1397
+ details: { question, context: normalizedContext, options, response: null, cancelled: false },
1121
1398
  });
1122
1399
 
1123
1400
  let result: AskUIResult | null;
1124
1401
  try {
1125
- // custom() returns undefined in RPC/headless mode — fall back to dialog methods
1126
1402
  const customResult = await ctx.ui.custom<AskUIResult | null>(
1127
1403
  (tui, theme, keybindings, done) => {
1128
- // Wire AbortSignal so agent cancellation auto-dismisses the overlay
1129
1404
  if (signal) {
1130
1405
  const onAbort = () => done(null);
1131
1406
  signal.addEventListener("abort", onAbort, { once: true });
1132
1407
  }
1133
1408
 
1134
- // Wire timeout for overlay mode
1135
1409
  if (timeout && timeout > 0) {
1136
1410
  setTimeout(() => done(null), timeout);
1137
1411
  }
@@ -1142,6 +1416,7 @@ export default function(pi: ExtensionAPI) {
1142
1416
  options,
1143
1417
  allowMultiple,
1144
1418
  allowFreeform,
1419
+ allowComment,
1145
1420
  tui,
1146
1421
  theme,
1147
1422
  keybindings,
@@ -1164,7 +1439,7 @@ export default function(pi: ExtensionAPI) {
1164
1439
  result = customResult;
1165
1440
  } else {
1166
1441
  // RPC/headless mode: degrade to select()/input() dialog protocol
1167
- result = await askViaDialogs(ctx.ui, question, normalizedContext, options, allowMultiple, allowFreeform, timeout);
1442
+ result = await askViaDialogs(ctx.ui, question, normalizedContext, options, allowMultiple, allowFreeform, allowComment, timeout);
1168
1443
  }
1169
1444
  } catch (error) {
1170
1445
  const message =
@@ -1180,25 +1455,23 @@ export default function(pi: ExtensionAPI) {
1180
1455
  pi.events.emit("ask:cancelled", { question, context: normalizedContext, options });
1181
1456
  return {
1182
1457
  content: [{ type: "text", text: "User cancelled the question" }],
1183
- details: { question, context: normalizedContext, options, answer: null, cancelled: true } as AskToolDetails,
1458
+ details: { question, context: normalizedContext, options, response: null, cancelled: true } as AskToolDetails,
1184
1459
  };
1185
1460
  }
1186
1461
 
1187
1462
  pi.events.emit("ask:answered", {
1188
1463
  question,
1189
1464
  context: normalizedContext,
1190
- answer: result.answer,
1191
- wasCustom: result.wasCustom,
1465
+ response: result,
1192
1466
  });
1193
1467
  return {
1194
- content: [{ type: "text", text: `User answered: ${result.answer}` }],
1468
+ content: [{ type: "text", text: `User answered: ${formatResponseSummary(result)}` }],
1195
1469
  details: {
1196
1470
  question,
1197
1471
  context: normalizedContext,
1198
1472
  options,
1199
- answer: result.answer,
1473
+ response: result,
1200
1474
  cancelled: false,
1201
- wasCustom: result.wasCustom,
1202
1475
  } as AskToolDetails,
1203
1476
  };
1204
1477
  },
@@ -1217,6 +1490,9 @@ export default function(pi: ExtensionAPI) {
1217
1490
  if (args.allowMultiple) {
1218
1491
  text += theme.fg("dim", " [multi-select]");
1219
1492
  }
1493
+ if (args.allowComment) {
1494
+ text += theme.fg("dim", " [optional comment]");
1495
+ }
1220
1496
  return new Text(text, 0, 0);
1221
1497
  },
1222
1498
 
@@ -1236,36 +1512,34 @@ export default function(pi: ExtensionAPI) {
1236
1512
  return new Text(theme.fg("muted", waitingText), 0, 0);
1237
1513
  }
1238
1514
 
1239
- if (!details || details.cancelled) {
1515
+ if (!details || details.cancelled || !details.response) {
1240
1516
  return new Text(theme.fg("warning", "Cancelled"), 0, 0);
1241
1517
  }
1242
1518
 
1243
- const answer = details.answer ?? "";
1519
+ const response = details.response;
1244
1520
  let text = theme.fg("success", "✓ ");
1245
- if (details.wasCustom) {
1521
+ if (response.kind === "freeform") {
1246
1522
  text += theme.fg("muted", "(wrote) ");
1247
1523
  }
1248
- text += theme.fg("accent", answer);
1524
+ text += theme.fg("accent", formatResponseSummary(response));
1249
1525
 
1250
1526
  if (options.expanded) {
1251
- const selectedTitles = new Set(
1252
- answer
1253
- .split(",")
1254
- .map((value) => value.trim())
1255
- .filter(Boolean),
1256
- );
1257
1527
  text += "\n" + theme.fg("dim", `Q: ${details.question}`);
1258
1528
  if (details.context) {
1259
1529
  text += "\n" + theme.fg("dim", details.context);
1260
1530
  }
1261
- if (details.options && details.options.length > 0) {
1531
+
1532
+ if (isSelectionResponse(response) && details.options.length > 0) {
1533
+ const selectedTitles = new Set(response.selections);
1262
1534
  text += "\n" + theme.fg("dim", "Options:");
1263
1535
  for (const opt of details.options) {
1264
1536
  const desc = opt.description ? ` — ${opt.description}` : "";
1265
- const isSelected = opt.title === answer || selectedTitles.has(opt.title);
1266
- const marker = isSelected ? theme.fg("success", "●") : theme.fg("dim", "○");
1537
+ const marker = selectedTitles.has(opt.title) ? theme.fg("success", "●") : theme.fg("dim", "○");
1267
1538
  text += `\n ${marker} ${theme.fg("dim", opt.title)}${theme.fg("dim", desc)}`;
1268
1539
  }
1540
+ if (response.comment) {
1541
+ text += `\n${theme.fg("dim", "Comment:")} ${theme.fg("dim", response.comment)}`;
1542
+ }
1269
1543
  }
1270
1544
  }
1271
1545
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-ask-user",
3
- "version": "0.5.2",
3
+ "version": "0.6.1",
4
4
  "description": "Interactive ask_user tool for pi-coding-agent with searchable split-pane selection UI, multi-select, and freeform input",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -13,6 +13,8 @@ export interface RenderSingleSelectRowsParams {
13
13
  selectedIndex: number;
14
14
  width: number;
15
15
  allowFreeform: boolean;
16
+ allowComment?: boolean;
17
+ commentEnabled?: boolean;
16
18
  maxRows?: number;
17
19
  hideDescriptions?: boolean;
18
20
  }
@@ -70,25 +72,36 @@ interface ItemBlock {
70
72
  lines: string[];
71
73
  }
72
74
 
75
+ type ListItem =
76
+ | { type: "option"; option: QuestionOption }
77
+ | { type: "comment-toggle"; option: QuestionOption }
78
+ | { type: "freeform"; option: QuestionOption };
79
+
73
80
  function buildItemBlocks(
74
81
  options: QuestionOption[],
75
82
  width: number,
76
83
  allowFreeform: boolean,
84
+ allowComment: boolean,
85
+ commentEnabled: boolean,
77
86
  selectedIndex: number,
78
87
  hideDescriptions = false,
79
88
  ): ItemBlock[] {
80
89
  const normalizedWidth = Math.max(12, width);
81
90
  const freeformLabel = "Type something. — Enter a custom response";
82
- const allItems = options.map((option) => ({ type: "option" as const, option }));
91
+ const commentToggleLabel = `${commentEnabled ? "[✓]" : "[ ]"} Add extra context after selection`;
92
+ const allItems: ListItem[] = options.map((option) => ({ type: "option", option }));
93
+ if (allowComment) {
94
+ allItems.push({ type: "comment-toggle", option: { title: commentToggleLabel } });
95
+ }
83
96
  if (allowFreeform) {
84
- allItems.push({ type: "freeform" as const, option: { title: freeformLabel } });
97
+ allItems.push({ type: "freeform", option: { title: freeformLabel } });
85
98
  }
86
99
 
87
100
  return allItems.map((item, itemIndex) => {
88
101
  const pointer = itemIndex === selectedIndex ? "→" : " ";
89
102
  const lines: string[] = [];
90
103
 
91
- if (item.type === "freeform") {
104
+ if (item.type === "comment-toggle" || item.type === "freeform") {
92
105
  const prefix = `${pointer} `;
93
106
  const wrapped = wrapText(item.option.title, Math.max(8, normalizedWidth - prefix.length));
94
107
  wrapped.forEach((line, lineIndex) => {
@@ -133,11 +146,13 @@ export function renderSingleSelectRows({
133
146
  selectedIndex,
134
147
  width,
135
148
  allowFreeform,
149
+ allowComment = false,
150
+ commentEnabled = false,
136
151
  maxRows,
137
152
  hideDescriptions,
138
153
  }: RenderSingleSelectRowsParams): AnnotatedRow[] {
139
- const itemCount = options.length + (allowFreeform ? 1 : 0);
140
- const blocks = buildItemBlocks(options, width, allowFreeform, selectedIndex, hideDescriptions);
154
+ const itemCount = options.length + (allowComment ? 1 : 0) + (allowFreeform ? 1 : 0);
155
+ const blocks = buildItemBlocks(options, width, allowFreeform, allowComment, commentEnabled, selectedIndex, hideDescriptions);
141
156
  const allRows = flatten(blocks, selectedIndex);
142
157
 
143
158
  if (!Number.isFinite(maxRows) || !maxRows || maxRows <= 0 || allRows.length <= maxRows) {