santree 0.7.0 → 0.7.2

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.
@@ -10,7 +10,7 @@ const require = createRequire(import.meta.url);
10
10
  const { version } = require("../../package.json");
11
11
  import { findMainRepoRoot, createWorktree, getDefaultBranch, getBaseBranch, hasInitScript, getInitScriptPath, removeWorktree, getDiffTool, getWorktreeStatus, stageFile, unstageFile, stageAll, unstageAll, discardFile, } from "../lib/git.js";
12
12
  import { run, spawnAsync } from "../lib/exec.js";
13
- import { resolveAgentBinary, fillCommitMessage } from "../lib/ai.js";
13
+ import { resolveAgentBinary, fillCommitMessage, askTicketQuestion } from "../lib/ai.js";
14
14
  import { getInstalledClaudeVersion } from "../lib/version.js";
15
15
  import { extractTicketId, getStagedDiffContent } from "../lib/git.js";
16
16
  import { getMultiplexer } from "../lib/multiplexer/index.js";
@@ -31,6 +31,7 @@ import { loadDashboardData, loadReviewsData } from "../lib/dashboard/data.js";
31
31
  import IssueList, { buildIssueListRows } from "../lib/dashboard/IssueList.js";
32
32
  import { detectTerminalTheme, getThemeForMode, } from "../lib/dashboard/theme.js";
33
33
  import DetailPanel, { buildIssueActions } from "../lib/dashboard/DetailPanel.js";
34
+ import TriageScheduleOverlay, { buildScheduleLines, } from "../lib/dashboard/TriageScheduleOverlay.js";
34
35
  import ReviewList from "../lib/dashboard/ReviewList.js";
35
36
  import ReviewDetailPanel, { buildReviewActions } from "../lib/dashboard/ReviewDetailPanel.js";
36
37
  import { CommitOverlay, PrCreateOverlay, HelpOverlay } from "../lib/dashboard/Overlays.js";
@@ -320,6 +321,10 @@ export default function Dashboard() {
320
321
  // `.code-workspace` file (VSCode/Cursor) AND such a file exists in the
321
322
  // repo root. Recomputed alongside the data refresh.
322
323
  const [hasWorkspaceFile, setHasWorkspaceFile] = useState(false);
324
+ // Whether the active tracker has a triage inbox (Linear). Drives whether the
325
+ // Triage tab appears at all. Recomputed on every data refresh — feature
326
+ // detection via `tracker.supportsTriage`, never a kind check.
327
+ const [supportsTriage, setSupportsTriage] = useState(false);
323
328
  const refreshTimerRef = useRef(null);
324
329
  const repoRootRef = useRef(null);
325
330
  const stateRef = useRef(state);
@@ -407,6 +412,7 @@ export default function Dashboard() {
407
412
  // commit/PR/context message via Ink's stdin handler.
408
413
  const overlay = stateRef.current.overlay;
409
414
  const inTextInput = overlay === "context-input" ||
415
+ (overlay === "triage-ask" && stateRef.current.triageAskPhase === "input") ||
410
416
  (overlay === "pr-create" && stateRef.current.prCreatePhase === "review") ||
411
417
  (overlay === "commit" && stateRef.current.commitPhase === "awaiting-message");
412
418
  const themeP = inTextInput ? Promise.resolve(null) : detectTerminalTheme();
@@ -432,8 +438,18 @@ export default function Dashboard() {
432
438
  }
433
439
  }
434
440
  setHasWorkspaceFile(hasWs);
441
+ const tracker = getIssueTracker(repoRoot);
442
+ setSupportsTriage(tracker.supportsTriage === true);
435
443
  dispatch({ type: "SET_DATA", ...data });
436
444
  dispatch({ type: "SET_REVIEWS_DATA", flatReviews: reviewData.flatReviews });
445
+ // Triage on-call rotations (Linear). Best-effort, non-blocking — never
446
+ // fails the refresh.
447
+ if (tracker.getTriageSchedules) {
448
+ tracker
449
+ .getTriageSchedules(repoRoot)
450
+ .then((schedules) => dispatch({ type: "SET_TRIAGE_SCHEDULES", schedules }))
451
+ .catch(() => { });
452
+ }
437
453
  }
438
454
  catch (e) {
439
455
  dispatch({
@@ -541,20 +557,34 @@ export default function Dashboard() {
541
557
  return;
542
558
  }
543
559
  {
544
- const isTreesTab = s.activeTab === "trees";
545
- const flat = isTreesTab ? s.flatTrees : s.flatIssues;
546
- const idx = isTreesTab ? s.treeSelectedIndex : s.selectedIndex;
547
- const detailOff = isTreesTab ? s.treeDetailScrollOffset : s.detailScrollOffset;
560
+ const tab = s.activeTab;
561
+ const flat = tab === "trees" ? s.flatTrees : tab === "triage" ? s.flatTriage : s.flatIssues;
562
+ const idx = tab === "trees"
563
+ ? s.treeSelectedIndex
564
+ : tab === "triage"
565
+ ? s.triageSelectedIndex
566
+ : s.selectedIndex;
567
+ const detailOff = tab === "trees"
568
+ ? s.treeDetailScrollOffset
569
+ : tab === "triage"
570
+ ? s.triageDetailScrollOffset
571
+ : s.detailScrollOffset;
572
+ const selectType = tab === "trees" ? "TREE_SELECT" : tab === "triage" ? "TRIAGE_SELECT" : "SELECT";
573
+ const scrollType = tab === "trees"
574
+ ? "TREE_SCROLL_DETAIL"
575
+ : tab === "triage"
576
+ ? "TRIAGE_SCROLL_DETAIL"
577
+ : "SCROLL_DETAIL";
548
578
  if (inLeftPane) {
549
579
  const maxIdx = flat.length - 1;
550
580
  if (maxIdx < 0)
551
581
  return;
552
582
  const next = Math.max(0, Math.min(idx + delta, maxIdx));
553
- dispatch({ type: isTreesTab ? "TREE_SELECT" : "SELECT", index: next });
583
+ dispatch({ type: selectType, index: next });
554
584
  }
555
585
  else {
556
586
  const next = Math.max(0, detailOff + delta);
557
- dispatch({ type: isTreesTab ? "TREE_SCROLL_DETAIL" : "SCROLL_DETAIL", offset: next });
587
+ dispatch({ type: scrollType, offset: next });
558
588
  }
559
589
  }
560
590
  return;
@@ -610,6 +640,10 @@ export default function Dashboard() {
610
640
  const s = stateRef.current;
611
641
  if (s.loading || s.error)
612
642
  return;
643
+ // Full-area triage overlays cover the content area — don't let a click
644
+ // fall through to the (hidden) list underneath.
645
+ if (s.overlay === "triage-ask" || s.overlay === "triage-schedule")
646
+ return;
613
647
  if (col < 2 || col > lw + 1)
614
648
  return;
615
649
  // Row 1 = title, row 2 = tab strip, row 3 = top border, content starts at row 4 (1-based)
@@ -628,16 +662,21 @@ export default function Dashboard() {
628
662
  return;
629
663
  }
630
664
  {
631
- const isTreesTab = s.activeTab === "trees";
632
- const flat = isTreesTab ? s.flatTrees : s.flatIssues;
633
- const grps = isTreesTab ? s.treeGroups : s.groups;
634
- const scrollOff = isTreesTab ? s.treeListScrollOffset : s.listScrollOffset;
665
+ const tab = s.activeTab;
666
+ const flat = tab === "trees" ? s.flatTrees : tab === "triage" ? s.flatTriage : s.flatIssues;
667
+ const grps = tab === "trees" ? s.treeGroups : tab === "triage" ? s.triageGroups : s.groups;
668
+ const scrollOff = tab === "trees"
669
+ ? s.treeListScrollOffset
670
+ : tab === "triage"
671
+ ? s.triageListScrollOffset
672
+ : s.listScrollOffset;
673
+ const selectType = tab === "trees" ? "TREE_SELECT" : tab === "triage" ? "TRIAGE_SELECT" : "SELECT";
635
674
  if (flat.length === 0)
636
675
  return;
637
676
  const listRow = scrollOff + contentRow;
638
677
  const flatIdx = getFlatIndexForListRow(grps, flat, listRow);
639
678
  if (flatIdx !== null && flatIdx >= 0 && flatIdx < flat.length) {
640
- dispatch({ type: isTreesTab ? "TREE_SELECT" : "SELECT", index: flatIdx });
679
+ dispatch({ type: selectType, index: flatIdx });
641
680
  }
642
681
  }
643
682
  };
@@ -705,6 +744,72 @@ export default function Dashboard() {
705
744
  dispatch({ type: "TREE_SCROLL_LIST", offset });
706
745
  }
707
746
  }, [state.treeSelectedIndex, state.treeGroups, contentHeight, state.treeListScrollOffset]);
747
+ // ── Triage list scroll tracking ──────────────────────────────────
748
+ useEffect(() => {
749
+ const rowIdx = getRowIndexForFlatIndex(state.triageGroups, state.flatTriage, state.triageSelectedIndex);
750
+ const maxVisible = contentHeight - LIST_FOOTER_HEIGHT;
751
+ let offset = state.triageListScrollOffset;
752
+ if (rowIdx < offset) {
753
+ offset = Math.max(0, rowIdx - 1);
754
+ }
755
+ else if (rowIdx >= offset + maxVisible) {
756
+ offset = rowIdx - maxVisible + 2;
757
+ }
758
+ if (offset !== state.triageListScrollOffset) {
759
+ dispatch({ type: "TRIAGE_SCROLL_LIST", offset });
760
+ }
761
+ }, [state.triageSelectedIndex, state.triageGroups, contentHeight, state.triageListScrollOffset]);
762
+ // ── Triage comment lazy-load ─────────────────────────────────────
763
+ // The assigned-issues list query doesn't include comments — fetch the full
764
+ // issue (with discussion) for the selected triage row on demand and cache it
765
+ // by identifier so the detail panel can show the thread without re-fetching
766
+ // on every keypress.
767
+ useEffect(() => {
768
+ if (state.activeTab !== "triage")
769
+ return;
770
+ const di = state.flatTriage[state.triageSelectedIndex];
771
+ if (!di)
772
+ return;
773
+ const id = di.issue.identifier;
774
+ if (state.triageCommentsById[id] !== undefined)
775
+ return;
776
+ const root = repoRootRef.current;
777
+ if (!root)
778
+ return;
779
+ let cancelled = false;
780
+ void (async () => {
781
+ try {
782
+ const tracker = getIssueTracker(root);
783
+ const res = await tracker.getIssue(id, root);
784
+ if (cancelled)
785
+ return;
786
+ dispatch({
787
+ type: "TRIAGE_COMMENTS_LOADED",
788
+ id,
789
+ comments: res.ok ? res.value.comments : [],
790
+ });
791
+ }
792
+ catch {
793
+ if (!cancelled)
794
+ dispatch({ type: "TRIAGE_COMMENTS_LOADED", id, comments: [] });
795
+ }
796
+ })();
797
+ return () => {
798
+ cancelled = true;
799
+ };
800
+ }, [state.activeTab, state.triageSelectedIndex, state.flatTriage, state.triageCommentsById]);
801
+ // ── Keep activeTab valid ─────────────────────────────────────────
802
+ // The Triage tab can disappear when the tracker is switched away from one
803
+ // that supports it. If the active tab is no longer in the order, fall back
804
+ // to the first tab so the strip + content don't end up on a dead tab.
805
+ useEffect(() => {
806
+ const order = supportsTriage
807
+ ? ["triage", "issues", "trees", "reviews"]
808
+ : ["issues", "trees", "reviews"];
809
+ if (!order.includes(state.activeTab)) {
810
+ dispatch({ type: "SET_TAB", tab: order[0] });
811
+ }
812
+ }, [supportsTriage, state.activeTab]);
708
813
  // ── Review list scroll tracking ──────────────────────────────────
709
814
  useEffect(() => {
710
815
  // Row index = 1 (column header) + flatIndex
@@ -728,6 +833,7 @@ export default function Dashboard() {
728
833
  // Disable tracking while any text-input overlay is mounted; restore on exit.
729
834
  useEffect(() => {
730
835
  const needsMouseOff = state.overlay === "context-input" ||
836
+ (state.overlay === "triage-ask" && state.triageAskPhase === "input") ||
731
837
  (state.overlay === "issue-form" && state.issueFormPhase !== "saving") ||
732
838
  (state.overlay === "pr-create" && state.prCreatePhase === "review") ||
733
839
  (state.overlay === "commit" && state.commitPhase === "awaiting-message");
@@ -737,7 +843,13 @@ export default function Dashboard() {
737
843
  return () => {
738
844
  process.stdout.write("\x1b[?1002h\x1b[?1006h");
739
845
  };
740
- }, [state.overlay, state.issueFormPhase, state.prCreatePhase, state.commitPhase]);
846
+ }, [
847
+ state.overlay,
848
+ state.triageAskPhase,
849
+ state.issueFormPhase,
850
+ state.prCreatePhase,
851
+ state.commitPhase,
852
+ ]);
741
853
  // ── Diff overlay: load file list when opened (gh pr diff path) ────
742
854
  // Reviews-tab PRs without a local worktree shell out to `gh pr diff <n>`,
743
855
  // parse the unified blob into per-file records, and stash the per-file
@@ -1028,7 +1140,9 @@ export default function Dashboard() {
1028
1140
  const sref = stateRef.current;
1029
1141
  const di = sref.activeTab === "trees"
1030
1142
  ? sref.flatTrees[sref.treeSelectedIndex]
1031
- : sref.flatIssues[sref.selectedIndex];
1143
+ : sref.activeTab === "triage"
1144
+ ? sref.flatTriage[sref.triageSelectedIndex]
1145
+ : sref.flatIssues[sref.selectedIndex];
1032
1146
  if (!di)
1033
1147
  return;
1034
1148
  const repoRoot = repoRootRef.current;
@@ -1139,7 +1253,9 @@ export default function Dashboard() {
1139
1253
  const doWork = useCallback((mode, customContext) => {
1140
1254
  const di = state.activeTab === "trees"
1141
1255
  ? state.flatTrees[state.treeSelectedIndex]
1142
- : state.flatIssues[state.selectedIndex];
1256
+ : state.activeTab === "triage"
1257
+ ? state.flatTriage[state.triageSelectedIndex]
1258
+ : state.flatIssues[state.selectedIndex];
1143
1259
  if (!di)
1144
1260
  return;
1145
1261
  const repoRoot = repoRootRef.current;
@@ -1188,12 +1304,62 @@ export default function Dashboard() {
1188
1304
  state.selectedIndex,
1189
1305
  state.flatTrees,
1190
1306
  state.treeSelectedIndex,
1307
+ state.flatTriage,
1308
+ state.triageSelectedIndex,
1191
1309
  state.activeTab,
1192
1310
  exit,
1193
1311
  launchWorkInTmux,
1194
1312
  proceedAfterBaseSelect,
1195
1313
  writeContextFile,
1196
1314
  ]);
1315
+ // ── Triage: ask Claude a clarifying question ─────────────────────
1316
+ // One-shot, read-only Q&A over the selected triage issue plus its full
1317
+ // comment thread. Runs non-interactively (async so the spinner keeps
1318
+ // animating) and shows the answer in the overlay.
1319
+ const doAsk = useCallback(async (question) => {
1320
+ const s = stateRef.current;
1321
+ const di = s.flatTriage[s.triageSelectedIndex];
1322
+ const root = repoRootRef.current;
1323
+ if (!di || !root)
1324
+ return;
1325
+ dispatch({ type: "TRIAGE_ASK_RUN" });
1326
+ try {
1327
+ const tracker = getIssueTracker(root);
1328
+ const res = await tracker.getIssue(di.issue.identifier, root);
1329
+ if (!res.ok) {
1330
+ dispatch({
1331
+ type: "TRIAGE_ASK_ERROR",
1332
+ error: res.message ?? `Could not load ${di.issue.identifier}`,
1333
+ });
1334
+ return;
1335
+ }
1336
+ // Cache the freshly-fetched comments so the detail panel reflects them too.
1337
+ dispatch({
1338
+ type: "TRIAGE_COMMENTS_LOADED",
1339
+ id: di.issue.identifier,
1340
+ comments: res.value.comments,
1341
+ });
1342
+ const result = await askTicketQuestion({
1343
+ ticket: res.value,
1344
+ trackerName: tracker.displayName,
1345
+ question,
1346
+ });
1347
+ if (!result.success || !result.output) {
1348
+ dispatch({
1349
+ type: "TRIAGE_ASK_ERROR",
1350
+ error: result.output || "Claude returned no answer (is the CLI authenticated?)",
1351
+ });
1352
+ return;
1353
+ }
1354
+ dispatch({ type: "TRIAGE_ASK_ANSWER", answer: result.output });
1355
+ }
1356
+ catch (e) {
1357
+ dispatch({
1358
+ type: "TRIAGE_ASK_ERROR",
1359
+ error: e instanceof Error ? e.message : "Ask failed",
1360
+ });
1361
+ }
1362
+ }, []);
1197
1363
  // ── Tracker selection ────────────────────────────────────────────
1198
1364
  // Mirrors `santree issue setup`. Local needs no account; Linear picks an
1199
1365
  // authenticated workspace (sub-list when >1); GitHub verifies `gh`.
@@ -1458,6 +1624,25 @@ export default function Dashboard() {
1458
1624
  dispatch({ type: "SET_ACTION_MESSAGE", message: "Failed to open workspace" });
1459
1625
  }
1460
1626
  }, []);
1627
+ // ── Worktree removal ─────────────────────────────────────────────
1628
+ // Runs a single deletion to completion, streaming staged progress into
1629
+ // `deletingTickets[ticketId]` (rendered in the detail pane). Designed to be
1630
+ // fired without awaiting so several worktrees can be removed concurrently —
1631
+ // all args are captured, nothing is read from state inside.
1632
+ const removeWorktreeWithProgress = useCallback(async (ticketId, branch, repoRoot, force) => {
1633
+ dispatch({ type: "DELETE_START", ticketId });
1634
+ const result = await removeWorktree(branch, repoRoot, force, (msg) => dispatch({ type: "DELETE_LOG", ticketId, logs: `${msg}\n` }));
1635
+ if (result.success) {
1636
+ dispatch({ type: "DELETE_DONE", ticketId });
1637
+ dispatch({ type: "SET_ACTION_MESSAGE", message: `Removed worktree for ${ticketId}` });
1638
+ refresh();
1639
+ }
1640
+ else {
1641
+ const error = result.error ?? "Unknown error";
1642
+ dispatch({ type: "DELETE_ERROR", ticketId, error });
1643
+ dispatch({ type: "SET_ACTION_MESSAGE", message: `Failed to remove ${ticketId}: ${error}` });
1644
+ }
1645
+ }, [refresh]);
1461
1646
  // ── PR create flow ───────────────────────────────────────────────
1462
1647
  const doPrCreate = useCallback(async (fill) => {
1463
1648
  const s = stateRef.current;
@@ -1636,6 +1821,11 @@ export default function Dashboard() {
1636
1821
  }
1637
1822
  }, [refresh]);
1638
1823
  // ── Keyboard ──────────────────────────────────────────────────────
1824
+ // Tab order + numeric keybinds. Triage leads (it's the inbox) but only
1825
+ // appears when the active tracker has a triage concept.
1826
+ const tabOrder = supportsTriage
1827
+ ? ["triage", "issues", "trees", "reviews"]
1828
+ : ["issues", "trees", "reviews"];
1639
1829
  useInput((input, key) => {
1640
1830
  // Clear action messages on any keypress
1641
1831
  if (state.actionMessage && input !== "q") {
@@ -1813,6 +2003,60 @@ export default function Dashboard() {
1813
2003
  if (state.overlay === "context-input") {
1814
2004
  return;
1815
2005
  }
2006
+ // Triage "ask Claude" overlay. The input phase is owned by
2007
+ // MultilineTextArea (outer disabled via isActive); running swallows
2008
+ // keys; answer/error are navigable + closable here.
2009
+ if (state.overlay === "triage-ask") {
2010
+ if (state.triageAskPhase === "input" || state.triageAskPhase === "running")
2011
+ return;
2012
+ if (key.escape || input === "q") {
2013
+ dispatch({ type: "TRIAGE_ASK_CLOSE" });
2014
+ return;
2015
+ }
2016
+ if (input === "a") {
2017
+ // Ask another question about the same issue (resets to input).
2018
+ dispatch({ type: "TRIAGE_ASK_OPEN", ticketId: state.triageAskTicketId ?? "" });
2019
+ return;
2020
+ }
2021
+ if (state.triageAskPhase === "answer") {
2022
+ if ((key.shift && key.downArrow) || input === "j") {
2023
+ dispatch({ type: "TRIAGE_ASK_SCROLL", offset: state.triageAskScrollOffset + 3 });
2024
+ return;
2025
+ }
2026
+ if ((key.shift && key.upArrow) || input === "k") {
2027
+ dispatch({
2028
+ type: "TRIAGE_ASK_SCROLL",
2029
+ offset: Math.max(0, state.triageAskScrollOffset - 3),
2030
+ });
2031
+ return;
2032
+ }
2033
+ }
2034
+ return;
2035
+ }
2036
+ // Triage on-call schedule overlay — read-only; scroll + close.
2037
+ if (state.overlay === "triage-schedule") {
2038
+ if (key.escape || input === "q") {
2039
+ dispatch({ type: "TRIAGE_SCHEDULE_CLOSE" });
2040
+ return;
2041
+ }
2042
+ const total = buildScheduleLines(state.triageSchedules).length;
2043
+ const maxOffset = Math.max(0, total - Math.max(1, contentHeight - 3));
2044
+ if (input === "j" || key.downArrow) {
2045
+ dispatch({
2046
+ type: "TRIAGE_SCHEDULE_SCROLL",
2047
+ offset: Math.min(maxOffset, state.triageScheduleScrollOffset + 3),
2048
+ });
2049
+ return;
2050
+ }
2051
+ if (input === "k" || key.upArrow) {
2052
+ dispatch({
2053
+ type: "TRIAGE_SCHEDULE_SCROLL",
2054
+ offset: Math.max(0, state.triageScheduleScrollOffset - 3),
2055
+ });
2056
+ return;
2057
+ }
2058
+ return;
2059
+ }
1816
2060
  // Diff overlay
1817
2061
  if (state.overlay === "diff") {
1818
2062
  // Pending discard modal — intercepts y/n/ESC/q so they don't
@@ -2004,30 +2248,17 @@ export default function Dashboard() {
2004
2248
  if (state.overlay === "confirm-delete") {
2005
2249
  if (input === "y") {
2006
2250
  dispatch({ type: "SET_OVERLAY", overlay: null });
2007
- // Worktree removal is a Trees-tab action.
2251
+ // Worktree removal is a Trees-tab action. Fire-and-forget so the
2252
+ // user can immediately confirm another deletion — each removal
2253
+ // closes over its own ticket/branch and streams progress into
2254
+ // `deletingTickets[ticketId]`, rendered in the detail pane.
2008
2255
  const di = state.flatTrees[state.treeSelectedIndex];
2009
- if (di?.worktree) {
2010
- const repoRoot = repoRootRef.current;
2011
- if (repoRoot) {
2012
- dispatch({ type: "DELETE_START", ticketId: di.issue.identifier });
2013
- const force = di.worktree.dirty;
2014
- removeWorktree(di.worktree.branch, repoRoot, force).then((result) => {
2015
- dispatch({ type: "DELETE_DONE" });
2016
- if (result.success) {
2017
- dispatch({
2018
- type: "SET_ACTION_MESSAGE",
2019
- message: `Removed worktree for ${di.issue.identifier}`,
2020
- });
2021
- refresh();
2022
- }
2023
- else {
2024
- dispatch({
2025
- type: "SET_ACTION_MESSAGE",
2026
- message: `Failed: ${result.error ?? "Unknown error"}`,
2027
- });
2028
- }
2029
- });
2030
- }
2256
+ const repoRoot = repoRootRef.current;
2257
+ if (di?.worktree && repoRoot) {
2258
+ const ticketId = di.issue.identifier;
2259
+ const branch = di.worktree.branch;
2260
+ const force = di.worktree.dirty;
2261
+ void removeWorktreeWithProgress(ticketId, branch, repoRoot, force);
2031
2262
  }
2032
2263
  return;
2033
2264
  }
@@ -2112,24 +2343,20 @@ export default function Dashboard() {
2112
2343
  if (state.overlay === "issue-form") {
2113
2344
  return;
2114
2345
  }
2115
- // Tab switching (only when no overlay is active)
2116
- const TAB_ORDER = ["issues", "trees", "reviews"];
2346
+ // Tab switching (only when no overlay is active). Order/count depend on
2347
+ // whether the Triage tab is present (`tabOrder`).
2117
2348
  if (key.tab) {
2118
- const idx = TAB_ORDER.indexOf(state.activeTab);
2119
- dispatch({ type: "SET_TAB", tab: TAB_ORDER[(idx + 1) % TAB_ORDER.length] });
2120
- return;
2121
- }
2122
- if (input === "1") {
2123
- dispatch({ type: "SET_TAB", tab: "issues" });
2349
+ const idx = tabOrder.indexOf(state.activeTab);
2350
+ dispatch({ type: "SET_TAB", tab: tabOrder[(idx + 1) % tabOrder.length] });
2124
2351
  return;
2125
2352
  }
2126
- if (input === "2") {
2127
- dispatch({ type: "SET_TAB", tab: "trees" });
2128
- return;
2129
- }
2130
- if (input === "3") {
2131
- dispatch({ type: "SET_TAB", tab: "reviews" });
2132
- return;
2353
+ if (input >= "1" && input <= "9") {
2354
+ const n = Number(input) - 1;
2355
+ const tab = tabOrder[n];
2356
+ if (tab) {
2357
+ dispatch({ type: "SET_TAB", tab });
2358
+ return;
2359
+ }
2133
2360
  }
2134
2361
  // Reopen tracker selection from any tab.
2135
2362
  if (input === "t") {
@@ -2367,20 +2594,80 @@ export default function Dashboard() {
2367
2594
  const ticketId = extractTicketId(ri.branch);
2368
2595
  if (!ticketId)
2369
2596
  return;
2370
- dispatch({ type: "DELETE_START", ticketId });
2371
- const force = ri.worktree.dirty;
2372
- removeWorktree(ri.branch, repoRoot, force).then((result) => {
2373
- dispatch({ type: "DELETE_DONE" });
2374
- if (result.success) {
2375
- dispatch({ type: "SET_ACTION_MESSAGE", message: `Removed worktree` });
2376
- refresh();
2377
- }
2378
- else {
2379
- dispatch({ type: "SET_ACTION_MESSAGE", message: `Failed: ${result.error}` });
2380
- }
2597
+ void removeWorktreeWithProgress(ticketId, ri.branch, repoRoot, ri.worktree.dirty);
2598
+ return;
2599
+ }
2600
+ return; // prevent fallthrough to issues actions
2601
+ }
2602
+ // ── Triage tab keyboard ──────────────────────────────────
2603
+ // Inbox issues: navigate, read comments (detail pane), ask Claude a
2604
+ // question (`a`), or send it to a tree (`w`, same as Issues tab).
2605
+ if (state.activeTab === "triage") {
2606
+ const maxIdx = state.flatTriage.length - 1;
2607
+ if (input === "j" || (key.downArrow && !key.shift)) {
2608
+ dispatch({
2609
+ type: "TRIAGE_SELECT",
2610
+ index: Math.min(state.triageSelectedIndex + 1, maxIdx),
2611
+ });
2612
+ return;
2613
+ }
2614
+ if (input === "k" || (key.upArrow && !key.shift)) {
2615
+ dispatch({
2616
+ type: "TRIAGE_SELECT",
2617
+ index: Math.max(state.triageSelectedIndex - 1, 0),
2381
2618
  });
2382
2619
  return;
2383
2620
  }
2621
+ if (key.shift && key.downArrow) {
2622
+ dispatch({
2623
+ type: "TRIAGE_SCROLL_DETAIL",
2624
+ offset: state.triageDetailScrollOffset + 3,
2625
+ });
2626
+ return;
2627
+ }
2628
+ if (key.shift && key.upArrow) {
2629
+ dispatch({
2630
+ type: "TRIAGE_SCROLL_DETAIL",
2631
+ offset: Math.max(0, state.triageDetailScrollOffset - 3),
2632
+ });
2633
+ return;
2634
+ }
2635
+ // On-call schedule overlay — works even with an empty inbox.
2636
+ if (input === "s") {
2637
+ dispatch({ type: "TRIAGE_SCHEDULE_OPEN" });
2638
+ return;
2639
+ }
2640
+ const di = state.flatTriage[state.triageSelectedIndex];
2641
+ if (!di)
2642
+ return;
2643
+ // Send to tree — reuses the Issues-tab worktree-creation flow
2644
+ // (mode-select → context-input → createAndLaunch).
2645
+ if (input === "w") {
2646
+ if (di.worktree?.sessionId) {
2647
+ dispatch({
2648
+ type: "SET_ACTION_MESSAGE",
2649
+ message: "Session active — switch to the Trees tab to resume.",
2650
+ });
2651
+ return;
2652
+ }
2653
+ dispatch({ type: "SET_OVERLAY", overlay: "mode-select" });
2654
+ return;
2655
+ }
2656
+ // Ask Claude a clarifying question about this issue + its comments.
2657
+ if (input === "a") {
2658
+ dispatch({ type: "TRIAGE_ASK_OPEN", ticketId: di.issue.identifier });
2659
+ return;
2660
+ }
2661
+ if (input === "o") {
2662
+ if (!di.issue.url) {
2663
+ dispatch({ type: "SET_ACTION_MESSAGE", message: "No issue URL available" });
2664
+ return;
2665
+ }
2666
+ if (openUrl(di.issue.url)) {
2667
+ dispatch({ type: "SET_ACTION_MESSAGE", message: "Opened in browser" });
2668
+ }
2669
+ return;
2670
+ }
2384
2671
  return; // prevent fallthrough to issues actions
2385
2672
  }
2386
2673
  // Issues tab = backlog/planning; Trees tab = worktrees in
@@ -2700,11 +2987,18 @@ export default function Dashboard() {
2700
2987
  dispatch({ type: "SET_ACTION_MESSAGE", message: "No worktree to remove" });
2701
2988
  return;
2702
2989
  }
2990
+ // Already mid-removal — don't queue a duplicate.
2991
+ if (state.deletingTickets[di.issue.identifier]?.phase === "removing") {
2992
+ dispatch({ type: "SET_ACTION_MESSAGE", message: "Already removing this worktree" });
2993
+ return;
2994
+ }
2703
2995
  dispatch({ type: "SET_OVERLAY", overlay: "confirm-delete" });
2704
2996
  return;
2705
2997
  }
2706
2998
  }, {
2707
2999
  isActive: state.overlay !== "context-input" &&
3000
+ // Triage ask: the input phase is owned by MultilineTextArea.
3001
+ (state.overlay !== "triage-ask" || state.triageAskPhase !== "input") &&
2708
3002
  // Issue form title/description are owned by MultilineTextArea;
2709
3003
  // only the "saving" phase needs the outer handler (a no-op
2710
3004
  // swallow), so disabling it for the whole overlay is fine.
@@ -2722,10 +3016,47 @@ export default function Dashboard() {
2722
3016
  // The active issue/tree row drives the detail pane and action row.
2723
3017
  const selectedIssue = (state.activeTab === "trees"
2724
3018
  ? state.flatTrees[state.treeSelectedIndex]
2725
- : state.flatIssues[state.selectedIndex]) ?? null;
3019
+ : state.activeTab === "triage"
3020
+ ? state.flatTriage[state.triageSelectedIndex]
3021
+ : state.flatIssues[state.selectedIndex]) ?? null;
2726
3022
  const selectedReview = state.flatReviews[state.reviewSelectedIndex] ?? null;
2727
3023
  const activeTracker = getIssueTracker(repoRootRef.current);
2728
- return (_jsxs(Box, { width: columns, height: rows, flexDirection: "column", children: [_jsxs(Box, { paddingX: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "santree" }), _jsxs(Text, { dimColor: true, children: [" ", "v", version] }), updateAvailable && latestVersion ? (_jsxs(Text, { color: "yellow", children: [" ⬆ v", latestVersion, " available — `santree update`"] })) : null, CLAUDE_VERSION ? (_jsxs(Text, { dimColor: true, children: [" · claude ", CLAUDE_VERSION] })) : null, claudeUpdateAvailable && latestClaudeVersion ? (_jsxs(Text, { color: "yellow", children: [" ⬆ ", latestClaudeVersion] })) : null, state.refreshing ? (_jsxs(Text, { dimColor: true, children: [" · ", _jsx(Spinner, { type: "dots" }), " refreshing"] })) : null, state.actionMessage ? (_jsxs(Text, { color: "yellow", children: [" · ", state.actionMessage] })) : null] }), _jsxs(Box, { paddingX: 1, children: [_jsx(Tab, { active: state.activeTab === "issues", label: `1 Issues (${state.flatIssues.length})`, mode: theme.mode }), _jsx(Text, { children: " " }), _jsx(Tab, { active: state.activeTab === "trees", label: `2 Trees (${state.flatTrees.length})`, mode: theme.mode }), _jsx(Text, { children: " " }), _jsx(Tab, { active: state.activeTab === "reviews", label: `3 Reviews (${state.flatReviews.length})`, mode: theme.mode })] }), _jsxs(Box, { flexGrow: 1, borderStyle: "round", borderColor: "cyan", flexDirection: "column", children: [state.overlay === "help" ? (_jsx(HelpOverlay, { width: innerWidth, height: contentHeight })) : state.overlay === "tracker-select" ? (_jsx(Box, { flexGrow: 1, justifyContent: "center", alignItems: "center", children: _jsx(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 3, paddingY: 1, children: state.trackerSelectPhase === "linear-org" ? (_jsxs(_Fragment, { children: [_jsx(Text, { bold: true, children: "Select a Linear workspace:" }), _jsx(Text, { children: " " }), state.trackerSelectOrgs.map((org, i) => {
3024
+ // Comments for the selected triage issue (lazily loaded; undefined = still
3025
+ // fetching, which the detail panel renders as "loading…").
3026
+ const triageComments = state.activeTab === "triage" && selectedIssue
3027
+ ? state.triageCommentsById[selectedIssue.issue.identifier]
3028
+ : undefined;
3029
+ // Compact on-call summary for the triage detail pane. Uses the first
3030
+ // schedule (sorted to put the viewer's own rotation first) and finds the
3031
+ // viewer's next upcoming shift.
3032
+ let onCall;
3033
+ if (state.activeTab === "triage" && state.triageSchedules.length > 0) {
3034
+ const sched = state.triageSchedules[0];
3035
+ const now = Date.now();
3036
+ const next = sched.shifts.find((s) => s.isMe && Date.parse(s.startsAt) > now);
3037
+ onCall = {
3038
+ currentName: sched.currentName,
3039
+ currentIsMe: sched.currentIsMe,
3040
+ myNext: next
3041
+ ? new Date(next.startsAt).toLocaleDateString("en-US", { month: "short", day: "numeric" })
3042
+ : null,
3043
+ };
3044
+ }
3045
+ // Worktree deletions in flight — `deletingIds` marks list rows, the selected
3046
+ // one's status drives the detail-pane progress view.
3047
+ const deletingIds = new Set(Object.keys(state.deletingTickets));
3048
+ const selectedDeleteStatus = selectedIssue
3049
+ ? state.deletingTickets[selectedIssue.issue.identifier]
3050
+ : undefined;
3051
+ return (_jsxs(Box, { width: columns, height: rows, flexDirection: "column", children: [_jsxs(Box, { paddingX: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "santree" }), _jsxs(Text, { dimColor: true, children: [" ", "v", version] }), updateAvailable && latestVersion ? (_jsxs(Text, { color: "yellow", children: [" ⬆ v", latestVersion, " available — `santree update`"] })) : null, CLAUDE_VERSION ? (_jsxs(Text, { dimColor: true, children: [" · claude ", CLAUDE_VERSION] })) : null, claudeUpdateAvailable && latestClaudeVersion ? (_jsxs(Text, { color: "yellow", children: [" ⬆ ", latestClaudeVersion] })) : null, state.refreshing ? (_jsxs(Text, { dimColor: true, children: [" · ", _jsx(Spinner, { type: "dots" }), " refreshing"] })) : null, state.actionMessage ? (_jsxs(Text, { color: "yellow", children: [" · ", state.actionMessage] })) : null] }), _jsx(Box, { paddingX: 1, children: tabOrder.map((tab, i) => {
3052
+ const meta = {
3053
+ triage: { name: "Triage", count: state.flatTriage.length },
3054
+ issues: { name: "Issues", count: state.flatIssues.length },
3055
+ trees: { name: "Trees", count: state.flatTrees.length },
3056
+ reviews: { name: "Reviews", count: state.flatReviews.length },
3057
+ }[tab];
3058
+ return (_jsxs(Box, { children: [i > 0 ? _jsx(Text, { children: " " }) : null, _jsx(Tab, { active: state.activeTab === tab, label: `${i + 1} ${meta.name} (${meta.count})`, mode: theme.mode })] }, tab));
3059
+ }) }), _jsxs(Box, { flexGrow: 1, borderStyle: "round", borderColor: "cyan", flexDirection: "column", children: [state.overlay === "help" ? (_jsx(HelpOverlay, { width: innerWidth, height: contentHeight })) : state.overlay === "tracker-select" ? (_jsx(Box, { flexGrow: 1, justifyContent: "center", alignItems: "center", children: _jsx(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 3, paddingY: 1, children: state.trackerSelectPhase === "linear-org" ? (_jsxs(_Fragment, { children: [_jsx(Text, { bold: true, children: "Select a Linear workspace:" }), _jsx(Text, { children: " " }), state.trackerSelectOrgs.map((org, i) => {
2729
3060
  const sel = i === state.trackerSelectIndex;
2730
3061
  return (_jsxs(Text, { color: sel ? "cyan" : undefined, bold: sel, children: [sel ? "> " : " ", org.name, " (", org.slug, ")"] }, org.slug));
2731
3062
  }), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: "j/k to navigate, Enter to link, ESC to go back" })] })) : (_jsxs(_Fragment, { children: [_jsx(Text, { bold: true, children: "Select an issue tracker for this repo:" }), _jsx(Text, { children: " " }), [
@@ -2752,15 +3083,32 @@ export default function Dashboard() {
2752
3083
  const defaultBranch = getDefaultBranch();
2753
3084
  const label = branch === defaultBranch ? `${branch} (default)` : branch;
2754
3085
  return (_jsx(Text, { children: _jsxs(Text, { color: selected ? "cyan" : undefined, bold: selected, children: [selected ? "> " : " ", label] }) }, branch));
2755
- }), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: "j/k to navigate, Enter to select, ESC to cancel" })] }) })) : state.overlay === "confirm-delete" ? (_jsx(Box, { flexGrow: 1, justifyContent: "center", alignItems: "center", children: _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "red", paddingX: 3, paddingY: 1, children: [_jsx(Text, { bold: true, color: "red", children: "Remove worktree?" }), _jsx(Text, { children: " " }), _jsx(Text, { children: selectedIssue?.worktree?.branch ?? "" }), selectedIssue?.worktree?.dirty && (_jsx(Text, { color: "yellow", children: "Warning: worktree has uncommitted changes" })), _jsx(Text, { children: " " }), _jsxs(Text, { children: [_jsx(Text, { color: "red", bold: true, children: "y" }), " Confirm"] }), _jsxs(Text, { children: [_jsx(Text, { color: "cyan", bold: true, children: "n" }), " Cancel"] })] }) })) : state.overlay === "diff" ? (_jsx(DiffOverlay, { width: innerWidth, height: contentHeight, ticketId: state.diffTicketId ?? "", baseBranch: state.diffBaseBranch ?? "", files: state.diffFiles, fileIndex: state.diffFileIndex, fileScrollOffset: state.diffFileScrollOffset, content: state.diffContent, contentScrollOffset: state.diffContentScrollOffset, loadingFiles: state.diffLoadingFiles, loadingContent: state.diffLoadingContent, error: state.diffError, selectionBg: theme.selectionBg, leftWidthOverride: diffLeftWidth ?? undefined, pendingDiscard: state.diffPendingDiscard })) : state.overlay === "confirm-setup" ? (_jsx(Box, { flexGrow: 1, justifyContent: "center", alignItems: "center", children: _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 3, paddingY: 1, children: [_jsx(Text, { bold: true, children: "Run setup script?" }), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: ".santree/init.sh" }), _jsx(Text, { children: " " }), _jsxs(Text, { children: [_jsx(Text, { color: "green", bold: true, children: "y" }), " Run setup"] }), _jsxs(Text, { children: [_jsx(Text, { color: "yellow", bold: true, children: "n" }), " Skip"] })] }) })) : (_jsxs(Box, { flexGrow: 1, children: [_jsx(Box, { width: leftWidth, children: state.activeTab === "reviews" ? (_jsx(ReviewList, { flatReviews: state.flatReviews, selectedIndex: state.reviewSelectedIndex, scrollOffset: state.reviewListScrollOffset, height: contentHeight, width: leftWidth, selectionBg: theme.selectionBg })) : (state.activeTab === "trees" ? state.flatTrees : state.flatIssues).length ===
3086
+ }), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: "j/k to navigate, Enter to select, ESC to cancel" })] }) })) : state.overlay === "confirm-delete" ? (_jsx(Box, { flexGrow: 1, justifyContent: "center", alignItems: "center", children: _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "red", paddingX: 3, paddingY: 1, children: [_jsx(Text, { bold: true, color: "red", children: "Remove worktree?" }), _jsx(Text, { children: " " }), _jsx(Text, { children: selectedIssue?.worktree?.branch ?? "" }), selectedIssue?.worktree?.dirty && (_jsx(Text, { color: "yellow", children: "Warning: worktree has uncommitted changes" })), _jsx(Text, { children: " " }), _jsxs(Text, { children: [_jsx(Text, { color: "red", bold: true, children: "y" }), " Confirm"] }), _jsxs(Text, { children: [_jsx(Text, { color: "cyan", bold: true, children: "n" }), " Cancel"] })] }) })) : state.overlay === "diff" ? (_jsx(DiffOverlay, { width: innerWidth, height: contentHeight, ticketId: state.diffTicketId ?? "", baseBranch: state.diffBaseBranch ?? "", files: state.diffFiles, fileIndex: state.diffFileIndex, fileScrollOffset: state.diffFileScrollOffset, content: state.diffContent, contentScrollOffset: state.diffContentScrollOffset, loadingFiles: state.diffLoadingFiles, loadingContent: state.diffLoadingContent, error: state.diffError, selectionBg: theme.selectionBg, leftWidthOverride: diffLeftWidth ?? undefined, pendingDiscard: state.diffPendingDiscard })) : state.overlay === "confirm-setup" ? (_jsx(Box, { flexGrow: 1, justifyContent: "center", alignItems: "center", children: _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 3, paddingY: 1, children: [_jsx(Text, { bold: true, children: "Run setup script?" }), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: ".santree/init.sh" }), _jsx(Text, { children: " " }), _jsxs(Text, { children: [_jsx(Text, { color: "green", bold: true, children: "y" }), " Run setup"] }), _jsxs(Text, { children: [_jsx(Text, { color: "yellow", bold: true, children: "n" }), " Skip"] })] }) })) : state.overlay === "triage-schedule" ? (_jsx(TriageScheduleOverlay, { schedules: state.triageSchedules, scrollOffset: state.triageScheduleScrollOffset, width: innerWidth, height: contentHeight })) : state.overlay === "triage-ask" ? (_jsxs(Box, { flexDirection: "column", width: innerWidth, height: contentHeight, paddingX: 1, children: [_jsxs(Text, { bold: true, color: "cyan", children: ["Ask about ", state.triageAskTicketId] }), state.triageAskPhase === "input" ? (_jsxs(_Fragment, { children: [_jsx(Text, { dimColor: true, children: "Claude answers from the issue + all its comments (read-only)." }), _jsx(Text, { children: " " }), _jsx(MultilineTextArea, { value: state.triageAskQuestion, onChange: (v) => dispatch({ type: "TRIAGE_ASK_CHANGE", value: v }), onSubmit: () => {
3087
+ const q = state.triageAskQuestion.trim();
3088
+ if (q)
3089
+ void doAsk(q);
3090
+ }, onCancel: () => dispatch({ type: "TRIAGE_ASK_CLOSE" }), width: innerWidth - 2, height: 6, placeholder: "e.g. Is this a real bug? Where would the fix go?" }), _jsx(Text, { children: " " }), _jsxs(Text, { dimColor: true, children: [_jsx(Text, { color: "cyan", bold: true, children: "Ctrl+D" }), " ask · ", _jsx(Text, { color: "cyan", bold: true, children: "Ctrl+G" }), " cancel"] })] })) : state.triageAskPhase === "running" ? (_jsx(Box, { flexGrow: 1, alignItems: "center", justifyContent: "center", children: _jsxs(Text, { color: "cyan", children: [_jsx(Spinner, { type: "dots" }), " Asking Claude\u2026"] }) })) : state.triageAskPhase === "error" ? (_jsxs(_Fragment, { children: [_jsx(Text, { children: " " }), _jsx(Text, { color: "red", children: state.triageAskError }), _jsx(Text, { children: " " }), _jsxs(Text, { dimColor: true, children: [_jsx(Text, { color: "cyan", bold: true, children: "a" }), " ask again · ", _jsx(Text, { color: "cyan", bold: true, children: "q" }), " close"] })] })) : (_jsxs(_Fragment, { children: [state.triageAskQuestion ? (_jsx(Text, { dimColor: true, children: `Q: ${state.triageAskQuestion.replace(/\n+/g, " ")}`.slice(0, innerWidth - 2) })) : null, _jsx(Text, { children: " " }), (() => {
3091
+ const allLines = (state.triageAskAnswer ?? "").split("\n");
3092
+ const bodyHeight = Math.max(1, contentHeight - 5);
3093
+ const off = Math.min(state.triageAskScrollOffset, Math.max(0, allLines.length - bodyHeight));
3094
+ return allLines
3095
+ .slice(off, off + bodyHeight)
3096
+ .map((ln, i) => (_jsx(Text, { children: ln.length > innerWidth - 2 ? ln.slice(0, innerWidth - 3) + "…" : ln || " " }, i)));
3097
+ })(), _jsx(Text, { children: " " }), _jsxs(Text, { dimColor: true, children: [_jsx(Text, { color: "cyan", bold: true, children: "\u21E7\u2191\u2193/j/k" }), " scroll · ", _jsx(Text, { color: "cyan", bold: true, children: "a" }), " ask again · ", _jsx(Text, { color: "cyan", bold: true, children: "q" }), " close"] })] }))] })) : (_jsxs(Box, { flexGrow: 1, children: [_jsx(Box, { width: leftWidth, children: state.activeTab === "reviews" ? (_jsx(ReviewList, { flatReviews: state.flatReviews, selectedIndex: state.reviewSelectedIndex, scrollOffset: state.reviewListScrollOffset, height: contentHeight, width: leftWidth, selectionBg: theme.selectionBg })) : state.activeTab === "triage" ? (state.flatTriage.length === 0 ? (_jsx(Box, { width: leftWidth, height: contentHeight, justifyContent: "center", alignItems: "center", children: _jsx(Text, { color: "yellow", children: "Triage inbox empty" }) })) : (_jsx(IssueList, { groups: state.triageGroups, flatIssues: state.flatTriage, selectedIndex: state.triageSelectedIndex, scrollOffset: state.triageListScrollOffset, height: contentHeight, width: leftWidth, selectionBg: theme.selectionBg, variant: "triage" }))) : (state.activeTab === "trees" ? state.flatTrees : state.flatIssues).length ===
2756
3098
  0 ? (_jsxs(Box, { width: leftWidth, height: contentHeight, justifyContent: "center", alignItems: "center", flexDirection: "column", children: [_jsx(Text, { color: "yellow", children: state.activeTab === "trees" ? "No worktrees yet" : "No active issues" }), state.activeTab === "issues" && activeTracker.canMutate ? (_jsx(Text, { dimColor: true, children: "Press n to create one" })) : null] })) : (_jsx(IssueList, { groups: state.activeTab === "trees" ? state.treeGroups : state.groups, flatIssues: state.activeTab === "trees" ? state.flatTrees : state.flatIssues, selectedIndex: state.activeTab === "trees" ? state.treeSelectedIndex : state.selectedIndex, scrollOffset: state.activeTab === "trees"
2757
3099
  ? state.treeListScrollOffset
2758
- : state.listScrollOffset, height: contentHeight, width: leftWidth, selectionBg: theme.selectionBg })) }), _jsx(Box, { flexDirection: "column", width: 3, children: Array.from({ length: contentHeight }).map((_, i) => (_jsx(Text, { dimColor: true, children: " │ " }, i))) }), _jsx(Box, { width: rightWidth, children: state.activeTab === "reviews" && state.creatingForTicket ? (_jsxs(Box, { flexDirection: "column", width: rightWidth, height: contentHeight, children: [_jsxs(Text, { color: "yellow", bold: true, children: ["Setting up worktree for ", state.creatingForTicket, "..."] }), state.creationLogs
3100
+ : state.listScrollOffset, height: contentHeight, width: leftWidth, selectionBg: theme.selectionBg, deletingIds: state.activeTab === "trees" ? deletingIds : undefined, variant: state.activeTab === "issues" ? "issues" : "default" })) }), _jsx(Box, { flexDirection: "column", width: 3, children: Array.from({ length: contentHeight }).map((_, i) => (_jsx(Text, { dimColor: true, children: " │ " }, i))) }), _jsx(Box, { width: rightWidth, children: state.activeTab === "reviews" && state.creatingForTicket ? (_jsxs(Box, { flexDirection: "column", width: rightWidth, height: contentHeight, children: [_jsxs(Text, { color: "yellow", bold: true, children: ["Setting up worktree for ", state.creatingForTicket, "..."] }), state.creationLogs
2759
3101
  .split("\n")
2760
3102
  .slice(-(contentHeight - 1))
2761
- .map((line, i) => (_jsx(Box, { children: _jsx(Text, { dimColor: true, children: line }) }, i)))] })) : state.activeTab === "reviews" ? (_jsx(ReviewDetailPanel, { item: selectedReview, scrollOffset: state.reviewDetailScrollOffset, height: contentHeight, width: rightWidth })) : state.overlay === "commit" ? (_jsx(CommitOverlay, { width: rightWidth, height: contentHeight, branch: state.commitBranch, ticketId: state.commitTicketId, gitStatus: state.commitGitStatus, phase: state.commitPhase, message: state.commitMessage, error: state.commitError, dispatch: dispatch, onSubmit: handleCommitSubmit })) : state.overlay === "pr-create" ? (_jsx(PrCreateOverlay, { width: rightWidth, height: contentHeight, branch: state.prCreateBranch, ticketId: state.prCreateTicketId, phase: state.prCreatePhase, error: state.prCreateError, url: state.prCreateUrl, body: state.prCreateBody, title: state.prCreateTitle, draft: state.prCreateDraft, dispatch: dispatch })) : (_jsx(DetailPanel, { issue: selectedIssue, scrollOffset: state.activeTab === "trees"
3103
+ .map((line, i) => (_jsx(Box, { children: _jsx(Text, { dimColor: true, children: line }) }, i)))] })) : state.activeTab === "reviews" ? (_jsx(ReviewDetailPanel, { item: selectedReview, scrollOffset: state.reviewDetailScrollOffset, height: contentHeight, width: rightWidth })) : state.overlay === "commit" ? (_jsx(CommitOverlay, { width: rightWidth, height: contentHeight, branch: state.commitBranch, ticketId: state.commitTicketId, gitStatus: state.commitGitStatus, phase: state.commitPhase, message: state.commitMessage, error: state.commitError, dispatch: dispatch, onSubmit: handleCommitSubmit })) : state.overlay === "pr-create" ? (_jsx(PrCreateOverlay, { width: rightWidth, height: contentHeight, branch: state.prCreateBranch, ticketId: state.prCreateTicketId, phase: state.prCreatePhase, error: state.prCreateError, url: state.prCreateUrl, body: state.prCreateBody, title: state.prCreateTitle, draft: state.prCreateDraft, dispatch: dispatch })) : state.activeTab === "triage" && !selectedIssue ? (
3104
+ // Empty (or nothing-selected) triage inbox: surface the
3105
+ // on-call rotation in the otherwise-idle right pane so it's
3106
+ // visible without selecting an issue or pressing [s].
3107
+ _jsx(TriageScheduleOverlay, { schedules: state.triageSchedules, scrollOffset: state.triageScheduleScrollOffset, width: rightWidth, height: contentHeight })) : (_jsx(DetailPanel, { issue: selectedIssue, scrollOffset: state.activeTab === "trees"
2762
3108
  ? state.treeDetailScrollOffset
2763
- : state.detailScrollOffset, height: contentHeight, width: rightWidth, creatingForTicket: state.creatingForTicket, creationLogs: state.creationLogs })) })] })), _jsx(Box, { children: state.overlay === "diff" ? (_jsx(Box, { width: innerWidth, paddingX: 1, children: _jsx(CommandBar, { showWorkspace: hasWorkspaceFile, mode: "diff" }) })) : (_jsxs(_Fragment, { children: [_jsx(Box, { width: leftWidth + separatorWidth, paddingX: 1, children: _jsx(CommandBar, { showWorkspace: hasWorkspaceFile, mode: "default" }) }), _jsx(Box, { width: rightWidth, children: _jsx(ActionRow, { activeTab: state.activeTab, selectedIssue: selectedIssue, selectedReview: selectedReview, overlay: state.overlay, trackerName: activeTracker.displayName, canMutate: activeTracker.canMutate === true }) })] })) })] })] }));
3109
+ : state.activeTab === "triage"
3110
+ ? state.triageDetailScrollOffset
3111
+ : state.detailScrollOffset, height: contentHeight, width: rightWidth, creatingForTicket: state.creatingForTicket, creationLogs: state.creationLogs, deleteStatus: selectedDeleteStatus, triage: state.activeTab === "triage", comments: triageComments, onCall: onCall })) })] })), _jsx(Box, { children: state.overlay === "diff" ? (_jsx(Box, { width: innerWidth, paddingX: 1, children: _jsx(CommandBar, { showWorkspace: hasWorkspaceFile, mode: "diff" }) })) : state.overlay === "triage-schedule" ? (_jsx(Box, { width: innerWidth, paddingX: 1, children: _jsxs(Text, { children: [_jsx(Text, { color: "cyan", bold: true, children: "j/k" }), _jsx(Text, { dimColor: true, children: " scroll" }), _jsx(Text, { dimColor: true, children: " · " }), _jsx(Text, { color: "cyan", bold: true, children: "q" }), _jsx(Text, { dimColor: true, children: " close" })] }) })) : (_jsxs(_Fragment, { children: [_jsx(Box, { width: leftWidth + separatorWidth, paddingX: 1, children: _jsx(CommandBar, { showWorkspace: hasWorkspaceFile, mode: "default" }) }), _jsx(Box, { width: rightWidth, children: _jsx(ActionRow, { activeTab: state.activeTab, selectedIssue: selectedIssue, selectedReview: selectedReview, overlay: state.overlay, trackerName: activeTracker.displayName, canMutate: activeTracker.canMutate === true }) })] })) })] })] }));
2764
3112
  }
2765
3113
  /**
2766
3114
  * Renders the per-issue action key hints (Resume / Editor / View diff / …)