@linxiraos/pi-tui 1.3.2 → 1.4.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 (116) hide show
  1. package/CHANGELOG.md +181 -181
  2. package/dist/types/apps/session-picker.d.ts +13 -1
  3. package/dist/types/autocomplete.d.ts +6 -0
  4. package/dist/types/components/sparkline.d.ts +34 -0
  5. package/dist/types/index.d.ts +1 -0
  6. package/dist/types/native/describe.d.ts +7 -1
  7. package/dist/types/native/overlay.d.ts +2 -0
  8. package/dist/types/native/tone.d.ts +2 -0
  9. package/dist/types/overlays/agent-hub.d.ts +3 -3
  10. package/dist/types/overlays/approval-dialog.d.ts +15 -1
  11. package/dist/types/overlays/jobs-panel.d.ts +3 -0
  12. package/dist/types/overlays/model-browser.d.ts +1 -1
  13. package/dist/types/overlays/resume-picker.d.ts +293 -0
  14. package/dist/types/overlays/resume-tree.d.ts +353 -0
  15. package/dist/types/overlays/session-selector.d.ts +22 -4
  16. package/dist/types/render/output-block.d.ts +2 -0
  17. package/dist/types/render/tool-card.d.ts +2 -0
  18. package/dist/types/sixel.d.ts +28 -0
  19. package/dist/types/status-line/host.d.ts +3 -0
  20. package/dist/types/terminal-capabilities.d.ts +6 -1
  21. package/dist/types/tools/bash.d.ts +2 -0
  22. package/dist/types/tools/default-renderer.d.ts +3 -0
  23. package/dist/types/tools/eval.d.ts +9 -8
  24. package/dist/types/tools/hub.d.ts +3 -1
  25. package/dist/types/tools/irc.d.ts +11 -1
  26. package/dist/types/tools/job-widget.d.ts +226 -0
  27. package/dist/types/tools/json-tree.d.ts +2 -0
  28. package/dist/types/tools/line-ranges.d.ts +8 -1
  29. package/dist/types/tools/mcp.d.ts +23 -0
  30. package/dist/types/tools/output-meta.d.ts +1 -1
  31. package/dist/types/tools/read.d.ts +8 -2
  32. package/dist/types/tools/renderer.d.ts +8 -2
  33. package/dist/types/tools/result-card.d.ts +1 -1
  34. package/dist/types/tools/streaming-output.d.ts +1 -0
  35. package/dist/types/tools/task.d.ts +12 -0
  36. package/dist/types/tools/wait.d.ts +11 -0
  37. package/package.json +9 -9
  38. package/src/apps/session-picker.ts +20 -2
  39. package/src/autocomplete.d.ts +6 -0
  40. package/src/autocomplete.ts +21 -6
  41. package/src/chat/skill-message.ts +16 -17
  42. package/src/chat/tool-execution.ts +35 -0
  43. package/src/chrome/transcript-container.ts +85 -13
  44. package/src/components/editor.ts +17 -7
  45. package/src/components/image.ts +5 -4
  46. package/src/components/markdown.ts +11 -3
  47. package/src/components/sparkline.d.ts +34 -0
  48. package/src/components/sparkline.ts +89 -0
  49. package/src/index.d.ts +1 -0
  50. package/src/index.ts +1 -0
  51. package/src/native/backend.ts +32 -6
  52. package/src/native/describe.d.ts +7 -1
  53. package/src/native/describe.ts +18 -1
  54. package/src/native/overlay.d.ts +2 -0
  55. package/src/native/overlay.ts +3 -0
  56. package/src/native/tone.d.ts +2 -0
  57. package/src/native/tone.ts +22 -0
  58. package/src/overlays/agent-hub.ts +14 -10
  59. package/src/overlays/agent-transcript-viewer.ts +2 -2
  60. package/src/overlays/agents-hub.ts +2 -1
  61. package/src/overlays/approval-dialog.ts +59 -13
  62. package/src/overlays/hub-frame.ts +2 -1
  63. package/src/overlays/jobs-panel.ts +24 -2
  64. package/src/overlays/model-browser.ts +3 -0
  65. package/src/overlays/model-hub.ts +2 -1
  66. package/src/overlays/resume-picker.ts +706 -0
  67. package/src/overlays/resume-tree.ts +1901 -0
  68. package/src/overlays/session-selector.ts +579 -39
  69. package/src/prompt/attachment-chips.ts +1 -1
  70. package/src/render/output-block.d.ts +2 -0
  71. package/src/render/output-block.ts +25 -12
  72. package/src/render/tool-card.d.ts +2 -0
  73. package/src/render/tool-card.ts +6 -0
  74. package/src/sixel.d.ts +28 -0
  75. package/src/sixel.ts +185 -0
  76. package/src/status-line/component.ts +19 -2
  77. package/src/status-line/host.ts +3 -0
  78. package/src/terminal-capabilities.d.ts +6 -1
  79. package/src/terminal-capabilities.ts +54 -3
  80. package/src/theme/symbols.ts +5 -3
  81. package/src/tools/bash.d.ts +2 -0
  82. package/src/tools/bash.ts +25 -9
  83. package/src/tools/default-renderer.d.ts +3 -0
  84. package/src/tools/default-renderer.ts +54 -17
  85. package/src/tools/eval.d.ts +9 -8
  86. package/src/tools/eval.ts +32 -14
  87. package/src/tools/find.ts +4 -5
  88. package/src/tools/glob.ts +4 -6
  89. package/src/tools/grep.ts +2 -4
  90. package/src/tools/hub.ts +4 -1
  91. package/src/tools/irc.d.ts +11 -1
  92. package/src/tools/irc.ts +11 -1
  93. package/src/tools/job-widget.d.ts +226 -0
  94. package/src/tools/job-widget.ts +1036 -0
  95. package/src/tools/json-tree.d.ts +2 -0
  96. package/src/tools/json-tree.ts +8 -0
  97. package/src/tools/line-ranges.d.ts +8 -1
  98. package/src/tools/line-ranges.ts +23 -13
  99. package/src/tools/mcp.d.ts +23 -0
  100. package/src/tools/mcp.ts +44 -1
  101. package/src/tools/output-meta.d.ts +1 -1
  102. package/src/tools/output-meta.ts +1 -1
  103. package/src/tools/proc-render.ts +1 -0
  104. package/src/tools/read.d.ts +8 -2
  105. package/src/tools/read.ts +64 -21
  106. package/src/tools/renderer.d.ts +8 -2
  107. package/src/tools/renderer.ts +8 -2
  108. package/src/tools/result-card.d.ts +1 -1
  109. package/src/tools/result-card.ts +1 -2
  110. package/src/tools/streaming-output.d.ts +1 -0
  111. package/src/tools/streaming-output.ts +11 -9
  112. package/src/tools/task.d.ts +12 -0
  113. package/src/tools/task.ts +25 -1
  114. package/src/tools/wait.d.ts +11 -0
  115. package/src/tools/wait.ts +296 -147
  116. package/src/tui.ts +45 -5
@@ -29,6 +29,19 @@ import {
29
29
  import type { DescribeContext, NativeChild, NativeNode, NativeUiEvent } from "../native/node";
30
30
  import { hintsRow, type NativeHint, overlayCard } from "../native/overlay";
31
31
  import { plainText } from "../native/spans";
32
+ import { tuiText, tuiTextFmt } from "../i18n";
33
+ import {
34
+ buildResumePickerModel,
35
+ nextResumeScope,
36
+ type ResumeDirectoryLike,
37
+ type ResumePickerModel,
38
+ type ResumeScopeInputLike,
39
+ type ResumeScopeKindLike,
40
+ type ResumeSourceLike,
41
+ resumeConversationContext,
42
+ resumeConversationPreview,
43
+ resumeScopeLabel,
44
+ } from "./resume-picker";
32
45
  import { theme } from "../theme/theme";
33
46
  import { contentRowWidth } from "../chrome/selector-helpers";
34
47
  import { matchesAppInterrupt, matchesSelectDown, matchesSelectUp } from "../keybinding-matchers";
@@ -52,6 +65,20 @@ export interface SessionSelectorEntry {
52
65
  }
53
66
  import { shortenPath } from "../render/render-utils";
54
67
  import { HookSelectorComponent } from "./hook-selector";
68
+ import { ResumeTreeComponent, type ResumeTreeSource } from "./resume-tree";
69
+
70
+ export type {
71
+ ResumeConversationLike,
72
+ ResumeDirectoryLike,
73
+ ResumeProjectLike,
74
+ ResumeQueryLike,
75
+ ResumeScopeInputLike,
76
+ ResumeScopeKindLike,
77
+ ResumeSourceLike,
78
+ ResumeViewLike,
79
+ ResumeViewRowLike,
80
+ ResumeWorkspaceLike,
81
+ } from "./resume-picker";
55
82
  import { bottomBorder, OverlayPanel, row, topBorder } from "../chrome/overlay-box";
56
83
  import { MenuSelection, getMenuWindow } from "../components/menu-selection";
57
84
  import { formatKeyHint, formatKeyHints } from "../app-keybindings";
@@ -563,6 +590,12 @@ class SessionList<T extends SessionSelectorEntry> implements Component {
563
590
  | undefined;
564
591
  /** Bumped when the session set itself changes (scope switch, delete): the picker catalogue's memo key. */
565
592
  #datasetVersion = 0;
593
+ /**
594
+ * Resume-model mode: the component owns the visible order (built from the
595
+ * resume view), so this list only mirrors it and search filtering happens
596
+ * in the model instead of here.
597
+ */
598
+ #resumeDriven = false;
566
599
  /** The picker catalogue (every session in scope), rebuilt only when the set, markers or minute change. */
567
600
  #pickerCatalogue: SessionCatalogue | undefined;
568
601
  /** Paths upserted through `itemsAdd` (history badges) since the catalogue was last sent. */
@@ -643,6 +676,26 @@ class SessionList<T extends SessionSelectorEntry> implements Component {
643
676
  this.#selectCurrentSession();
644
677
  }
645
678
 
679
+ /**
680
+ * Resume-model mode: the given sessions are already the model's ordered,
681
+ * filtered view, so mirror them and keep the selection when it survives.
682
+ */
683
+ syncSessions(sessions: readonly T[], preferredPath?: string | null): void {
684
+ this.#resumeDriven = true;
685
+ if (this.#menu.visibleItems === sessions && (preferredPath == null || this.#menu.selectedKey === preferredPath)) {
686
+ return;
687
+ }
688
+ const selected = preferredPath ?? this.#menu.selectedKey;
689
+ const keep = selected && sessions.some(session => session.path === selected) ? selected : sessions[0]?.path;
690
+ this.#menu.setItems(sessions, keep);
691
+ }
692
+
693
+ /** Search query changed while resume-driven: the model rebuilds the rows, not this list. */
694
+ #resumeQueryChanged(): void {
695
+ this.#itemsVersion++;
696
+ this.onRequestRender?.();
697
+ }
698
+
646
699
  #filterSessions(query: string): void {
647
700
  this.#scanGeneration++;
648
701
  this.#itemsVersion++;
@@ -799,9 +852,16 @@ class SessionList<T extends SessionSelectorEntry> implements Component {
799
852
 
800
853
  removeSession(sessionPath: string): void {
801
854
  const index = this.#allSessions.findIndex(s => s.path === sessionPath);
802
- if (index === -1) return;
803
- this.#datasetVersion++;
804
- this.#allSessions.splice(index, 1);
855
+ if (index !== -1) {
856
+ this.#datasetVersion++;
857
+ this.#allSessions.splice(index, 1);
858
+ }
859
+ if (this.#resumeDriven) {
860
+ // The host owns the resume view; the picker hides the path itself.
861
+ const remain = this.#menu.visibleItems.filter(session => session.path !== sessionPath);
862
+ this.#menu.setItems(remain, remain[0]?.path);
863
+ return;
864
+ }
805
865
  // Re-filter to update the composed result set
806
866
  this.#filterSessions(this.#searchInput.getValue());
807
867
  // Adjust selection if we deleted the last item or beyond
@@ -856,6 +916,10 @@ class SessionList<T extends SessionSelectorEntry> implements Component {
856
916
  /** Empty the search query (the empty state's "Clear search"). */
857
917
  clearSearch(): void {
858
918
  this.#searchInput.setValue("");
919
+ if (this.#resumeDriven) {
920
+ this.#resumeQueryChanged();
921
+ return;
922
+ }
859
923
  this.#filterSessions("");
860
924
  }
861
925
 
@@ -1130,17 +1194,7 @@ class SessionList<T extends SessionSelectorEntry> implements Component {
1130
1194
  lines.push(""); // Blank line after search
1131
1195
 
1132
1196
  if (this.#menu.visibleItems.length === 0) {
1133
- if (this.#showCwd) {
1134
- lines.push(truncateToWidth(theme.fg("muted", "No sessions found"), width));
1135
- } else {
1136
- // "Current folder" scope - hint to try "all"
1137
- lines.push(
1138
- truncateToWidth(
1139
- theme.fg("muted", `No sessions in current folder. Press ${formatKeyHint("tab")} to view all.`),
1140
- width,
1141
- ),
1142
- );
1143
- }
1197
+ lines.push(truncateToWidth(theme.fg("muted", "No sessions found"), width));
1144
1198
  return lines;
1145
1199
  }
1146
1200
 
@@ -1325,7 +1379,8 @@ class SessionList<T extends SessionSelectorEntry> implements Component {
1325
1379
  }
1326
1380
  // Pass everything else to search input
1327
1381
  this.#searchInput.handleInput(keyData);
1328
- this.#filterSessions(this.#searchInput.getValue());
1382
+ if (this.#resumeDriven) this.#resumeQueryChanged();
1383
+ else this.#filterSessions(this.#searchInput.getValue());
1329
1384
  }
1330
1385
  }
1331
1386
 
@@ -1336,6 +1391,17 @@ export interface SessionSelectorOptions<T extends SessionSelectorEntry = Session
1336
1391
  loadAllSessions?: () => Promise<T[]>;
1337
1392
  /** Preloaded all-projects list; cached so the first Tab toggle is instant. */
1338
1393
  allSessions?: T[];
1394
+ /**
1395
+ * The host's resume model (project → workspace → conversation) and its
1396
+ * pure view builder. When set, the native `picker` describes the resume
1397
+ * tree instead of the flat day-grouped list; the ANSI path is unchanged.
1398
+ */
1399
+ resumeSource?: ResumeSourceLike;
1400
+ /**
1401
+ * What delete does to the session file: an in-session picker moves it to
1402
+ * the trash, `--resume` deletes it permanently. Wording only.
1403
+ */
1404
+ deleteMode?: "trash" | "permanent";
1339
1405
  /** Picker heading; defaults to "Resume Session". */
1340
1406
  title?: string;
1341
1407
  /** Primary action label; defaults to "Resume" (ANSI footer defaults to "select"). */
@@ -1373,10 +1439,53 @@ export interface SessionSelectorOptions<T extends SessionSelectorEntry = Session
1373
1439
  }
1374
1440
 
1375
1441
  /**
1376
- * Component that renders a session selector with optional confirmation dialog
1442
+ * One footer line: key hints left, mouse hints right. Hints drop from the end
1443
+ * of the key list first, then the mouse list truncates, so the line always
1444
+ * fits the panel's inner width.
1377
1445
  */
1446
+ function composeFooter(width: number, keys: readonly string[], mouse: readonly string[]): string {
1447
+ const contentWidth = Math.max(1, width - 4);
1448
+ const rightText = mouse.join(` ${theme.sep.dot} `);
1449
+ const right = theme.fg("dim", `[${rightText}]`);
1450
+ const rightWidth = visibleWidth(right);
1451
+ const kept = [...keys];
1452
+ const leftText = () => theme.fg("muted", `[${kept.join(` ${theme.sep.dot} `)}]`);
1453
+ const gap = 2;
1454
+ while (kept.length > 0 && visibleWidth(leftText()) + gap + rightWidth > contentWidth) kept.pop();
1455
+ const left = kept.length > 0 ? leftText() : "";
1456
+ if (!left)
1457
+ return truncateToWidth(
1458
+ rightWidth > contentWidth ? right : `${" ".repeat(Math.max(0, contentWidth - rightWidth))}${right}`,
1459
+ contentWidth,
1460
+ );
1461
+ const room = contentWidth - visibleWidth(left) - gap;
1462
+ if (room < rightWidth) {
1463
+ const truncated = truncateToWidth(right, Math.max(0, room));
1464
+ return truncateToWidth(left + " ".repeat(gap) + truncated, contentWidth);
1465
+ }
1466
+ return left + " ".repeat(contentWidth - visibleWidth(left) - rightWidth) + right;
1467
+ }
1468
+
1469
+ /** Component that renders a session selector with optional confirmation dialog. */
1378
1470
  export class SessionSelectorComponent<T extends SessionSelectorEntry = SessionSelectorEntry> extends OverlayPanel {
1379
1471
  #sessionList: SessionList<T>;
1472
+ /**
1473
+ * ANSI three-level resume tree. Mounted lazily on the first ANSI frame: the
1474
+ * native picker owns its own event model, so mounting the tree up front
1475
+ * would swallow the raw keys the native path never sends through here.
1476
+ */
1477
+ #resumeTree: ResumeTreeComponent | undefined;
1478
+ #resumeHandlers:
1479
+ | {
1480
+ onSelect: (session: T) => void;
1481
+ onCancel: () => void;
1482
+ onExit: () => void;
1483
+ }
1484
+ | undefined;
1485
+ #resumeTreeSource: ResumeTreeSource | undefined;
1486
+ /** True once the terminal answered with native `picker` support. */
1487
+ #nativePicker = false;
1488
+ readonly #getTerminalRowsOption: (() => number) | undefined;
1380
1489
  #confirmationDialog: HookSelectorComponent | null = null;
1381
1490
  // Hosts whichever of `#sessionList` / `#confirmationDialog` is live this
1382
1491
  // frame. The delete dialog REPLACES the list in this slot rather than being
@@ -1414,6 +1523,43 @@ export class SessionSelectorComponent<T extends SessionSelectorEntry = SessionSe
1414
1523
  /** The native picker shows `#message`'s error until the next key or pointer event. */
1415
1524
  #pickerErrorOpen = false;
1416
1525
  readonly #standalone: boolean;
1526
+ /** The host's resume model; absent keeps the flat day-grouped picker. */
1527
+ readonly #resumeSource?: ResumeSourceLike;
1528
+ /** The live session's path, used to resolve unfixed resume scopes and mark `current`. */
1529
+ readonly #getCurrentSessionPath: () => string | undefined;
1530
+ /** Delete wording: in-session moves to the trash, `--resume` deletes permanently. */
1531
+ readonly #deleteMode: "trash" | "permanent";
1532
+ /** The active resume scope (workspace → project → all); expanded keys pin the current group. */
1533
+ #resumeScope: ResumeScopeInputLike;
1534
+ /** Conversations deleted in this view; the directory is host-owned and not rebuilt here. */
1535
+ readonly #resumeHidden = new Set<string>();
1536
+ #resumeModelMemo:
1537
+ | {
1538
+ source: ResumeSourceLike;
1539
+ directory: ResumeDirectoryLike | null;
1540
+ query: string;
1541
+ scopeKey: string;
1542
+ currentPath: string | undefined;
1543
+ hidden: number;
1544
+ showCwd: boolean;
1545
+ model: ResumePickerModel | undefined;
1546
+ }
1547
+ | undefined;
1548
+ #resumeSessionsMemo: { model: ResumePickerModel; sessions: readonly T[] } | undefined;
1549
+ #resumeLoadingMemo:
1550
+ | { source: ResumeSourceLike; error: string | undefined; query: string; cursor: number; node: NativeNode }
1551
+ | undefined;
1552
+ #resumePickerMemo:
1553
+ | {
1554
+ model: ResumePickerModel;
1555
+ preview: readonly NativeChild[];
1556
+ choice: DeleteChoice<T> | null;
1557
+ errorOpen: boolean;
1558
+ message: SessionPickerMessage | undefined;
1559
+ selected: string | null;
1560
+ node: NativeNode;
1561
+ }
1562
+ | undefined;
1417
1563
  /** A caller's heading for the picker head; the default `/resume` picker has none (the search names it). */
1418
1564
  readonly #pickerTitle: string | undefined;
1419
1565
  /** The open delete confirmation's two answers, for the picker's confirm strip. */
@@ -1452,7 +1598,7 @@ export class SessionSelectorComponent<T extends SessionSelectorEntry = SessionSe
1452
1598
  onExit: () => void,
1453
1599
  options: SessionSelectorOptions<T> = {},
1454
1600
  ) {
1455
- super(options.title ?? "Resume Session");
1601
+ super(options.title ?? tuiText("ssTitleResume", "Resume Session"));
1456
1602
 
1457
1603
  this.#messageContainer = new Container();
1458
1604
  this.#onDelete = options.onDelete;
@@ -1464,10 +1610,16 @@ export class SessionSelectorComponent<T extends SessionSelectorEntry = SessionSe
1464
1610
  this.#globalSessions = options.allSessions ?? null;
1465
1611
  this.#getTerminalRows = options.getTerminalRows ?? (() => 24);
1466
1612
  this.#fillHeight = options.fillHeight ?? false;
1467
- this.#title = options.title ?? "Resume Session";
1613
+ this.#title = options.title ?? tuiText("ssTitleResume", "Resume Session");
1468
1614
  this.#actionLabel = options.actionLabel ?? "Resume";
1469
1615
  this.#pickerTitle = options.title;
1470
1616
  this.#standalone = options.standalone ?? false;
1617
+ this.#resumeSource = options.resumeSource;
1618
+ this.#resumeScope = options.resumeSource?.initialScope ?? "workspace";
1619
+ this.#deleteMode = options.deleteMode ?? options.resumeSource?.deleteMode ?? "permanent";
1620
+ const currentSessionPath = options.currentSessionPath;
1621
+ this.#getCurrentSessionPath =
1622
+ typeof currentSessionPath === "function" ? currentSessionPath : () => currentSessionPath;
1471
1623
  this.#scopeLabel = options.scopeLabel;
1472
1624
  this.title = this.#headerLabel();
1473
1625
  // One spacer of breathing room; OverlayPanel supplies the two outer
@@ -1500,7 +1652,7 @@ export class SessionSelectorComponent<T extends SessionSelectorEntry = SessionSe
1500
1652
  onExit();
1501
1653
  };
1502
1654
  this.#sessionList.onRequestRender = () => this.#onRequestRender?.();
1503
- if (this.#onDelete) {
1655
+ if (this.#onDelete || this.#resumeSource?.deleteConversation) {
1504
1656
  this.#sessionList.onDeleteRequest = (session: T) => {
1505
1657
  this.#showDeleteConfirmation(session);
1506
1658
  };
@@ -1510,14 +1662,58 @@ export class SessionSelectorComponent<T extends SessionSelectorEntry = SessionSe
1510
1662
  void this.#toggleScope();
1511
1663
  };
1512
1664
  }
1665
+ this.#resumeHandlers = { onSelect, onCancel, onExit };
1666
+ this.#resumeTreeSource = (
1667
+ options as SessionSelectorOptions<T> & { resumeSource?: ResumeTreeSource }
1668
+ ).resumeSource;
1669
+ this.#getTerminalRowsOption = options.getTerminalRows;
1670
+ // The resume picker's Tab cycles the three-way scope; it replaces the
1671
+ // folder/all toggle so the keys and the segmented control agree.
1672
+ if (this.#resumeSource) this.#sessionList.onToggleScope = () => this.#cycleResumeScope();
1513
1673
  this.#contentSlot = new Container();
1514
1674
  this.#contentSlot.addChild(this.#sessionList);
1515
1675
  this.addChild(this.#contentSlot);
1676
+ this.#mountResumeTree();
1677
+ }
1678
+
1679
+ /**
1680
+ * Swap the flat list for the ANSI three-level tree on the first frame that
1681
+ * reports no native `picker` support. Native terminals keep the flat list and
1682
+ * drive the resume model through `#describePicker`, so the tree never sees
1683
+ * their keys.
1684
+ */
1685
+ #mountResumeTree(): void {
1686
+ if (this.#resumeTree || !this.#resumeTreeSource || !this.#resumeHandlers || !this.#contentSlot) return;
1687
+ const handlers = this.#resumeHandlers;
1688
+ this.#resumeTree = new ResumeTreeComponent({
1689
+ source: this.#resumeTreeSource,
1690
+ getTerminalRows: this.#getTerminalRowsOption,
1691
+ onSelect: conversation => {
1692
+ this.#sessionList.dispose();
1693
+ handlers.onSelect(conversation.session as unknown as T);
1694
+ },
1695
+ onCancel: () => {
1696
+ this.#sessionList.dispose();
1697
+ handlers.onCancel();
1698
+ },
1699
+ onExit: () => {
1700
+ this.#sessionList.dispose();
1701
+ handlers.onExit();
1702
+ },
1703
+ });
1704
+ this.#resumeTree.onRequestRender = () => this.#onRequestRender?.();
1705
+ this.#contentSlot.clear();
1706
+ this.#contentSlot.addChild(this.#resumeTree);
1707
+ this.title = this.#title;
1516
1708
  }
1517
1709
 
1518
1710
  #headerLabel(): string {
1519
1711
  if (this.#scopeLabel === false) return this.#title;
1520
- const scopeLabel = this.#scopeLabel ?? (this.#scope === "all" ? "all projects" : "current folder");
1712
+ const scopeLabel =
1713
+ this.#scopeLabel ??
1714
+ (this.#scope === "all"
1715
+ ? tuiText("resumeScopeAll", "all projects")
1716
+ : tuiText("resumeScopeWorkspace", "this workspace"));
1521
1717
  return `${this.#title} (${scopeLabel})`;
1522
1718
  }
1523
1719
 
@@ -1534,8 +1730,9 @@ export class SessionSelectorComponent<T extends SessionSelectorEntry = SessionSe
1534
1730
  if (!this.#loadAllSessions) return;
1535
1731
  this.#toggling = true;
1536
1732
  this.#messageContainer.clear();
1537
- this.#messageContainer.addChild(new Text(theme.fg("muted", "Loading all projects…"), 0, 0));
1538
- this.#message = { kind: "loading", text: "Loading all projects…" };
1733
+ const loading = tuiText("resumeLoading", "Loading conversations…");
1734
+ this.#messageContainer.addChild(new Text(theme.fg("muted", loading), 0, 0));
1735
+ this.#message = { kind: "loading", text: loading };
1539
1736
  this.#onRequestRender?.();
1540
1737
  try {
1541
1738
  global = await this.#loadAllSessions();
@@ -1562,6 +1759,7 @@ export class SessionSelectorComponent<T extends SessionSelectorEntry = SessionSe
1562
1759
 
1563
1760
  setOnRequestRender(callback: () => void): void {
1564
1761
  this.#onRequestRender = callback;
1762
+ if (this.#resumeTree) this.#resumeTree.onRequestRender = callback;
1565
1763
  }
1566
1764
  /** Ignore input after selection while the host resumes the session. */
1567
1765
  lockInput(): void {
@@ -1579,10 +1777,25 @@ export class SessionSelectorComponent<T extends SessionSelectorEntry = SessionSe
1579
1777
  */
1580
1778
  override dispose(): void {
1581
1779
  this.#sessionList.dispose();
1780
+ this.#resumeTree?.dispose();
1582
1781
  clearTimeout(this.#previewTimer);
1583
1782
  super.dispose();
1584
1783
  }
1585
1784
 
1785
+ /** True when some deleter is wired (the host callback or the resume source). */
1786
+ #canDelete(): boolean {
1787
+ return this.#onDelete !== undefined || this.#resumeSource?.deleteConversation !== undefined;
1788
+ }
1789
+
1790
+ /** Delete through the host callback, else the resume source's conversation entry. */
1791
+ #deleteSession(session: T): Promise<boolean> {
1792
+ if (this.#onDelete) return this.#onDelete(session);
1793
+ const source = this.#resumeSource;
1794
+ if (!source?.deleteConversation || !source.directory) return Promise.resolve(false);
1795
+ const context = resumeConversationContext(source.directory, session.path);
1796
+ return Promise.resolve(context ? source.deleteConversation(context.conversation) : false);
1797
+ }
1798
+
1586
1799
  #clearError(): void {
1587
1800
  this.#messageContainer.clear();
1588
1801
  this.#message = undefined;
@@ -1609,11 +1822,12 @@ export class SessionSelectorComponent<T extends SessionSelectorEntry = SessionSe
1609
1822
  this.#onRequestRender?.();
1610
1823
  };
1611
1824
  const answer = async (option: string) => {
1612
- if (option === "Yes" && this.#onDelete) {
1825
+ if (option === "Yes" && this.#canDelete()) {
1613
1826
  this.#clearError();
1614
1827
  try {
1615
- const deleted = await this.#onDelete(session);
1828
+ const deleted = await this.#deleteSession(session);
1616
1829
  if (deleted) {
1830
+ this.#resumeHidden.add(session.path);
1617
1831
  this.#sessionList.removeSession(session.path);
1618
1832
  this.#folderSessions = this.#folderSessions.filter(s => s.path !== session.path);
1619
1833
  if (this.#globalSessions) {
@@ -1677,13 +1891,35 @@ export class SessionSelectorComponent<T extends SessionSelectorEntry = SessionSe
1677
1891
 
1678
1892
  /** Blank · keybinding hint · bottom border. Rendered by {@link render}. */
1679
1893
  #footerLines(width: number): string[] {
1680
- const scopeHint = this.#scope === "all" ? "current folder" : "all projects";
1681
- // Keys mirror SessionList#handleInput; cancel is `app.interrupt` (raw Escape when unbound).
1682
1894
  const cancel = interruptKey();
1683
- const hint = theme.fg(
1684
- "muted",
1685
- `[${this.#onDelete ? `${formatKeyHints(["delete", "backspace"])} delete · ` : ""}${formatKeyHint("enter")} select · ${formatKeyHint("tab")} ${scopeHint} · ${cancel} cancel]`,
1686
- );
1895
+ const hint = this.#resumeTree
1896
+ ? composeFooter(
1897
+ width,
1898
+ [
1899
+ ...(this.#resumeTree.scope === "workspace"
1900
+ ? []
1901
+ : [`^f ${tuiText("resumeFooterPaths", "full paths")}`]),
1902
+ `${formatKeyHint("enter")} resume`,
1903
+ `^r ${tuiText("resumeMenuRename", "rename")}`,
1904
+ `${formatKeyHint("delete")} delete`,
1905
+ `${formatKeyHint("tab")} ${tuiText("resumeFooterScope", "scope")}`,
1906
+ `/ ${tuiText("resumeFooterSearch", "search")}`,
1907
+ `r ${tuiText("resumeFooterRefresh", "refresh")}`,
1908
+ `p/P ${tuiText("resumeFooterPin", "pin")}`,
1909
+ `m ${tuiText("resumeFooterMenu", "menu")}`,
1910
+ `${cancel} ${tuiText("resumeFooterClose", "clear/close")}`,
1911
+ ],
1912
+ [
1913
+ tuiText("resumeFooterClick", "click select"),
1914
+ tuiText("resumeFooterDoubleClick", "dbl-click resume"),
1915
+ tuiText("resumeFooterWheel", "wheel"),
1916
+ `⋯ ${tuiText("resumeFooterMenu", "menu")}`,
1917
+ ],
1918
+ )
1919
+ : theme.fg(
1920
+ "muted",
1921
+ `[${this.#onDelete ? `${formatKeyHints(["delete", "backspace"])} delete · ` : ""}${formatKeyHint("enter")} select · ${formatKeyHint("tab")} ${this.#scope === "all" ? tuiText("resumeScopeWorkspace", "this workspace") : tuiText("resumeScopeAll", "all projects")} · ${cancel} cancel]`,
1922
+ );
1687
1923
  return [row("", width), row(hint, width), row("", width), bottomBorder(width)];
1688
1924
  }
1689
1925
 
@@ -1704,7 +1940,14 @@ export class SessionSelectorComponent<T extends SessionSelectorEntry = SessionSe
1704
1940
  * path — and the key-hint footer.
1705
1941
  */
1706
1942
  override describe(cx: DescribeContext): NativeNode {
1707
- if (cx.supports("picker")) return this.#describePicker();
1943
+ if (cx.supports("picker")) {
1944
+ // The native picker owns its own event model; raw keys must keep going
1945
+ // to the list even though the ANSI tree is constructed.
1946
+ this.#nativePicker = true;
1947
+ return this.#describePicker();
1948
+ }
1949
+ this.#nativePicker = false;
1950
+ this.#mountResumeTree();
1708
1951
  const list = this.#sessionList.describeList();
1709
1952
  const memo = this.#nativeMemo;
1710
1953
  if (
@@ -1752,6 +1995,283 @@ export class SessionSelectorComponent<T extends SessionSelectorEntry = SessionSe
1752
1995
  return result;
1753
1996
  }
1754
1997
 
1998
+ /**
1999
+ * Resume-model sheet: the model's project/workspace sections (or its
2000
+ * workspace sections in project scope), three-way scope tabs with counts,
2001
+ * conversation cards with turns/branch/model facts, the resume-tree
2002
+ * preview and the same action bar the ANSI footer carries (resume, delete,
2003
+ * pin, refresh, scope, close).
2004
+ */
2005
+ #describeResumePicker(): NativeNode {
2006
+ const source = this.#resumeSource!;
2007
+ const list = this.#sessionList;
2008
+ const model = this.#resumeModel();
2009
+ if (!model) return this.#describeResumeLoading(source);
2010
+ list.syncSessions(this.#resumeSessions(model), list.selectedSession?.path ?? model.selected);
2011
+ const selected = list.selectedSession?.path ?? null;
2012
+ const preview = this.#previewFor(list.selectedSession, session => {
2013
+ const context = source.directory ? resumeConversationContext(source.directory, session.path) : undefined;
2014
+ return context ? resumeConversationPreview(context.conversation, context.project, context.workspace) : [];
2015
+ });
2016
+ const message = this.#message;
2017
+ const errorOpen = this.#pickerErrorOpen && message?.kind === "error";
2018
+ const choice = this.#deleteChoice;
2019
+ const memo = this.#resumePickerMemo;
2020
+ if (
2021
+ memo?.model === model &&
2022
+ memo.preview === preview &&
2023
+ memo.choice === choice &&
2024
+ memo.errorOpen === errorOpen &&
2025
+ memo.message === message &&
2026
+ memo.selected === selected
2027
+ ) {
2028
+ return memo.node;
2029
+ }
2030
+ const loading = message?.kind === "loading";
2031
+ const kind = model.view.scope.kind;
2032
+ const selectedConversation =
2033
+ selected && source.directory ? resumeConversationContext(source.directory, selected) : undefined;
2034
+ const actions = [pickerAction("resume", this.#actionLabel, "enter", { primary: true })];
2035
+ if (this.#canDelete()) {
2036
+ actions.push(
2037
+ pickerAction(
2038
+ "delete",
2039
+ this.#deleteMode === "trash"
2040
+ ? tuiText("resumeDeleteTrash", "Move to trash")
2041
+ : tuiText("resumeDeletePermanent", "Delete"),
2042
+ "backspace",
2043
+ ),
2044
+ );
2045
+ }
2046
+ if (source.pinConversation && selectedConversation) {
2047
+ actions.push(
2048
+ pickerAction(
2049
+ "pin",
2050
+ selectedConversation.conversation.pinned
2051
+ ? tuiText("resumeUnpinAction", "Unpin")
2052
+ : tuiText("resumePinAction", "Pin"),
2053
+ "p",
2054
+ ),
2055
+ );
2056
+ }
2057
+ if (source.refresh) actions.push(pickerAction("refresh", tuiText("resumeRefreshAction", "Refresh"), "r"));
2058
+ if (list.onToggleScope) actions.push(pickerAction("scope", resumeScopeLabel(nextResumeScope(kind)), "tab"));
2059
+ actions.push(
2060
+ pickerAction("close", "Close", boundKeys("app.interrupt", ["escape"])[0] ?? "escape", { end: true }),
2061
+ );
2062
+ const banner =
2063
+ !loading && !errorOpen && model.hidden > 0
2064
+ ? tuiTextFmt("resumeHiddenByFilterFmt", "%d conversations hidden by filter", model.hidden)
2065
+ : undefined;
2066
+ const title = this.#pickerTitle;
2067
+ const folder = this.#scopeLabel === false ? undefined : (this.#scopeLabel ?? path.basename(getProjectDir()));
2068
+ const result = picker(
2069
+ {
2070
+ ...(title ? { title, ...(folder ? { subtitle: folder } : {}) } : {}),
2071
+ icon: "history",
2072
+ noun: "conversations",
2073
+ size: this.#standalone ? "screen" : "lg",
2074
+ layout: "cards",
2075
+ preview: "side",
2076
+ query: list.searchInput.getValue(),
2077
+ cursor: list.searchInput.getCursor(),
2078
+ placeholder: model.placeholder,
2079
+ tabs: model.tabs,
2080
+ tab: kind,
2081
+ columns: [
2082
+ { id: "when", format: "time" },
2083
+ { id: "turns", format: "num" },
2084
+ { id: "branch", format: "dim", priority: 1 },
2085
+ { id: "model", format: "dim", priority: 2 },
2086
+ ],
2087
+ items: model.items,
2088
+ order: model.order,
2089
+ ...(model.hits ? { hits: model.hits } : {}),
2090
+ selected,
2091
+ current: model.current,
2092
+ total: model.total,
2093
+ actions,
2094
+ state: loading ? "loading" : errorOpen ? "error" : "ready",
2095
+ ...((loading || errorOpen) && message ? { message: message.text } : banner ? { message: banner } : {}),
2096
+ empty: model.empty,
2097
+ confirm: choice
2098
+ ? {
2099
+ text:
2100
+ this.#deleteMode === "trash"
2101
+ ? tuiTextFmt("resumeDeleteTrashFmt", "Move “%s” to trash?", sessionLabel(choice.session))
2102
+ : tuiTextFmt(
2103
+ "resumeDeletePermanentFmt",
2104
+ "Permanently delete “%s”?",
2105
+ sessionLabel(choice.session),
2106
+ ),
2107
+ act: "delete-confirm",
2108
+ label:
2109
+ this.#deleteMode === "trash"
2110
+ ? tuiText("resumeDeleteTrash", "Move to trash")
2111
+ : tuiText("resumeDeletePermanent", "Delete"),
2112
+ }
2113
+ : null,
2114
+ },
2115
+ preview,
2116
+ );
2117
+ this.#resumePickerMemo = { model, preview, choice, errorOpen, message, selected, node: result };
2118
+ return result;
2119
+ }
2120
+
2121
+ /** Skeleton sheet while the host's directory scan is still running (or failed). */
2122
+ #describeResumeLoading(source: ResumeSourceLike): NativeNode {
2123
+ const list = this.#sessionList;
2124
+ const error = this.#message?.kind === "error" ? this.#message.text : source.loadError?.();
2125
+ const query = list.searchInput.getValue();
2126
+ const cursor = list.searchInput.getCursor();
2127
+ const memo = this.#resumeLoadingMemo;
2128
+ if (memo?.source === source && memo.error === error && memo.query === query && memo.cursor === cursor) {
2129
+ return memo.node;
2130
+ }
2131
+ const title = this.#pickerTitle;
2132
+ const node = picker({
2133
+ ...(title ? { title } : {}),
2134
+ icon: "history",
2135
+ noun: "conversations",
2136
+ size: this.#standalone ? "screen" : "lg",
2137
+ layout: "cards",
2138
+ preview: "none",
2139
+ query,
2140
+ cursor,
2141
+ items: [],
2142
+ order: [],
2143
+ selected: null,
2144
+ actions: [
2145
+ pickerAction("close", "Close", boundKeys("app.interrupt", ["escape"])[0] ?? "escape", { end: true }),
2146
+ ],
2147
+ state: error ? "error" : "loading",
2148
+ message: error ?? tuiText("resumeLoading", "Loading conversations…"),
2149
+ });
2150
+ this.#resumeLoadingMemo = { source, error, query, cursor, node };
2151
+ return node;
2152
+ }
2153
+
2154
+ /** The memoized resume model for the active scope, query and directory snapshot. */
2155
+ #resumeModel(): ResumePickerModel | undefined {
2156
+ const source = this.#resumeSource!;
2157
+ const query = this.#sessionList.searchInput.getValue();
2158
+ const currentPath = this.#getCurrentSessionPath();
2159
+ const scope = this.#resumeScope;
2160
+ const scopeKey =
2161
+ typeof scope === "string" ? scope : `${scope.kind}:${scope.projectKey ?? ""}:${scope.workspaceKey ?? ""}`;
2162
+ const kind = typeof scope === "string" ? scope : scope.kind;
2163
+ const showCwd = kind !== "workspace";
2164
+ const cached = this.#resumeModelMemo;
2165
+ if (
2166
+ cached?.source === source &&
2167
+ cached.directory === source.directory &&
2168
+ cached.query === query &&
2169
+ cached.scopeKey === scopeKey &&
2170
+ cached.currentPath === currentPath &&
2171
+ cached.hidden === this.#resumeHidden.size &&
2172
+ cached.showCwd === showCwd
2173
+ ) {
2174
+ return cached.model;
2175
+ }
2176
+ const model = buildResumePickerModel(source, {
2177
+ scope,
2178
+ query,
2179
+ currentSessionPath: currentPath,
2180
+ hidden: this.#resumeHidden,
2181
+ showCwd,
2182
+ preferredPath: this.#sessionList.selectedSession?.path,
2183
+ });
2184
+ this.#resumeModelMemo = {
2185
+ source,
2186
+ directory: source.directory,
2187
+ query,
2188
+ scopeKey,
2189
+ currentPath,
2190
+ hidden: this.#resumeHidden.size,
2191
+ showCwd,
2192
+ model,
2193
+ };
2194
+ return model;
2195
+ }
2196
+
2197
+ /** The view's sessions as list entries; rebuilt only when the view itself changes. */
2198
+ #resumeSessions(model: ResumePickerModel): readonly T[] {
2199
+ const memo = this.#resumeSessionsMemo;
2200
+ if (memo?.model === model) return memo.sessions;
2201
+ const sessions = (model.view.conversations ?? []).map(row => row.conversation.session as unknown as T);
2202
+ this.#resumeSessionsMemo = { model, sessions };
2203
+ return sessions;
2204
+ }
2205
+
2206
+ #invalidateResume(): void {
2207
+ this.#resumeModelMemo = undefined;
2208
+ this.#resumeSessionsMemo = undefined;
2209
+ this.#resumePickerMemo = undefined;
2210
+ }
2211
+
2212
+ /** Pin/unpin the selected conversation, then re-read the directory. */
2213
+ #toggleResumePin(): void {
2214
+ const source = this.#resumeSource;
2215
+ const pin = source?.pinConversation;
2216
+ const path = this.#sessionList.selectedSession?.path;
2217
+ if (!source || !pin || !path || !source.directory) return;
2218
+ const context = resumeConversationContext(source.directory, path);
2219
+ if (!context) return;
2220
+ void (async () => {
2221
+ await pin.call(source, context.conversation, !context.conversation.pinned);
2222
+ await source.refresh?.();
2223
+ this.#invalidateResume();
2224
+ this.#onRequestRender?.();
2225
+ })().catch((error: unknown) => {
2226
+ this.#showError(error instanceof Error ? error.message : String(error));
2227
+ this.#onRequestRender?.();
2228
+ });
2229
+ }
2230
+
2231
+ /** Re-scan the directory, showing the loading line meanwhile. */
2232
+ #refreshResume(): void {
2233
+ const source = this.#resumeSource;
2234
+ const refresh = source?.refresh;
2235
+ if (!source || !refresh) return;
2236
+ this.#message = { kind: "loading", text: tuiText("resumeLoading", "Loading conversations…") };
2237
+ this.#invalidateResume();
2238
+ this.#onRequestRender?.();
2239
+ void (async () => {
2240
+ await refresh.call(source);
2241
+ if (this.#message?.kind === "loading") this.#message = undefined;
2242
+ this.#invalidateResume();
2243
+ this.#onRequestRender?.();
2244
+ })().catch((error: unknown) => {
2245
+ this.#showError(error instanceof Error ? error.message : String(error));
2246
+ this.#onRequestRender?.();
2247
+ });
2248
+ }
2249
+
2250
+ /** Tab / the scope button: workspace → project → all → workspace. */
2251
+ #cycleResumeScope(): void {
2252
+ const current = this.#resumeModel()?.view.scope;
2253
+ this.#setResumeScope(
2254
+ nextResumeScope(
2255
+ current?.kind ?? (typeof this.#resumeScope === "string" ? this.#resumeScope : this.#resumeScope.kind),
2256
+ ),
2257
+ );
2258
+ }
2259
+
2260
+ /** Switch the three-way scope, pinning the group the current scope resolved to. */
2261
+ #setResumeScope(kind: ResumeScopeKindLike): void {
2262
+ const current = this.#resumeModel()?.view.scope;
2263
+ if (kind === "all") this.#resumeScope = "all";
2264
+ else if (kind === "project") {
2265
+ this.#resumeScope = current?.projectKey ? { kind, projectKey: current.projectKey } : kind;
2266
+ } else {
2267
+ this.#resumeScope =
2268
+ current?.kind === "workspace" && current.workspaceKey ? { kind, workspaceKey: current.workspaceKey } : kind;
2269
+ }
2270
+ void this.#resumeSource?.persistScope?.(kind);
2271
+ this.#invalidateResume();
2272
+ this.#onRequestRender?.();
2273
+ }
2274
+
1755
2275
  /**
1756
2276
  * `lg` cards sheet (`screen` for the standalone app): sessions grouped by
1757
2277
  * day (ranked flat while searching), the selected session's preview, the
@@ -1760,6 +2280,7 @@ export class SessionSelectorComponent<T extends SessionSelectorEntry = SessionSe
1760
2280
  * Untitled unless the caller named it; the placeholder says the scope.
1761
2281
  */
1762
2282
  #describePicker(): NativeNode {
2283
+ if (this.#resumeSource) return this.#describeResumePicker();
1763
2284
  const list = this.#sessionList;
1764
2285
  const view = list.pickerView();
1765
2286
  const preview = this.#previewFor(list.selectedSession);
@@ -1840,7 +2361,14 @@ export class SessionSelectorComponent<T extends SessionSelectorEntry = SessionSe
1840
2361
  * selection, then only after the selection has rested
1841
2362
  * {@link PREVIEW_SETTLE_MS} (the old preview stays meanwhile).
1842
2363
  */
1843
- #previewFor(target: T | undefined): readonly NativeChild[] {
2364
+ #previewFor(
2365
+ target: T | undefined,
2366
+ build: (session: T) => readonly NativeChild[] = session =>
2367
+ sessionPreview(
2368
+ session,
2369
+ session.parentSessionPath ? this.#sessionList.parentLabel(session.parentSessionPath) : undefined,
2370
+ ),
2371
+ ): readonly NativeChild[] {
1844
2372
  const shown = this.#preview;
1845
2373
  if (shown && shown.session === target) return shown.nodes;
1846
2374
  if (shown && target !== this.#previewSettled) {
@@ -1855,12 +2383,7 @@ export class SessionSelectorComponent<T extends SessionSelectorEntry = SessionSe
1855
2383
  }
1856
2384
  return shown.nodes;
1857
2385
  }
1858
- const nodes = target
1859
- ? sessionPreview(
1860
- target,
1861
- target.parentSessionPath ? this.#sessionList.parentLabel(target.parentSessionPath) : undefined,
1862
- )
1863
- : [];
2386
+ const nodes = target ? build(target) : [];
1864
2387
  this.#preview = { session: target, nodes };
1865
2388
  return nodes;
1866
2389
  }
@@ -1882,12 +2405,16 @@ export class SessionSelectorComponent<T extends SessionSelectorEntry = SessionSe
1882
2405
  }
1883
2406
  if (ev.act === "delete-confirm") choice?.confirm();
1884
2407
  else if (ev.act === "cancel") choice?.cancel();
1885
- else if (ev.act === "close") {
2408
+ else if (ev.act === "tab") {
2409
+ if (this.#resumeSource && ev.value) this.#setResumeScope(ev.value as ResumeScopeKindLike);
2410
+ } else if (ev.act === "close") {
1886
2411
  if (choice) choice.cancel();
1887
2412
  else list.onCancel?.();
1888
2413
  } else if (choice) return;
1889
2414
  else if (ev.act === "resume") list.resumeSelected();
1890
2415
  else if (ev.act === "delete") list.requestDelete();
2416
+ else if (ev.act === "pin") this.#toggleResumePin();
2417
+ else if (ev.act === "refresh") this.#refreshResume();
1891
2418
  else if (ev.act === "scope") {
1892
2419
  list.onToggleScope?.();
1893
2420
  } else if (ev.act === "clear") list.clearSearch();
@@ -1919,6 +2446,8 @@ export class SessionSelectorComponent<T extends SessionSelectorEntry = SessionSe
1919
2446
  }
1920
2447
  if (this.#confirmationDialog) {
1921
2448
  this.#confirmationDialog.handleInput(keyData);
2449
+ } else if (this.#resumeTree && !this.#nativePicker) {
2450
+ this.#resumeTree.handleInput(keyData);
1922
2451
  } else {
1923
2452
  this.#sessionList.handleInput(keyData);
1924
2453
  }
@@ -1932,6 +2461,12 @@ export class SessionSelectorComponent<T extends SessionSelectorEntry = SessionSe
1932
2461
  */
1933
2462
  #handleMouse(data: string): void {
1934
2463
  if (this.#confirmationDialog) return;
2464
+ if (this.#resumeTree) {
2465
+ // The panel paints the tree inside `│ ` … ` │`; rows start at
2466
+ // #listLineOffset and the first content column is 2.
2467
+ this.#resumeTree.handleMouse(data, { row: this.#listLineOffset, col: 2 });
2468
+ return;
2469
+ }
1935
2470
  routeSgrMouseInput(data, event => {
1936
2471
  if (event.wheel !== null) {
1937
2472
  this.#sessionList.handleWheel(event.wheel);
@@ -1947,4 +2482,9 @@ export class SessionSelectorComponent<T extends SessionSelectorEntry = SessionSe
1947
2482
  getSessionList(): SessionList<T> {
1948
2483
  return this.#sessionList;
1949
2484
  }
2485
+
2486
+ /** The ANSI resume tree when the host supplied a resume source. */
2487
+ getResumeTree(): ResumeTreeComponent | undefined {
2488
+ return this.#resumeTree;
2489
+ }
1950
2490
  }