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.
- package/dist/commands/dashboard.js +418 -70
- package/dist/lib/ai.d.ts +13 -0
- package/dist/lib/ai.js +16 -0
- package/dist/lib/dashboard/DetailPanel.d.ts +18 -2
- package/dist/lib/dashboard/DetailPanel.js +144 -1
- package/dist/lib/dashboard/IssueList.d.ts +9 -1
- package/dist/lib/dashboard/IssueList.js +35 -8
- package/dist/lib/dashboard/Overlays.js +20 -5
- package/dist/lib/dashboard/TriageScheduleOverlay.d.ts +16 -0
- package/dist/lib/dashboard/TriageScheduleOverlay.js +78 -0
- package/dist/lib/dashboard/data.d.ts +2 -0
- package/dist/lib/dashboard/data.js +26 -4
- package/dist/lib/dashboard/due.d.ts +24 -0
- package/dist/lib/dashboard/due.js +32 -0
- package/dist/lib/dashboard/types.d.ts +90 -5
- package/dist/lib/dashboard/types.js +129 -4
- package/dist/lib/git.d.ts +1 -1
- package/dist/lib/git.js +7 -1
- package/dist/lib/trackers/linear/api.d.ts +2 -1
- package/dist/lib/trackers/linear/api.js +137 -1
- package/dist/lib/trackers/linear/index.js +17 -1
- package/dist/lib/trackers/types.d.ts +58 -0
- package/dist/lib/trackers/types.js +5 -1
- package/package.json +1 -1
- package/prompts/ask.njk +10 -0
|
@@ -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
|
|
545
|
-
const flat =
|
|
546
|
-
const idx =
|
|
547
|
-
|
|
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:
|
|
583
|
+
dispatch({ type: selectType, index: next });
|
|
554
584
|
}
|
|
555
585
|
else {
|
|
556
586
|
const next = Math.max(0, detailOff + delta);
|
|
557
|
-
dispatch({ type:
|
|
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
|
|
632
|
-
const flat =
|
|
633
|
-
const grps =
|
|
634
|
-
const scrollOff =
|
|
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:
|
|
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
|
-
}, [
|
|
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.
|
|
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.
|
|
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
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
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
|
-
|
|
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 =
|
|
2119
|
-
dispatch({ type: "SET_TAB", tab:
|
|
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
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
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
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
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.
|
|
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
|
-
|
|
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, {
|
|
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 })) :
|
|
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.
|
|
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 / …)
|