@oh-my-pi/pi-tui 18.13.2 → 18.13.3

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 (36) hide show
  1. package/CHANGELOG.md +24 -14
  2. package/dist/types/apps/session-picker.d.ts +4 -0
  3. package/dist/types/chat/assistant-message.d.ts +1 -1
  4. package/dist/types/chat/bash-execution.d.ts +2 -0
  5. package/dist/types/chat/display-preferences.d.ts +2 -0
  6. package/dist/types/chat/tool-execution.d.ts +1 -1
  7. package/dist/types/components/editor.d.ts +2 -0
  8. package/dist/types/overlays/annotation-overlay.d.ts +7 -1
  9. package/dist/types/overlays/annotation-types.d.ts +28 -0
  10. package/dist/types/overlays/plan-review-overlay.d.ts +2 -0
  11. package/dist/types/overlays/session-selector.d.ts +13 -2
  12. package/dist/types/theme/theme.d.ts +0 -23
  13. package/dist/types/tools/bash-interactive.d.ts +6 -0
  14. package/dist/types/tui.d.ts +4 -1
  15. package/package.json +10 -10
  16. package/src/apps/session-picker.ts +5 -0
  17. package/src/chat/advisor-message.ts +2 -0
  18. package/src/chat/assistant-message.ts +3 -1
  19. package/src/chat/bash-execution.ts +6 -0
  20. package/src/chat/display-preferences.ts +3 -0
  21. package/src/chat/tool-execution.ts +7 -3
  22. package/src/components/editor.ts +3 -1
  23. package/src/native/backend.ts +2 -64
  24. package/src/overlays/annotation-overlay.ts +125 -37
  25. package/src/overlays/annotation-types.ts +24 -0
  26. package/src/overlays/hook-selector.ts +2 -2
  27. package/src/overlays/plan-review-overlay.ts +13 -2
  28. package/src/overlays/session-selector.ts +195 -26
  29. package/src/overlays/usage-dashboard.ts +5 -12
  30. package/src/prompt/custom-editor.ts +6 -1
  31. package/src/terminal.ts +24 -2
  32. package/src/theme/theme.ts +0 -65
  33. package/src/tools/bash-interactive.ts +6 -1
  34. package/src/tools/job-widget.ts +13 -1
  35. package/src/tools/json-tree.ts +1 -1
  36. package/src/tui.ts +7 -2
@@ -19,6 +19,7 @@ import type { Keybinding } from "../keybindings";
19
19
  import { sanitizeText } from "@oh-my-pi/pi-utils";
20
20
  import { type Theme } from "../theme/theme";
21
21
  import type {
22
+ AnnotationOverlayDraft,
22
23
  CodeReviewAnnotation,
23
24
  CodeReviewOverlayResult,
24
25
  ReviewDiffFile,
@@ -125,8 +126,14 @@ const MIN_BODY_ROWS = 3;
125
126
  const SIDEBAR_MIN_TOTAL_WIDTH = 64;
126
127
  const SIDEBAR_MIN_BODY_WIDTH = 40;
127
128
  const MAX_ANNOTATION_EDITOR_ROWS = 6;
128
- const CODE_REVIEW_ACTIONS = [CONTINUE_CODE_REVIEW_ACTION, PASTE_CODE_REVIEW_ACTION] as const;
129
- const TEXT_REVIEW_ACTIONS = [PASTE_CODE_REVIEW_ACTION] as const;
129
+ /** Clears every note and closes; the host then has nothing to keep. Needs a second confirm. */
130
+ export const DISCARD_ANNOTATIONS_ACTION = "Discard notes and close";
131
+ const CODE_REVIEW_ACTIONS = [
132
+ CONTINUE_CODE_REVIEW_ACTION,
133
+ PASTE_CODE_REVIEW_ACTION,
134
+ DISCARD_ANNOTATIONS_ACTION,
135
+ ] as const;
136
+ const TEXT_REVIEW_ACTIONS = [PASTE_CODE_REVIEW_ACTION, DISCARD_ANNOTATIONS_ACTION] as const;
130
137
 
131
138
  function isSourceRow(row: ReviewDiffRow): row is ReviewSourceRow {
132
139
  return row.kind === "context" || row.kind === "added" || row.kind === "removed";
@@ -229,6 +236,8 @@ export class AnnotationOverlay implements Focusable {
229
236
  /** Set once {@link replaceTextSource} runs; the paste result then carries the edited text. */
230
237
  #textSourceEdited = false;
231
238
  #finished = false;
239
+ /** Discard was chosen once; choosing it again discards, any other input disarms it. */
240
+ #discardArmed = false;
232
241
  #annotations: CommittedAnnotation[] = [];
233
242
  #textAnnotations: CommittedTextAnnotation[] = [];
234
243
  /** Annotation lists before each create/edit/delete, restored by `u`. */
@@ -295,6 +304,8 @@ export class AnnotationOverlay implements Focusable {
295
304
  this.#files = filesOrSource;
296
305
  this.#mode = modeOrCallbacks as string;
297
306
  this.#callbacks = maybeCallbacks!;
307
+ // Start on Paste, not the LLM review: a stray Enter must not submit (it does nothing until a note exists).
308
+ this.#actionIndex = 1;
298
309
  }
299
310
  const symbols = {
300
311
  cursor: theme.nav.cursor,
@@ -357,6 +368,60 @@ export class AnnotationOverlay implements Focusable {
357
368
  this.#finished = true;
358
369
  }
359
370
 
371
+ /** Notes, the unfinished note, and any source edit, in the form {@link restoreDraft} replays. */
372
+ getDraft(): AnnotationOverlayDraft {
373
+ const editing =
374
+ this.#editingAnnotationIndex === undefined ? {} : { editingAnnotationIndex: this.#editingAnnotationIndex };
375
+ const text = this.#editor.getExpandedText();
376
+ if (this.#textSource) {
377
+ const scope = this.#annotationScope === "text" ? "text" : "line";
378
+ const sourceIndex = scope === "text" ? -1 : this.#sourceIndex;
379
+ return {
380
+ kind: "text",
381
+ annotations: this.#textAnnotations.map(entry => ({ ...entry, annotation: { ...entry.annotation } })),
382
+ ...(this.#textSourceEdited ? { editedText: this.#textSource.text } : {}),
383
+ ...(this.#annotating ? { editor: { sourceIndex, scope, ...editing, text } } : {}),
384
+ };
385
+ }
386
+ const scope = this.#annotationScope === "file" ? "file" : "line";
387
+ const editor = { fileIndex: this.#fileIndex, sourceIndex: this.#sourceIndex, scope, ...editing, text } as const;
388
+ return {
389
+ kind: "diff",
390
+ annotations: this.#annotations.map(entry => ({ ...entry, annotation: { ...entry.annotation } })),
391
+ ...(this.#annotating ? { editor } : {}),
392
+ };
393
+ }
394
+
395
+ /** Replay a draft taken from this same source; anything anchored outside it is dropped. */
396
+ restoreDraft(draft: AnnotationOverlayDraft): void {
397
+ const rows = (fileIndex: number) =>
398
+ this.#textSource ? this.#textLines.length : (this.#files[fileIndex]?.rows.filter(isSourceRow).length ?? -1);
399
+ const fits = (fileIndex: number, sourceIndex: number, whole: boolean) =>
400
+ rows(fileIndex) >= 0 && (whole ? sourceIndex === -1 : sourceIndex >= 0 && sourceIndex < rows(fileIndex));
401
+ if (draft.kind === "text" && this.#textSource) {
402
+ if (draft.editedText !== undefined) this.replaceTextSource(draft.editedText);
403
+ this.#textAnnotations = draft.annotations.filter(e => fits(0, e.sourceIndex, e.annotation.scope === "text"));
404
+ } else if (draft.kind === "diff" && !this.#textSource) {
405
+ this.#annotations = draft.annotations.filter(e =>
406
+ fits(e.fileIndex, e.sourceIndex, e.annotation.scope === "file"),
407
+ );
408
+ } else return;
409
+ const editor = draft.editor;
410
+ const fileIndex = editor && "fileIndex" in editor ? editor.fileIndex : 0;
411
+ if (editor && fits(fileIndex, editor.sourceIndex, editor.scope === "text" || editor.scope === "file")) {
412
+ this.#fileIndex = fileIndex;
413
+ if (editor.sourceIndex >= 0) this.#sourceIndex = editor.sourceIndex;
414
+ this.#annotationScope = editor.scope;
415
+ const count = this.#textSource ? this.#textAnnotations.length : this.#annotations.length;
416
+ this.#editingAnnotationIndex =
417
+ (editor.editingAnnotationIndex ?? count) < count ? editor.editingAnnotationIndex : undefined;
418
+ this.#annotating = true;
419
+ this.#editor.setText(editor.text);
420
+ }
421
+ this.#annotationRev++;
422
+ this.#nativeBody = undefined;
423
+ }
424
+
360
425
  getAnnotations(): CodeReviewAnnotation[] {
361
426
  return this.#annotations.map(entry => ({ ...entry.annotation }));
362
427
  }
@@ -422,6 +487,8 @@ export class AnnotationOverlay implements Focusable {
422
487
 
423
488
  handleInput(data: string): void {
424
489
  if (this.#finished || this.#externalOperation) return;
490
+ const discardArmed = this.#discardArmed;
491
+ this.#discardArmed = false;
425
492
  if (this.#annotationChooser) {
426
493
  this.#handleAnnotationChooser(data);
427
494
  return;
@@ -431,8 +498,9 @@ export class AnnotationOverlay implements Focusable {
431
498
  void this.#openAnnotationEditor();
432
499
  return;
433
500
  }
501
+ // Close, keeping the unfinished note in the draft so it reopens in the editor.
434
502
  if (this.#keybindings.matches(data, "tui.select.cancel")) {
435
- this.#cancelAnnotation();
503
+ this.#finish(undefined);
436
504
  return;
437
505
  }
438
506
  this.#editor.handleInput(data);
@@ -476,7 +544,7 @@ export class AnnotationOverlay implements Focusable {
476
544
  }
477
545
  if (this.#focus === "files") this.#handleFiles(data);
478
546
  else if (this.#focus === "diff") this.#handleDiff(data);
479
- else this.#handleActions(data);
547
+ else this.#handleActions(data, discardArmed);
480
548
  }
481
549
 
482
550
  /** Primary key bound to `action` in this overlay's keybindings, formatted for footer hints. */
@@ -495,10 +563,9 @@ export class AnnotationOverlay implements Focusable {
495
563
  }
496
564
  }
497
565
 
498
- /** Esc, by state: leave the note chooser, drop the note draft, else close the review. */
566
+ /** Esc, by state: leave the note chooser, else close the review; the host keeps the draft. */
499
567
  #escape(): void {
500
568
  if (this.#annotationChooser) this.#annotationChooser = undefined;
501
- else if (this.#annotating) this.#cancelAnnotation();
502
569
  else this.#finish(undefined);
503
570
  }
504
571
 
@@ -602,33 +669,41 @@ export class AnnotationOverlay implements Focusable {
602
669
  }
603
670
  }
604
671
 
605
- #handleActions(data: string): void {
606
- const actionCount = this.#actions.length;
607
- if (actionCount === 0) return;
608
- const hasAnnotations = this.#textSource ? this.#textAnnotations.length > 0 : this.#annotations.length > 0;
609
- if (this.#keybindings.matches(data, "tui.select.up") || matchesKey(data, "k")) {
610
- this.#actionIndex = this.#textSource ? (this.#actionIndex - 1 + actionCount) % actionCount : 0;
611
- return;
612
- }
613
- if (this.#keybindings.matches(data, "tui.select.down") || matchesKey(data, "j")) {
614
- if (this.#textSource) {
615
- this.#actionIndex = (this.#actionIndex + 1) % actionCount;
616
- } else if (hasAnnotations) {
617
- this.#actionIndex = Math.min(actionCount - 1, this.#actionIndex + 1);
672
+ #handleActions(data: string, discardArmed: boolean): void {
673
+ const up = this.#keybindings.matches(data, "tui.select.up") || matchesKey(data, "k");
674
+ if (up || this.#keybindings.matches(data, "tui.select.down") || matchesKey(data, "j")) {
675
+ const step = up ? -1 : 1;
676
+ for (let index = this.#actionIndex + step; index >= 0 && index < this.#actions.length; index += step) {
677
+ if (this.#actionDisabled(index)) continue;
678
+ this.#actionIndex = index;
679
+ break;
618
680
  }
619
681
  return;
620
682
  }
621
- if (this.#keybindings.matches(data, "tui.select.confirm")) this.#confirmAction();
683
+ if (this.#keybindings.matches(data, "tui.select.confirm")) this.#confirmAction(discardArmed);
622
684
  }
623
685
 
624
686
  #actionDisabled(index: number): boolean {
687
+ if (this.#actions[index] === DISCARD_ANNOTATIONS_ACTION) return false;
625
688
  const hasAnnotations = this.#textSource ? this.#textAnnotations.length > 0 : this.#annotations.length > 0;
626
689
  return this.#textSource ? !hasAnnotations : index === 1 && !hasAnnotations;
627
690
  }
628
691
 
629
- #confirmAction(): void {
692
+ #confirmAction(discardArmed = false): void {
630
693
  if (this.#actionDisabled(this.#actionIndex)) return;
631
- if (this.#textSource) {
694
+ if (this.#actions[this.#actionIndex] === DISCARD_ANNOTATIONS_ACTION) {
695
+ if (!discardArmed) {
696
+ this.#discardArmed = true;
697
+ return;
698
+ }
699
+ this.#annotations = [];
700
+ this.#textAnnotations = [];
701
+ this.#textSourceEdited = false;
702
+ this.#annotating = false;
703
+ this.#editingAnnotationIndex = undefined;
704
+ this.#editor.setText("");
705
+ this.#finish(undefined);
706
+ } else if (this.#textSource) {
632
707
  this.#finish({
633
708
  action: "paste",
634
709
  annotations: this.getTextAnnotations(),
@@ -721,12 +796,6 @@ export class AnnotationOverlay implements Focusable {
721
796
  this.#editor.setText(existing?.annotation.note ?? "");
722
797
  }
723
798
 
724
- #cancelAnnotation(): void {
725
- this.#annotating = false;
726
- this.#editingAnnotationIndex = undefined;
727
- this.#editor.setText("");
728
- }
729
-
730
799
  #commitAnnotation(value: string): void {
731
800
  if (this.#textSource) {
732
801
  this.#commitTextAnnotation(value);
@@ -925,7 +994,6 @@ export class AnnotationOverlay implements Focusable {
925
994
  this.#annotationRev++;
926
995
  this.#annotations = snapshot.annotations;
927
996
  this.#textAnnotations = snapshot.textAnnotations;
928
- if (this.#annotations.length === 0 && this.#actionIndex === 1) this.#actionIndex = 0;
929
997
  }
930
998
 
931
999
  async #openAnnotationEditor(): Promise<void> {
@@ -1146,9 +1214,10 @@ export class AnnotationOverlay implements Focusable {
1146
1214
  }
1147
1215
 
1148
1216
  #renderActions(): string[] {
1149
- const hasAnnotations = this.#textSource ? this.#textAnnotations.length > 0 : this.#annotations.length > 0;
1150
- return this.#actions.map((label, index) => {
1151
- const disabled = this.#textSource ? !hasAnnotations : index === 1 && !hasAnnotations;
1217
+ return this.#actions.map((action, index) => {
1218
+ const label =
1219
+ action === DISCARD_ANNOTATIONS_ACTION && this.#discardArmed ? "Confirm: discard all notes" : action;
1220
+ const disabled = this.#actionDisabled(index);
1152
1221
  const selected = index === this.#actionIndex;
1153
1222
  const cursor = selected ? `${this.#theme.nav.cursor} ` : " ";
1154
1223
  const text = disabled
@@ -1224,7 +1293,7 @@ export class AnnotationOverlay implements Focusable {
1224
1293
  const hints = [
1225
1294
  `${editorKey("tui.input.submit")} save`,
1226
1295
  `${editorKey("tui.input.newLine")} newline`,
1227
- `${this.#key("tui.select.cancel")} cancel`,
1296
+ `${this.#key("tui.select.cancel")} close, keep note`,
1228
1297
  ];
1229
1298
  hints.push(`${formatKeyHint(appExternalEditorKey())} editor`);
1230
1299
  return [caption, ...this.#editor.render(width), this.#theme.fg("dim", hints.join(" · "))];
@@ -1428,6 +1497,7 @@ export class AnnotationOverlay implements Focusable {
1428
1497
  this.#bodyHeight,
1429
1498
  this.#annotating ? `annotating:${this.#annotationScope}:${this.#editingAnnotationIndex}` : "",
1430
1499
  chooser ? `chooser:${chooser.selected}:${chooser.entries.join()}` : "",
1500
+ this.#discardArmed ? "discard-armed" : "",
1431
1501
  ].join("|");
1432
1502
  if (this.#nativeRoot && sig === this.#nativeSig && body === this.#nativeRootBody) return this.#nativeRoot;
1433
1503
 
@@ -1462,6 +1532,8 @@ export class AnnotationOverlay implements Focusable {
1462
1532
 
1463
1533
  handleNativeEvent(event: NativeUiEvent): void {
1464
1534
  if (this.#finished || this.#externalOperation) return;
1535
+ const discardArmed = this.#discardArmed;
1536
+ this.#discardArmed = false;
1465
1537
  if (event.type === "action") {
1466
1538
  if (event.act === "close") {
1467
1539
  this.#escape();
@@ -1470,10 +1542,18 @@ export class AnnotationOverlay implements Focusable {
1470
1542
  // A draft or the note chooser is open: finish or leave it first.
1471
1543
  if (this.#annotating || this.#annotationChooser) return;
1472
1544
  const index =
1473
- event.act === "review" && !this.#textSource ? 0 : event.act === "paste" ? (this.#textSource ? 0 : 1) : -1;
1545
+ event.act === "discard"
1546
+ ? this.#actions.indexOf(DISCARD_ANNOTATIONS_ACTION)
1547
+ : event.act === "review" && !this.#textSource
1548
+ ? 0
1549
+ : event.act === "paste"
1550
+ ? this.#textSource
1551
+ ? 0
1552
+ : 1
1553
+ : -1;
1474
1554
  if (index < 0 || this.#actionDisabled(index)) return;
1475
1555
  this.#actionIndex = index;
1476
- this.#confirmAction();
1556
+ this.#confirmAction(discardArmed);
1477
1557
  return;
1478
1558
  }
1479
1559
  if (event.type !== "select" && event.type !== "activate") return;
@@ -1824,7 +1904,15 @@ export class AnnotationOverlay implements Focusable {
1824
1904
  mark ? ({ ...button, p: { ...button.p, mark } } as NativeNode) : button;
1825
1905
  const hasNotes = this.#textSource ? this.#textAnnotations.length > 0 : this.#annotations.length > 0;
1826
1906
  const pasteTitle = hasNotes ? PASTE_CODE_REVIEW_ACTION : "Add a note to paste it into the prompt";
1827
- const buttons: NativeNode[] = [actionButton("Cancel", "close", { keys: "escape" })];
1907
+ const buttons: NativeNode[] = [
1908
+ actionButton("Cancel", "close", { keys: "escape" }),
1909
+ marked(
1910
+ actionButton(this.#discardArmed ? "Confirm discard" : "Discard", "discard", {
1911
+ title: DISCARD_ANNOTATIONS_ACTION,
1912
+ }),
1913
+ pick(this.#actions.indexOf(DISCARD_ANNOTATIONS_ACTION)),
1914
+ ),
1915
+ ];
1828
1916
  if (this.#textSource) {
1829
1917
  buttons.push(
1830
1918
  marked(actionButton("Paste into prompt", "paste", { tone: "accent", title: pasteTitle }), pick(0)),
@@ -1860,7 +1948,7 @@ export class AnnotationOverlay implements Focusable {
1860
1948
  return [
1861
1949
  this.#hint("tui.input.submit", "save"),
1862
1950
  this.#hint("tui.input.newLine", "new line"),
1863
- this.#hint("tui.select.cancel", "discard"),
1951
+ this.#hint("tui.select.cancel", "close, keep note"),
1864
1952
  this.#callbacks.onAnnotationExternalEditor ? key(appExternalEditorKey(), "editor") : undefined,
1865
1953
  ];
1866
1954
  }
@@ -78,3 +78,27 @@ export interface TextReviewOverlayResult {
78
78
  /** Source text after an external-editor edit; absent when the source was not edited. */
79
79
  editedText?: string;
80
80
  }
81
+
82
+ export type AnnotationOverlayDraft =
83
+ | {
84
+ kind: "diff";
85
+ annotations: { fileIndex: number; sourceIndex: number; annotation: CodeReviewAnnotation }[];
86
+ editor?: {
87
+ fileIndex: number;
88
+ sourceIndex: number;
89
+ scope: "file" | "line";
90
+ editingAnnotationIndex?: number;
91
+ text: string;
92
+ };
93
+ }
94
+ | {
95
+ kind: "text";
96
+ annotations: { sourceIndex: number; annotation: TextReviewAnnotation }[];
97
+ editedText?: string;
98
+ editor?: {
99
+ sourceIndex: number;
100
+ scope: "text" | "line";
101
+ editingAnnotationIndex?: number;
102
+ text: string;
103
+ };
104
+ };
@@ -647,9 +647,9 @@ export class HookSelectorComponent extends OverlayPanel {
647
647
  return;
648
648
  }
649
649
 
650
- if (matchesSelectUp(keyData) || (!this.#isSearchEnabled() && matchesKey(keyData, "k"))) {
650
+ if (matchesSelectUp(keyData)) {
651
651
  this.#moveSelection(-1);
652
- } else if (matchesSelectDown(keyData) || (!this.#isSearchEnabled() && matchesKey(keyData, "j"))) {
652
+ } else if (matchesSelectDown(keyData)) {
653
653
  this.#moveSelection(1);
654
654
  } else if (matchesKey(keyData, "enter") || matchesKey(keyData, "return") || keyData === "\n") {
655
655
  const selected = this.#menu.selectedItem;
@@ -256,6 +256,8 @@ export interface PlanReviewOverlayOptions {
256
256
  slider?: HookSelectorSlider;
257
257
  /** Display label for the external-editor key, surfaced in the footer help. */
258
258
  externalEditorLabel?: string;
259
+ /** The external editor's name ("Notepad", "nvim") for the native Edit button; `$EDITOR` when unset. */
260
+ externalEditorName?: string;
259
261
  /** Serializable annotations restored into this overlay instance. */
260
262
  annotationState?: PlanReviewAnnotationState;
261
263
  }
@@ -286,6 +288,7 @@ export class PlanReviewOverlay implements Component {
286
288
  #disabled: Set<number>;
287
289
  #helpSuffix: string | undefined;
288
290
  #externalEditorLabel: string | undefined;
291
+ #externalEditorName: string | undefined;
289
292
  #promptTitle: string | undefined;
290
293
  #selectedIndex: number;
291
294
  #slider: HookSelectorSlider | undefined;
@@ -354,6 +357,7 @@ export class PlanReviewOverlay implements Component {
354
357
  );
355
358
  this.#helpSuffix = options.helpText;
356
359
  this.#externalEditorLabel = options.externalEditorLabel;
360
+ this.#externalEditorName = options.externalEditorName;
357
361
  this.#promptTitle = options.promptTitle;
358
362
  this.#selectedIndex = this.#coerceIndex(options.initialIndex ?? 0);
359
363
  if (options.slider && options.slider.segments.length > 0) {
@@ -1632,7 +1636,13 @@ export class PlanReviewOverlay implements Component {
1632
1636
  // Unbound: the button still mirrors the callback, without a keycap.
1633
1637
  const editorKeyId = getKeybindings().getKeys("app.editor.external")[0];
1634
1638
  if (this.callbacks.onExternalEditor) {
1635
- buttons.push(actionButton("Edit in $EDITOR", "externalEditor", editorKeyId ? { keys: editorKeyId } : {}));
1639
+ buttons.push(
1640
+ actionButton(
1641
+ `Edit in ${this.#externalEditorName ?? "$EDITOR"}`,
1642
+ "externalEditor",
1643
+ editorKeyId ? { keys: editorKeyId } : {},
1644
+ ),
1645
+ );
1636
1646
  }
1637
1647
  // Cancel the review the way the cancel key does; while annotating that key
1638
1648
  // only leaves the draft/chooser, so the button is hidden there.
@@ -1754,7 +1764,8 @@ export class PlanReviewOverlay implements Component {
1754
1764
  "tabs",
1755
1765
  );
1756
1766
  const track: NativeChild[] = slider.caption ? [text([span(slider.caption, "dim")]), tabs] : [tabs];
1757
- const out = [keyed(rowNode(track, { gap: "sm", align: "center" }), "slider")];
1767
+ // A role of its own: Tern's sheet lets only the role-less split row (Contents + body) shrink and scroll.
1768
+ const out = [keyed(rowNode(track, { role: "omp.plan.slider", gap: "sm", align: "center" }), "slider")];
1758
1769
  const detail = slider.segments[this.#sliderIndex]?.detail;
1759
1770
  if (detail) out.push(keyed(text([span("↳ ", "dim"), span(detail, "muted")], { wrap: "word" }), "sliderDetail"));
1760
1771
  return out;