pi-ask-popup 0.1.0 → 0.1.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.
Files changed (35) hide show
  1. package/README.md +95 -32
  2. package/docs/configuration.md +93 -0
  3. package/docs/hosts.md +71 -0
  4. package/docs/keyboard.md +65 -0
  5. package/docs/tool-schema.md +124 -0
  6. package/package.json +12 -2
  7. package/preview/popup-submit.webp +0 -0
  8. package/preview/popup-with-notes.webp +0 -0
  9. package/preview/popup-with-tab.webp +0 -0
  10. package/src/ask-user-question.ts +50 -24
  11. package/src/config.ts +66 -23
  12. package/src/rpc-fallback.ts +30 -15
  13. package/src/state/build-questionnaire.ts +3 -1
  14. package/src/state/external-editor.ts +6 -2
  15. package/src/state/key-router.ts +129 -41
  16. package/src/state/questionnaire-session.ts +35 -13
  17. package/src/state/row-intent.ts +6 -2
  18. package/src/state/selectors/derivations.ts +18 -6
  19. package/src/state/selectors/focus.ts +6 -2
  20. package/src/state/selectors/projections.ts +3 -1
  21. package/src/state/state-reducer.ts +99 -33
  22. package/src/tool/response-envelope.ts +32 -20
  23. package/src/tool/types.ts +13 -2
  24. package/src/view/component-binding.ts +3 -1
  25. package/src/view/components/inline-input.ts +3 -1
  26. package/src/view/components/multi-select-view.ts +15 -5
  27. package/src/view/components/preview/markdown-content-cache.ts +21 -6
  28. package/src/view/components/preview/preview-block-renderer.ts +8 -1
  29. package/src/view/components/preview/preview-box-renderer.ts +4 -5
  30. package/src/view/components/preview/preview-layout-decider.ts +20 -15
  31. package/src/view/components/preview/preview-pane.ts +54 -18
  32. package/src/view/components/wrapping-select.ts +15 -5
  33. package/src/view/dialog-builder.ts +24 -8
  34. package/src/view/props-adapter.ts +21 -7
  35. package/src/view/tab-content-strategy.ts +60 -20
@@ -119,17 +119,25 @@ export class PreviewPane implements StatefulView<PreviewPaneProps>, Component {
119
119
  }
120
120
 
121
121
  render(width: number): string[] {
122
- if (this.question.multiSelect === true) return this.optionListView.render(width);
122
+ if (this.question.multiSelect === true) {
123
+ return this.optionListView.render(width);
124
+ }
123
125
  // Spec: hide the preview pane entirely when no option carries a `preview`.
124
- if (!this.previewBlock.hasAnyPreview()) return this.optionListView.render(width);
126
+ if (!this.previewBlock.hasAnyPreview()) {
127
+ return this.optionListView.render(width);
128
+ }
125
129
  // `inputMode` (typing on the "other" custom-answer row): the preview is irrelevant —
126
130
  // the row sits at index `options.length`, out of bounds for any option's preview — so
127
131
  // render the option list at the full pane width instead of the cramped left column.
128
132
  // Side-by-side + preview block resume verbatim on nav-away (inputMode clears).
129
- if (this.props.inputMode) return this.optionListView.render(width);
133
+ if (this.props.inputMode) {
134
+ return this.optionListView.render(width);
135
+ }
130
136
 
131
137
  const mode = decideLayout(this.getTerminalWidth(), width);
132
- if (mode === "side-by-side") return this.renderSideBySide(width, mode);
138
+ if (mode === "side-by-side") {
139
+ return this.renderSideBySide(width, mode);
140
+ }
133
141
 
134
142
  // Stacked: options + blank gap + preview block.
135
143
  return [
@@ -146,24 +154,38 @@ export class PreviewPane implements StatefulView<PreviewPaneProps>, Component {
146
154
  }
147
155
 
148
156
  focusedItemRowRange(width: number): [number, number] {
149
- if (this.question.multiSelect === true) return this.optionListView.focusedItemRowRange(width);
150
- if (!this.previewBlock.hasAnyPreview()) return this.optionListView.focusedItemRowRange(width);
157
+ if (this.question.multiSelect === true) {
158
+ return this.optionListView.focusedItemRowRange(width);
159
+ }
160
+ if (!this.previewBlock.hasAnyPreview()) {
161
+ return this.optionListView.focusedItemRowRange(width);
162
+ }
151
163
  // `inputMode`: compute the focused row range against the FULL pane width (not the
152
164
  // side-by-side leftWidth), mirroring `render`'s full-width option list.
153
- if (this.props.inputMode) return this.optionListView.focusedItemRowRange(width);
165
+ if (this.props.inputMode) {
166
+ return this.optionListView.focusedItemRowRange(width);
167
+ }
154
168
  const mode = decideLayout(this.getTerminalWidth(), width);
155
- if (mode === "stacked") return this.optionListView.focusedItemRowRange(width);
169
+ if (mode === "stacked") {
170
+ return this.optionListView.focusedItemRowRange(width);
171
+ }
156
172
  const adaptiveLeft = this.getAdaptiveLeft(width);
157
173
  const { leftWidth } = columnWidths(width, adaptiveLeft);
158
174
  return this.optionListView.focusedItemRowRange(leftWidth);
159
175
  }
160
176
 
161
177
  naturalHeight(width: number): number {
162
- if (this.question.multiSelect === true) return this.optionListView.render(width).length;
163
- if (!this.previewBlock.hasAnyPreview()) return this.optionListView.render(width).length;
178
+ if (this.question.multiSelect === true) {
179
+ return this.optionListView.render(width).length;
180
+ }
181
+ if (!this.previewBlock.hasAnyPreview()) {
182
+ return this.optionListView.render(width).length;
183
+ }
164
184
  // `inputMode`: height is the full-width option list only (no preview block) — preserves
165
185
  // the `naturalHeight === render.length` parity invariant.
166
- if (this.props.inputMode) return this.optionListView.render(width).length;
186
+ if (this.props.inputMode) {
187
+ return this.optionListView.render(width).length;
188
+ }
167
189
  const mode = decideLayout(this.getTerminalWidth(), width);
168
190
  const adaptiveLeft = this.getAdaptiveLeft(width);
169
191
  const { optionsWidth, previewWidth } = bodyWidths(width, mode, adaptiveLeft);
@@ -173,16 +195,24 @@ export class PreviewPane implements StatefulView<PreviewPaneProps>, Component {
173
195
  this.props.selectedIndex,
174
196
  mode,
175
197
  );
176
- if (mode === "side-by-side") return Math.max(optionsHeight, previewBlockHeight);
198
+ if (mode === "side-by-side") {
199
+ return Math.max(optionsHeight, previewBlockHeight);
200
+ }
177
201
  return optionsHeight + STACKED_GAP_ROWS + previewBlockHeight;
178
202
  }
179
203
 
180
204
  maxNaturalHeight(width: number): number {
181
- if (this.question.multiSelect === true) return this.optionListView.render(width).length;
182
- if (!this.previewBlock.hasAnyPreview()) return this.optionListView.render(width).length;
205
+ if (this.question.multiSelect === true) {
206
+ return this.optionListView.render(width).length;
207
+ }
208
+ if (!this.previewBlock.hasAnyPreview()) {
209
+ return this.optionListView.render(width).length;
210
+ }
183
211
  // `inputMode`: like naturalHeight — full-width option list only, so the
184
212
  // `maxNaturalHeight >= naturalHeight` parity invariant holds (both equal the list height).
185
- if (this.props.inputMode) return this.optionListView.render(width).length;
213
+ if (this.props.inputMode) {
214
+ return this.optionListView.render(width).length;
215
+ }
186
216
  const mode = decideLayout(this.getTerminalWidth(), width);
187
217
  const adaptiveLeft = this.getAdaptiveLeft(width);
188
218
  const { optionsWidth, previewWidth } = bodyWidths(width, mode, adaptiveLeft);
@@ -190,9 +220,13 @@ export class PreviewPane implements StatefulView<PreviewPaneProps>, Component {
190
220
  let maxPreviewBlock = 0;
191
221
  for (let i = 0; i < this.question.options.length; i++) {
192
222
  const h = this.previewBlock.blockHeight(previewWidth, i, mode);
193
- if (h > maxPreviewBlock) maxPreviewBlock = h;
223
+ if (h > maxPreviewBlock) {
224
+ maxPreviewBlock = h;
225
+ }
226
+ }
227
+ if (mode === "side-by-side") {
228
+ return Math.max(optionsHeight, maxPreviewBlock);
194
229
  }
195
- if (mode === "side-by-side") return Math.max(optionsHeight, maxPreviewBlock);
196
230
  return optionsHeight + STACKED_GAP_ROWS + maxPreviewBlock;
197
231
  }
198
232
 
@@ -227,7 +261,9 @@ export class PreviewPane implements StatefulView<PreviewPaneProps>, Component {
227
261
  const boxWidth = Math.max(1, visibleWidth(contentLines[0] ?? ""));
228
262
  const boxAlignedPad = Math.max(PREVIEW_PADDING_LEFT, colWidth - boxWidth);
229
263
  return contentLines.map((line) => {
230
- if (line === "") return "";
264
+ if (line === "") {
265
+ return "";
266
+ }
231
267
  // A line wider than the box (a long-locale notes affordance) slides left to
232
268
  // stay fully visible; truncation engages only when the column itself runs out.
233
269
  const pad = Math.max(
@@ -130,7 +130,9 @@ export class WrappingSelect implements Component {
130
130
  invalidate(): void {}
131
131
 
132
132
  render(width: number): string[] {
133
- if (this.items.length === 0) return [];
133
+ if (this.items.length === 0) {
134
+ return [];
135
+ }
134
136
 
135
137
  const { startIndex, endIndex } = this.computeVisibleWindow();
136
138
  const numberWidth = String(Math.max(1, this.totalItemsForNumbering)).length;
@@ -138,7 +140,9 @@ export class WrappingSelect implements Component {
138
140
 
139
141
  for (let i = startIndex; i < endIndex; i++) {
140
142
  const item = this.items[i];
141
- if (!item) continue;
143
+ if (!item) {
144
+ continue;
145
+ }
142
146
  const isActive = i === this.selectedIndex && this.focused;
143
147
  lines.push(...this.renderItem(item, i, isActive, width, numberWidth));
144
148
  }
@@ -155,13 +159,17 @@ export class WrappingSelect implements Component {
155
159
  * summing per-item row counts — O(maxVisible) per call.
156
160
  */
157
161
  focusedItemRowRange(width: number): [number, number] {
158
- if (this.items.length === 0) return [0, 0];
162
+ if (this.items.length === 0) {
163
+ return [0, 0];
164
+ }
159
165
  const { startIndex, endIndex } = this.computeVisibleWindow();
160
166
  const numberWidth = String(Math.max(1, this.totalItemsForNumbering)).length;
161
167
  let row = 0;
162
168
  for (let i = startIndex; i < endIndex; i++) {
163
169
  const item = this.items[i];
164
- if (!item) continue;
170
+ if (!item) {
171
+ continue;
172
+ }
165
173
  const isActive = i === this.selectedIndex && this.focused;
166
174
  const itemRowCount = this.computeItemRowCount(item, i, isActive, width, numberWidth);
167
175
  if (i === this.selectedIndex) {
@@ -306,7 +314,9 @@ export class WrappingSelect implements Component {
306
314
  continuationPrefix: string,
307
315
  contentWidth: number,
308
316
  ): string[] {
309
- if (!description) return [];
317
+ if (!description) {
318
+ return [];
319
+ }
310
320
  const wrapped = wrapTextWithAnsi(description, contentWidth);
311
321
  return wrapped.map((segment) => `${continuationPrefix}${this.theme.description(segment)}`);
312
322
  }
@@ -103,7 +103,9 @@ function computeScrollStart(
103
103
  availableMiddle: number,
104
104
  middleRows: number,
105
105
  ): number {
106
- if (!bodyRange) return 0;
106
+ if (!bodyRange) {
107
+ return 0;
108
+ }
107
109
  const focusedRowInMiddle = headingCount + bodyRange[0];
108
110
  const focusedHeight = bodyRange[1] - bodyRange[0];
109
111
  const idealStart =
@@ -122,13 +124,19 @@ function decorateOverflow(
122
124
  hasDown: boolean,
123
125
  theme: Theme,
124
126
  ): void {
125
- if (scrollableMiddle.length === 0) return;
127
+ if (scrollableMiddle.length === 0) {
128
+ return;
129
+ }
126
130
  if (hasUp && hasDown && scrollableMiddle.length === 1) {
127
131
  scrollableMiddle[0] = theme.fg("dim", OVERFLOW_BOTH);
128
132
  return;
129
133
  }
130
- if (hasUp) scrollableMiddle[0] = theme.fg("dim", OVERFLOW_UP);
131
- if (hasDown) scrollableMiddle[scrollableMiddle.length - 1] = theme.fg("dim", OVERFLOW_DOWN);
134
+ if (hasUp) {
135
+ scrollableMiddle[0] = theme.fg("dim", OVERFLOW_UP);
136
+ }
137
+ if (hasDown) {
138
+ scrollableMiddle[scrollableMiddle.length - 1] = theme.fg("dim", OVERFLOW_DOWN);
139
+ }
132
140
  }
133
141
 
134
142
  export type DialogState = QuestionnaireState;
@@ -307,16 +315,24 @@ export class DialogView implements StatefulView<DialogProps> {
307
315
  const border = () => new DynamicBorder((s) => theme.fg("accent", s));
308
316
 
309
317
  container.addChild(border());
310
- if (isMulti && tabBar) container.addChild(tabBar);
318
+ if (isMulti && tabBar) {
319
+ container.addChild(tabBar);
320
+ }
311
321
  container.addChild(new Spacer(1));
312
322
 
313
- for (const c of headingRowCache) container.addChild(c);
323
+ for (const c of headingRowCache) {
324
+ container.addChild(c);
325
+ }
314
326
  container.addChild(strategy.bodyComponent(state));
315
327
  container.addChild(new Spacer(1));
316
- for (const c of strategy.midRows(state)) container.addChild(c);
328
+ for (const c of strategy.midRows(state)) {
329
+ container.addChild(c);
330
+ }
317
331
 
318
332
  container.addChild(border());
319
- for (const c of strategy.footerRows(state)) container.addChild(c);
333
+ for (const c of strategy.footerRows(state)) {
334
+ container.addChild(c);
335
+ }
320
336
 
321
337
  // The residual spacer lives in render(), not here: whether it applies
322
338
  // depends on overflow, which the container cannot see.
@@ -22,7 +22,9 @@ function getInputCursorOffset(input: Editor): number {
22
22
  const lines = input.getLines();
23
23
  const cursor = input.getCursor();
24
24
  let offset = cursor.col;
25
- for (let i = 0; i < cursor.line; i++) offset += (lines[i]?.length ?? 0) + 1;
25
+ for (let i = 0; i < cursor.line; i++) {
26
+ offset += (lines[i]?.length ?? 0) + 1;
27
+ }
26
28
  return offset;
27
29
  }
28
30
 
@@ -82,7 +84,9 @@ export class QuestionnairePropsAdapter {
82
84
  // so there is always a tab. Skipping the tick beats asserting non-null and
83
85
  // throwing out of a render path if that ever stops being true.
84
86
  const activePreviewPane = this.tabsByIndex[paneIndex]?.preview ?? firstTab?.preview;
85
- if (!activePreviewPane) return;
87
+ if (!activePreviewPane) {
88
+ return;
89
+ }
86
90
 
87
91
  const ctx: BindingContext = {
88
92
  questions: this.questions,
@@ -94,13 +98,19 @@ export class QuestionnairePropsAdapter {
94
98
  activePreviewPane,
95
99
  };
96
100
 
97
- for (const binding of this.globalBindings) binding.apply(state, ctx);
101
+ for (const binding of this.globalBindings) {
102
+ binding.apply(state, ctx);
103
+ }
98
104
 
99
105
  for (let i = 0; i < this.tabsByIndex.length; i++) {
100
106
  const tab = this.tabsByIndex[i];
101
- if (!tab) continue;
107
+ if (!tab) {
108
+ continue;
109
+ }
102
110
  const tabCtx: PerTabBindingContext = { ...ctx, tab, i };
103
- for (const binding of this.perTabBindings) binding.apply(state, tabCtx);
111
+ for (const binding of this.perTabBindings) {
112
+ binding.apply(state, tabCtx);
113
+ }
104
114
  }
105
115
 
106
116
  this.tui.requestRender();
@@ -113,12 +123,16 @@ export class QuestionnairePropsAdapter {
113
123
  * refresh them. Walks the same registries `apply()` does, then the extras.
114
124
  */
115
125
  invalidate(): void {
116
- for (const b of this.globalBindings) b.invalidate();
126
+ for (const b of this.globalBindings) {
127
+ b.invalidate();
128
+ }
117
129
  for (const tab of this.tabsByIndex) {
118
130
  tab.optionList.invalidate();
119
131
  tab.preview.invalidate();
120
132
  tab.multiSelect?.invalidate();
121
133
  }
122
- for (const x of this.extraInvalidatables) x.invalidate();
134
+ for (const x of this.extraInvalidatables) {
135
+ x.invalidate();
136
+ }
123
137
  }
124
138
  }
@@ -152,7 +152,9 @@ export class QuestionTabStrategy implements TabContentStrategy {
152
152
  bodyComponent(state: DialogState): Component {
153
153
  const question = this.config.questions[state.currentTab];
154
154
  const multiSelect = this.config.tabsByIndex[state.currentTab]?.multiSelect;
155
- if (question?.multiSelect === true && multiSelect) return multiSelect;
155
+ if (question?.multiSelect === true && multiSelect) {
156
+ return multiSelect;
157
+ }
156
158
  return this.config.getPreviewPane();
157
159
  }
158
160
 
@@ -161,7 +163,9 @@ export class QuestionTabStrategy implements TabContentStrategy {
161
163
  }
162
164
 
163
165
  midRows(state: DialogState): Component[] {
164
- if (!state.notesVisible) return this.restingNoteRows(state);
166
+ if (!state.notesVisible) {
167
+ return this.restingNoteRows(state);
168
+ }
165
169
  return [
166
170
  new Text(this.config.theme.fg("muted", NOTES_HEADER), 1, 0),
167
171
  this.config.notesInput,
@@ -190,9 +194,13 @@ export class QuestionTabStrategy implements TabContentStrategy {
190
194
  * put noise on every un-noted tab of every questionnaire.
191
195
  */
192
196
  restingNoteRows(state: DialogState): Component[] {
193
- if (this.restingNoteRowCount(state) === 0) return [];
197
+ if (this.restingNoteRowCount(state) === 0) {
198
+ return [];
199
+ }
194
200
  const note = noteForTab(state, state.currentTab);
195
- if (note.length === 0) return [new Spacer(1)];
201
+ if (note.length === 0) {
202
+ return [new Spacer(1)];
203
+ }
196
204
  return [
197
205
  new OneLineClippedText(
198
206
  this.config.theme.fg("dim", `${NOTES_LABEL} ${collapseToOneLine(note)}`),
@@ -213,9 +221,13 @@ export class QuestionTabStrategy implements TabContentStrategy {
213
221
  * then, and its own height is the intentional expansion.
214
222
  */
215
223
  restingNoteRowCount(state: DialogState): number {
216
- if (state.notesVisible) return 0;
224
+ if (state.notesVisible) {
225
+ return 0;
226
+ }
217
227
  for (let i = 0; i < this.config.questions.length; i++) {
218
- if (noteForTab(state, i).length > 0) return 1;
228
+ if (noteForTab(state, i).length > 0) {
229
+ return 1;
230
+ }
219
231
  }
220
232
  return 0;
221
233
  }
@@ -273,13 +285,17 @@ export class SubmitTabStrategy implements TabContentStrategy {
273
285
  const c = new Container();
274
286
  for (let i = 0; i < this.config.questions.length; i++) {
275
287
  const q = this.config.questions[i];
276
- if (!q) continue;
288
+ if (!q) {
289
+ continue;
290
+ }
277
291
  const a = state.answers.get(i);
278
292
  const note = noteForTab(state, i);
279
293
  // A note with no answer still gets an entry. Skipping it, which is what
280
294
  // this loop used to do, dropped the note from the review and from the
281
295
  // result: the user wrote something and was never told it went nowhere.
282
- if (!a && note.length === 0) continue;
296
+ if (!a && note.length === 0) {
297
+ continue;
298
+ }
283
299
  c.addChild(new Text(this.config.theme.fg("muted", ` ● ${tabLabel(q.header, i)}`), 1, 0));
284
300
  // No arrow row when there is no answer. The absent row is the signal, and
285
301
  // the footer already names the question in its incomplete warning, so
@@ -326,7 +342,9 @@ export class SubmitTabStrategy implements TabContentStrategy {
326
342
  midRows(state: DialogState): Component[] {
327
343
  // The question tabs' notes editor, mirrored for the global note. The draft
328
344
  // itself is reducer-owned; this only renders it.
329
- if (!state.notesVisible) return [];
345
+ if (!state.notesVisible) {
346
+ return [];
347
+ }
330
348
  return [
331
349
  new Text(this.config.theme.fg("muted", GLOBAL_NOTES_HEADER), 1, 0),
332
350
  this.config.notesInput,
@@ -338,7 +356,9 @@ export class SubmitTabStrategy implements TabContentStrategy {
338
356
  const missing: string[] = [];
339
357
  for (let i = 0; i < this.config.questions.length; i++) {
340
358
  const q = this.config.questions[i];
341
- if (q && !state.answers.has(i)) missing.push(tabLabel(q.header, i));
359
+ if (q && !state.answers.has(i)) {
360
+ missing.push(tabLabel(q.header, i));
361
+ }
342
362
  }
343
363
  const promptText =
344
364
  missing.length === 0
@@ -380,9 +400,13 @@ export class SubmitTabStrategy implements TabContentStrategy {
380
400
  }
381
401
 
382
402
  function remainingLabel(state: DialogState): string | undefined {
383
- if (state.timerCancelled) return undefined;
403
+ if (state.timerCancelled) {
404
+ return undefined;
405
+ }
384
406
  const ms = state.remainingMs;
385
- if (ms === undefined) return undefined;
407
+ if (ms === undefined) {
408
+ return undefined;
409
+ }
386
410
  const secs = Math.max(0, Math.ceil(ms / 1000));
387
411
  return `${secs}s left`;
388
412
  }
@@ -408,7 +432,9 @@ export function buildHintText(
408
432
  collapseKey: string,
409
433
  ): string {
410
434
  const parts: string[] = [HINT_PART_ENTER, HINT_PART_NAV];
411
- if (question?.multiSelect === true) parts.push(HINT_PART_TOGGLE);
435
+ if (question?.multiSelect === true) {
436
+ parts.push(HINT_PART_TOGGLE);
437
+ }
412
438
  if (question && !state.notesVisible && !state.inputMode) {
413
439
  // "add" is wrong once one exists, and the hint is the only affordance
414
440
  // telling a keyboard user the resting row can be reopened at all.
@@ -416,17 +442,25 @@ export function buildHintText(
416
442
  noteForTab(state, state.currentTab).length > 0 ? HINT_PART_NOTES_EDIT : HINT_PART_NOTES,
417
443
  );
418
444
  }
419
- if (isMulti) parts.push(HINT_PART_TAB);
445
+ if (isMulti) {
446
+ parts.push(HINT_PART_TAB);
447
+ }
420
448
  parts.push(HINT_PART_CANCEL);
421
449
  if (collapseKey !== COLLAPSE_KEY_OFF) {
422
450
  parts.push(
423
451
  HINT_PART_COLLAPSE_TEMPLATE.replace(KEY_PLACEHOLDER, formatKeySpecForDisplay(collapseKey)),
424
452
  );
425
453
  }
426
- if (state.notesVisible || state.inputMode) parts.push(HINT_PART_NEW_LINE);
427
- if (state.inputMode) parts.push(HINT_PART_CLEAR);
454
+ if (state.notesVisible || state.inputMode) {
455
+ parts.push(HINT_PART_NEW_LINE);
456
+ }
457
+ if (state.inputMode) {
458
+ parts.push(HINT_PART_CLEAR);
459
+ }
428
460
  const rem = remainingLabel(state);
429
- if (rem) parts.push(rem);
461
+ if (rem) {
462
+ parts.push(rem);
463
+ }
430
464
  return parts.join(" · ");
431
465
  }
432
466
 
@@ -438,10 +472,16 @@ export function buildHintText(
438
472
  */
439
473
  export function buildSubmitHintText(state: DialogState): string {
440
474
  const parts: string[] = [HINT_PART_ENTER, HINT_PART_NAV];
441
- if (!state.notesVisible) parts.push(REVIEW_GLOBAL_HINT);
475
+ if (!state.notesVisible) {
476
+ parts.push(REVIEW_GLOBAL_HINT);
477
+ }
442
478
  parts.push(HINT_PART_CANCEL);
443
- if (state.notesVisible) parts.push(HINT_PART_NEW_LINE);
479
+ if (state.notesVisible) {
480
+ parts.push(HINT_PART_NEW_LINE);
481
+ }
444
482
  const rem = remainingLabel(state);
445
- if (rem) parts.push(rem);
483
+ if (rem) {
484
+ parts.push(rem);
485
+ }
446
486
  return parts.join(" · ");
447
487
  }