@mrclrchtr/supi-ask-user 1.4.0 → 1.6.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.
@@ -0,0 +1,400 @@
1
+ import {
2
+ type Component,
3
+ Editor,
4
+ type Focusable,
5
+ Key,
6
+ matchesKey,
7
+ SelectList,
8
+ } from "@earendil-works/pi-tui";
9
+ import type { NormalizedChoiceQuestion } from "../types.ts";
10
+ import { createActionList } from "./overlay-actions.ts";
11
+ import {
12
+ clampIndex,
13
+ currentCustomValue,
14
+ currentPreviewText,
15
+ currentTextValue,
16
+ makeEditorTheme,
17
+ makeSelectListTheme,
18
+ renderOverlayFrame,
19
+ } from "./overlay-render.ts";
20
+ import {
21
+ buildChoiceItems,
22
+ buildChoiceRows,
23
+ type ChoiceRow,
24
+ choiceRowValue,
25
+ defaultChoiceRowIndex,
26
+ type FocusTarget,
27
+ noteTargetLabel,
28
+ type OverlayAction,
29
+ type OverlayMode,
30
+ previewOptionIndexForRows,
31
+ } from "./overlay-view.ts";
32
+ import type { OverlayArgs } from "./types.ts";
33
+
34
+ export class AskUserOverlay implements Component, Focusable {
35
+ focused = false;
36
+ private readonly editor: Editor;
37
+ private focus: FocusTarget = "choices";
38
+ private mode: OverlayMode = "choice";
39
+ private closed = false;
40
+ private cachedWidth: number | undefined;
41
+ private cachedLines: string[] | undefined;
42
+ private readonly onAbort: () => void;
43
+ private choiceRows: ChoiceRow[] = [];
44
+ private choiceRowIndex = 0;
45
+ private previewOptionIndex = 0;
46
+ private choiceList: SelectList | undefined;
47
+ private textActions: Array<{ action: OverlayAction; label: string }> = [];
48
+ private actionIndex = 0;
49
+ private actionList: SelectList | undefined;
50
+ constructor(private readonly args: OverlayArgs) {
51
+ this.editor = new Editor(args.tui, makeEditorTheme(args.theme));
52
+ this.editor.onSubmit = (value) => this.handleEditorSubmit(value);
53
+ this.syncCurrentQuestion();
54
+ this.onAbort = () => {
55
+ this.args.controller.abort();
56
+ this.finish();
57
+ };
58
+ args.signal?.addEventListener("abort", this.onAbort);
59
+ }
60
+
61
+ render(width: number): string[] {
62
+ this.editor.focused = this.focus === "editor";
63
+ if (this.cachedWidth === width && this.cachedLines) return this.cachedLines;
64
+ this.cachedWidth = width;
65
+ this.cachedLines = renderOverlayFrame({
66
+ width,
67
+ theme: this.args.theme,
68
+ controller: this.args.controller,
69
+ mode: this.mode,
70
+ focus: this.focus,
71
+ editor: this.editor,
72
+ choiceRows: this.choiceRows,
73
+ choiceRowIndex: this.choiceRowIndex,
74
+ actionList: this.actionList,
75
+ textActionLabels: this.textActions.map(({ label }) => label),
76
+ previewText: currentPreviewText(
77
+ this.args.controller.currentQuestion,
78
+ this.previewOptionIndex,
79
+ ),
80
+ noteTargetLabel:
81
+ this.mode === "note-input"
82
+ ? noteTargetLabel(this.args.controller, this.choiceRows, this.choiceRowIndex)
83
+ : undefined,
84
+ });
85
+ return this.cachedLines;
86
+ }
87
+
88
+ handleInput(data: string): void {
89
+ if (this.closed || this.args.controller.isTerminal) return;
90
+
91
+ if (this.args.keybindings.matches(data, "app.tools.expand")) {
92
+ this.args.onToggleToolsExpanded?.();
93
+ return;
94
+ }
95
+
96
+ if (this.mode === "note-input" && matchesKey(data, Key.escape)) {
97
+ const question = this.args.controller.currentQuestion;
98
+ const row = this.choiceRows[this.choiceRowIndex];
99
+ if (question.type === "choice" && row?.kind === "option") {
100
+ this.restoreChoiceMode(question, row.optionIndex);
101
+ }
102
+ return;
103
+ }
104
+ if (matchesKey(data, Key.escape)) {
105
+ this.args.controller.cancel();
106
+ this.finish();
107
+ return;
108
+ }
109
+ if (this.mode !== "note-input" && matchesKey(data, Key.left)) {
110
+ if (this.args.controller.goBack()) {
111
+ this.syncCurrentQuestion();
112
+ this.refresh();
113
+ }
114
+ return;
115
+ }
116
+
117
+ switch (this.focus) {
118
+ case "choices":
119
+ this.handleChoiceKey(data);
120
+ return;
121
+ case "actions":
122
+ this.handleActionKey(data);
123
+ return;
124
+ case "editor":
125
+ this.handleEditorKey(data);
126
+ return;
127
+ }
128
+ }
129
+
130
+ invalidate(): void {
131
+ this.cachedLines = undefined;
132
+ this.choiceList?.invalidate();
133
+ this.actionList?.invalidate();
134
+ this.editor.invalidate();
135
+ }
136
+
137
+ dispose(): void {
138
+ this.closed = true;
139
+ this.args.signal?.removeEventListener("abort", this.onAbort);
140
+ }
141
+
142
+ private handleChoiceKey(data: string): void {
143
+ const question = this.args.controller.currentQuestion;
144
+ if (question.type !== "choice" || !this.choiceList) return;
145
+
146
+ if (data === "n") {
147
+ const row = this.choiceRows[this.choiceRowIndex];
148
+ if (row?.kind === "option") this.openNoteEditor(question, row.optionIndex);
149
+ return;
150
+ }
151
+
152
+ if (matchesKey(data, Key.space)) {
153
+ const row = this.choiceRows[this.choiceRowIndex];
154
+ if (row?.kind === "option") this.applyChoiceSelection(question, row.optionIndex, false);
155
+ return;
156
+ }
157
+
158
+ this.choiceList.handleInput(data);
159
+ }
160
+ private handleActionKey(data: string): void {
161
+ const question = this.args.controller.currentQuestion;
162
+ if (!this.actionList) return;
163
+
164
+ if (question.type === "text") {
165
+ if (matchesKey(data, Key.up) && this.actionIndex === 0) {
166
+ this.focus = "editor";
167
+ this.refresh();
168
+ return;
169
+ }
170
+ } else if (matchesKey(data, Key.up) && this.actionIndex === 0) {
171
+ this.focus = "choices";
172
+ this.refresh();
173
+ return;
174
+ }
175
+
176
+ this.actionList.handleInput(data);
177
+ this.refresh();
178
+ }
179
+
180
+ private handleEditorKey(data: string): void {
181
+ if (this.mode === "text" && matchesKey(data, Key.down) && this.textActions.length > 0) {
182
+ this.focus = "actions";
183
+ this.refresh();
184
+ return;
185
+ }
186
+
187
+ this.editor.handleInput(data);
188
+ this.refresh();
189
+ }
190
+
191
+ private handleEditorSubmit(value: string): void {
192
+ const trimmed = value.trim();
193
+ const question = this.args.controller.currentQuestion;
194
+
195
+ if (this.mode === "discuss-input") {
196
+ this.args.controller.finishDiscuss(trimmed || undefined);
197
+ this.finish();
198
+ return;
199
+ }
200
+ if (this.mode === "note-input") {
201
+ if (question.type !== "choice") return;
202
+ const row = this.choiceRows[this.choiceRowIndex];
203
+ if (row?.kind !== "option") return;
204
+ this.args.controller.setChoiceOptionNote(question, row.optionIndex, trimmed || undefined);
205
+ this.restoreChoiceMode(question, row.optionIndex);
206
+ return;
207
+ }
208
+ if (this.mode === "custom-input") {
209
+ if (trimmed.length === 0) return;
210
+ this.args.controller.setAnswer(question.id, { kind: "custom", value: trimmed });
211
+ this.advanceAfterQuestion();
212
+ return;
213
+ }
214
+ if (trimmed.length === 0) {
215
+ if (question.required) return;
216
+ this.args.controller.clearAnswer(question.id);
217
+ this.advanceAfterQuestion();
218
+ return;
219
+ }
220
+
221
+ this.args.controller.setAnswer(question.id, { kind: "text", value: trimmed });
222
+ this.advanceAfterQuestion();
223
+ }
224
+
225
+ private applyChoiceSelection(
226
+ question: NormalizedChoiceQuestion,
227
+ optionIndex: number,
228
+ submit: boolean,
229
+ ): void {
230
+ if (question.multi) {
231
+ this.toggleMultiChoice(question, optionIndex);
232
+ if (submit && this.args.controller.hasAnswer(question.id)) this.advanceAfterQuestion();
233
+ return;
234
+ }
235
+
236
+ this.args.controller.selectChoiceOption(question, optionIndex);
237
+ this.choiceRowIndex = optionIndex;
238
+ this.previewOptionIndex = optionIndex;
239
+ if (submit) {
240
+ this.advanceAfterQuestion();
241
+ return;
242
+ }
243
+ this.buildChoiceList(question);
244
+ this.refresh();
245
+ }
246
+
247
+ private toggleMultiChoice(question: NormalizedChoiceQuestion, optionIndex: number): void {
248
+ this.args.controller.toggleChoiceOption(question, optionIndex);
249
+ this.choiceRowIndex = optionIndex;
250
+ this.previewOptionIndex = optionIndex;
251
+ this.buildChoiceList(question);
252
+ this.refresh();
253
+ }
254
+
255
+ private openNoteEditor(question: NormalizedChoiceQuestion, optionIndex: number): void {
256
+ const option = question.options[optionIndex];
257
+ if (!option) return;
258
+ this.mode = "note-input";
259
+ this.focus = "editor";
260
+ this.choiceRowIndex = optionIndex;
261
+ this.previewOptionIndex = optionIndex;
262
+ this.editor.setText(this.args.controller.getChoiceOptionNote(question.id, option.value) ?? "");
263
+ this.refresh();
264
+ }
265
+
266
+ private restoreChoiceMode(question: NormalizedChoiceQuestion, optionIndex: number): void {
267
+ this.mode = "choice";
268
+ this.focus = "choices";
269
+ this.editor.setText("");
270
+ this.choiceRows = buildChoiceRows(this.args.controller, question);
271
+ const nextIndex = this.choiceRows.findIndex(
272
+ (row) => row.kind === "option" && row.optionIndex === optionIndex,
273
+ );
274
+ this.choiceRowIndex = clampIndex(
275
+ nextIndex >= 0 ? nextIndex : optionIndex,
276
+ this.choiceRows.length,
277
+ );
278
+ this.previewOptionIndex = optionIndex;
279
+ this.buildChoiceList(question);
280
+ this.refresh();
281
+ }
282
+
283
+ private handleAction(action: OverlayAction): void {
284
+ switch (action) {
285
+ case "other":
286
+ this.mode = "custom-input";
287
+ this.focus = "editor";
288
+ this.editor.setText(currentCustomValue(this.args.controller));
289
+ this.refresh();
290
+ return;
291
+ case "skip":
292
+ this.args.controller.clearAnswer(this.args.controller.currentQuestion.id);
293
+ this.advanceAfterQuestion();
294
+ return;
295
+ case "discuss":
296
+ this.mode = "discuss-input";
297
+ this.focus = "editor";
298
+ this.editor.setText("");
299
+ this.refresh();
300
+ return;
301
+ case "partial":
302
+ this.args.controller.finishPartial();
303
+ this.finish();
304
+ return;
305
+ }
306
+ }
307
+
308
+ private advanceAfterQuestion(): void {
309
+ if (!this.args.controller.goNext()) {
310
+ this.args.controller.finishSubmitted();
311
+ this.finish();
312
+ return;
313
+ }
314
+ this.syncCurrentQuestion();
315
+ this.refresh();
316
+ }
317
+
318
+ private syncCurrentQuestion(): void {
319
+ const question = this.args.controller.currentQuestion;
320
+ if (question.type === "text") {
321
+ this.mode = "text";
322
+ this.focus = "editor";
323
+ this.editor.setText(currentTextValue(this.args.controller, question.initial));
324
+ this.buildTextActions();
325
+ this.choiceRows = [];
326
+ return;
327
+ }
328
+
329
+ this.mode = "choice";
330
+ this.focus = "choices";
331
+ this.textActions = [];
332
+ this.actionList = undefined;
333
+ this.editor.setText("");
334
+ this.choiceRows = buildChoiceRows(this.args.controller, question);
335
+ this.choiceRowIndex = clampIndex(
336
+ defaultChoiceRowIndex(this.args.controller, question, this.choiceRows),
337
+ this.choiceRows.length,
338
+ );
339
+ this.previewOptionIndex =
340
+ previewOptionIndexForRows(this.choiceRows, this.choiceRowIndex, this.previewOptionIndex) ?? 0;
341
+ this.buildChoiceList(question);
342
+ }
343
+
344
+ private buildChoiceList(question: NormalizedChoiceQuestion): void {
345
+ const items = buildChoiceItems(this.args.controller, question, this.choiceRows);
346
+
347
+ const list = new SelectList(
348
+ items,
349
+ Math.min(this.choiceRows.length, 10),
350
+ makeSelectListTheme(this.args.theme),
351
+ );
352
+ list.onSelectionChange = (item) => {
353
+ const nextIndex = this.choiceRows.findIndex((row) => choiceRowValue(row) === item.value);
354
+ if (nextIndex < 0) return;
355
+ this.choiceRowIndex = nextIndex;
356
+ this.previewOptionIndex =
357
+ previewOptionIndexForRows(this.choiceRows, nextIndex, this.previewOptionIndex) ??
358
+ this.previewOptionIndex;
359
+ this.refresh();
360
+ };
361
+ list.onSelect = (item) => {
362
+ const row = this.choiceRows.find((candidate) => choiceRowValue(candidate) === item.value);
363
+ if (!row) return;
364
+ if (row.kind === "option") {
365
+ this.applyChoiceSelection(question, row.optionIndex, true);
366
+ return;
367
+ }
368
+ this.handleAction(row.action);
369
+ };
370
+ list.setSelectedIndex(this.choiceRowIndex);
371
+ this.choiceList = list;
372
+ }
373
+
374
+ private buildTextActions(): void {
375
+ const state = createActionList({
376
+ controller: this.args.controller,
377
+ theme: this.args.theme,
378
+ actionIndex: this.actionIndex,
379
+ onIndexChange: (index) => {
380
+ this.actionIndex = index;
381
+ this.refresh();
382
+ },
383
+ onAction: (action) => this.handleAction(action),
384
+ });
385
+ this.textActions = state.entries;
386
+ this.actionList = state.list;
387
+ this.actionIndex = state.index;
388
+ }
389
+
390
+ private refresh(): void {
391
+ this.cachedLines = undefined;
392
+ this.args.tui.requestRender();
393
+ }
394
+ private finish(): void {
395
+ if (this.closed) return;
396
+ this.closed = true;
397
+ this.args.signal?.removeEventListener("abort", this.onAbort);
398
+ this.args.done(this.args.controller.outcome());
399
+ }
400
+ }
@@ -8,7 +8,14 @@ import {
8
8
  } from "@earendil-works/pi-tui";
9
9
  import type { AskUserController } from "../session/controller.ts";
10
10
  import type { NormalizedQuestionnaire } from "../types.ts";
11
- import { type FocusTarget, footerText, type OverlayMode, splitColumns } from "./overlay-view.ts";
11
+ import {
12
+ type ChoiceRow,
13
+ type FocusTarget,
14
+ footerText,
15
+ type OverlayMode,
16
+ renderChoiceList,
17
+ splitColumns,
18
+ } from "./overlay-view.ts";
12
19
 
13
20
  export interface RenderOverlayFrameArgs {
14
21
  width: number;
@@ -17,10 +24,12 @@ export interface RenderOverlayFrameArgs {
17
24
  mode: OverlayMode;
18
25
  focus: FocusTarget;
19
26
  editor: Editor;
20
- choiceList: SelectList | undefined;
27
+ choiceRows: ChoiceRow[];
28
+ choiceRowIndex: number;
21
29
  actionList: SelectList | undefined;
22
30
  textActionLabels: string[];
23
31
  previewText?: string;
32
+ noteTargetLabel?: string;
24
33
  }
25
34
 
26
35
  export function renderOverlayFrame(args: RenderOverlayFrameArgs): string[] {
@@ -114,9 +123,22 @@ function renderBody(args: RenderOverlayFrameArgs): string[] {
114
123
  }
115
124
 
116
125
  function renderChoiceBody(args: RenderOverlayFrameArgs): string[] {
117
- const leftLines = args.choiceList?.render(splitLeftWidth(args.width)) ?? [];
118
-
119
- if (args.mode === "custom-input" || args.mode === "discuss-input") {
126
+ const question = args.controller.currentQuestion;
127
+ const listWidth = splitLeftWidth(args.width);
128
+
129
+ const leftLines =
130
+ question.type === "choice"
131
+ ? renderChoiceList({
132
+ controller: args.controller,
133
+ question,
134
+ rows: args.choiceRows,
135
+ selectedIndex: args.choiceRowIndex,
136
+ theme: args.theme,
137
+ width: listWidth,
138
+ })
139
+ : [];
140
+
141
+ if (args.mode === "custom-input" || args.mode === "discuss-input" || args.mode === "note-input") {
120
142
  const rightLines = renderEditorLines(args, splitRightWidth(args.width));
121
143
  if (args.width >= 100) {
122
144
  return splitColumns({
@@ -169,7 +191,11 @@ function renderEditorLines(args: RenderOverlayFrameArgs, width: number): string[
169
191
  ? "Discuss instead"
170
192
  : args.mode === "custom-input"
171
193
  ? "Other answer"
172
- : "Your answer";
194
+ : args.mode === "note-input"
195
+ ? args.noteTargetLabel
196
+ ? `Note for: ${args.noteTargetLabel}`
197
+ : "Option note"
198
+ : "Your answer";
173
199
 
174
200
  const lines = [args.theme.fg("accent", label), ...args.editor.render(Math.max(20, width - 1))];
175
201
  const question = args.controller.currentQuestion;
@@ -1,12 +1,12 @@
1
1
  import type { Theme } from "@earendil-works/pi-coding-agent";
2
2
  import type { SelectItem } from "@earendil-works/pi-tui";
3
- import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
3
+ import { truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
4
4
  import type { AskUserController } from "../session/controller.ts";
5
5
  import type { NormalizedChoiceQuestion } from "../types.ts";
6
6
 
7
7
  export type OverlayAction = "other" | "skip" | "discuss" | "partial";
8
8
  export type FocusTarget = "choices" | "editor" | "actions";
9
- export type OverlayMode = "choice" | "text" | "custom-input" | "discuss-input";
9
+ export type OverlayMode = "choice" | "text" | "custom-input" | "discuss-input" | "note-input";
10
10
 
11
11
  export type ChoiceRow =
12
12
  | { kind: "option"; optionIndex: number }
@@ -45,8 +45,8 @@ export function buildChoiceItems(
45
45
  question,
46
46
  optionIndex: row.optionIndex,
47
47
  label: option.label,
48
- description: option.description,
49
48
  selectedIndexes,
49
+ hasNote: !!controller.getChoiceOptionNote(question.id, option.value),
50
50
  }),
51
51
  ]
52
52
  : [];
@@ -117,9 +117,12 @@ export function footerText(args: {
117
117
  if (mode === "custom-input" || mode === "discuss-input") {
118
118
  return "Enter submit • Esc cancel";
119
119
  }
120
+ if (mode === "note-input") {
121
+ return "Enter save • Esc close";
122
+ }
120
123
  return question.multi
121
- ? "↑↓ move • Space toggle • Enter submit • ← back • Esc cancel"
122
- : "↑↓ move • Space select • Enter submit • ← back • Esc cancel";
124
+ ? "↑↓ move • Space toggle • Enter submit • n note • ← back • Esc cancel"
125
+ : "↑↓ move • Space select • Enter submit • n note • ← back • Esc cancel";
123
126
  }
124
127
 
125
128
  export function splitColumns(args: {
@@ -147,14 +150,123 @@ export function choiceRowValue(row: ChoiceRow): string {
147
150
  return row.kind === "option" ? `option:${row.optionIndex}` : `action:${row.action}`;
148
151
  }
149
152
 
153
+ export function noteTargetLabel(
154
+ controller: AskUserController,
155
+ choiceRows: ChoiceRow[],
156
+ choiceRowIndex: number,
157
+ ): string | undefined {
158
+ const question = controller.currentQuestion;
159
+ if (question.type !== "choice") return undefined;
160
+ const row = choiceRows[choiceRowIndex];
161
+ if (row?.kind !== "option") return undefined;
162
+ return question.options[row.optionIndex]?.label;
163
+ }
164
+
165
+ function renderOptionRow(args: {
166
+ option: { label: string; description?: string };
167
+ labelText: string;
168
+ hasNote: boolean;
169
+ isSelected: boolean;
170
+ theme: Theme;
171
+ width: number;
172
+ }): string[] {
173
+ const { theme, isSelected, labelText, hasNote, width, option } = args;
174
+ const prefix = isSelected ? "\u2192 " : " ";
175
+ const baseText = isSelected
176
+ ? theme.fg("accent", `${prefix}${labelText}`)
177
+ : `${prefix}${labelText}`;
178
+ const noteSuffix = hasNote ? ` ${theme.fg("accent", "[note]")}` : "";
179
+
180
+ const lines: string[] = [`${baseText}${noteSuffix}`];
181
+
182
+ if (option.description) {
183
+ const descWidth = Math.max(10, width - 2);
184
+ const wrapped = wrapTextWithAnsi(option.description, descWidth);
185
+ for (const descLine of wrapped) {
186
+ lines.push(theme.fg("muted", ` ${descLine}`));
187
+ }
188
+ }
189
+
190
+ return lines;
191
+ }
192
+
193
+ function renderActionRow(args: {
194
+ actionLabel: string;
195
+ isSelected: boolean;
196
+ theme: Theme;
197
+ }): string[] {
198
+ const { theme, isSelected, actionLabel } = args;
199
+ const prefix = isSelected ? "\u2192 " : " ";
200
+ return [isSelected ? theme.fg("accent", `${prefix}${actionLabel}`) : `${prefix}${actionLabel}`];
201
+ }
202
+
203
+ function prepareOptionMarker(
204
+ question: NormalizedChoiceQuestion,
205
+ optionIndex: number,
206
+ selectedIndexes: Set<number>,
207
+ ): string {
208
+ if (question.multi) {
209
+ return selectedIndexes.has(optionIndex) ? "[x]" : "[ ]";
210
+ }
211
+ return selectedIndexes.has(optionIndex) ? "(*)" : "( )";
212
+ }
213
+
214
+ function prepareOptionLabel(
215
+ option: { label: string },
216
+ marker: string,
217
+ recommended: boolean,
218
+ ): string {
219
+ return `${marker} ${option.label}${recommended ? " (recommended)" : ""}`;
220
+ }
221
+
222
+ export function renderChoiceList(args: {
223
+ controller: AskUserController;
224
+ question: NormalizedChoiceQuestion;
225
+ rows: ChoiceRow[];
226
+ selectedIndex: number;
227
+ theme: Theme;
228
+ width: number;
229
+ }): string[] {
230
+ const { controller, question, rows, selectedIndex, theme, width } = args;
231
+ const lines: string[] = [];
232
+ const selectedIndexes = new Set(controller.getSelectedIndexes(question));
233
+
234
+ for (let i = 0; i < rows.length; i++) {
235
+ const row = rows[i];
236
+ const isSelected = i === selectedIndex;
237
+
238
+ if (row.kind === "option") {
239
+ const option = question.options[row.optionIndex];
240
+ if (!option) continue;
241
+
242
+ const marker = prepareOptionMarker(question, row.optionIndex, selectedIndexes);
243
+ const recommended = question.recommendedIndexes.includes(row.optionIndex);
244
+ const hasNote = !!controller.getChoiceOptionNote(question.id, option.value);
245
+ const labelText = prepareOptionLabel(option, marker, recommended);
246
+
247
+ lines.push(...renderOptionRow({ option, labelText, hasNote, isSelected, theme, width }));
248
+ } else {
249
+ const answer = controller.getAnswer(question.id);
250
+ const actionLabelText =
251
+ row.action === "other" && answer?.kind === "custom"
252
+ ? `Other \u2014 ${answer.value}`
253
+ : actionLabel(row.action);
254
+
255
+ lines.push(...renderActionRow({ actionLabel: actionLabelText, isSelected, theme }));
256
+ }
257
+ }
258
+
259
+ return lines;
260
+ }
261
+
150
262
  function buildOptionItem(args: {
151
263
  question: NormalizedChoiceQuestion;
152
264
  optionIndex: number;
153
265
  label: string;
154
- description: string | undefined;
155
266
  selectedIndexes: Set<number>;
267
+ hasNote: boolean;
156
268
  }): SelectItem {
157
- const { question, optionIndex, label, description, selectedIndexes } = args;
269
+ const { question, optionIndex, label, selectedIndexes, hasNote } = args;
158
270
  const recommended = question.recommendedIndexes.includes(optionIndex) ? " (recommended)" : "";
159
271
  const marker = question.multi
160
272
  ? selectedIndexes.has(optionIndex)
@@ -165,8 +277,7 @@ function buildOptionItem(args: {
165
277
  : "( )";
166
278
  return {
167
279
  value: choiceRowValue({ kind: "option", optionIndex }),
168
- label: `${marker} ${label}${recommended}`,
169
- description,
280
+ label: `${marker} ${label}${recommended}${hasNote ? " [note]" : ""}`,
170
281
  };
171
282
  }
172
283