santree 0.6.2 → 0.6.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -7
- package/dist/commands/dashboard.js +420 -46
- package/dist/commands/issue/setup.d.ts +2 -0
- package/dist/commands/issue/setup.js +108 -0
- package/dist/commands/issue/switch.d.ts +1 -0
- package/dist/commands/issue/switch.js +2 -2
- package/dist/commands/worktree/work.js +1 -1
- package/dist/lib/ai.js +4 -0
- package/dist/lib/dashboard/DetailPanel.d.ts +5 -2
- package/dist/lib/dashboard/DetailPanel.js +18 -1
- package/dist/lib/dashboard/Overlays.js +14 -0
- package/dist/lib/dashboard/data.d.ts +2 -0
- package/dist/lib/dashboard/data.js +56 -54
- package/dist/lib/dashboard/types.d.ts +77 -2
- package/dist/lib/dashboard/types.js +93 -1
- package/dist/lib/multiplexer/cmux.js +0 -15
- package/dist/lib/multiplexer/none.js +0 -3
- package/dist/lib/multiplexer/tmux.js +0 -8
- package/dist/lib/multiplexer/types.d.ts +0 -1
- package/dist/lib/session-signal.d.ts +5 -3
- package/dist/lib/session-signal.js +5 -22
- package/dist/lib/trackers/config.js +1 -1
- package/dist/lib/trackers/index.d.ts +11 -0
- package/dist/lib/trackers/index.js +26 -0
- package/dist/lib/trackers/local/frontmatter.d.ts +12 -0
- package/dist/lib/trackers/local/frontmatter.js +91 -0
- package/dist/lib/trackers/local/index.d.ts +2 -0
- package/dist/lib/trackers/local/index.js +102 -0
- package/dist/lib/trackers/local/store.d.ts +30 -0
- package/dist/lib/trackers/local/store.js +203 -0
- package/dist/lib/trackers/types.d.ts +26 -1
- package/package.json +1 -1
|
@@ -18,7 +18,10 @@ import { shellEscape } from "../lib/multiplexer/types.js";
|
|
|
18
18
|
import SquirrelLoader from "../lib/squirrel-loader.js";
|
|
19
19
|
import { getPRTemplate } from "../lib/github.js";
|
|
20
20
|
import { renderPrompt, renderDiff, renderTicket } from "../lib/prompts.js";
|
|
21
|
-
import { getIssueTracker } from "../lib/trackers/index.js";
|
|
21
|
+
import { getIssueTracker, isRepoTrackerConfigured, setRepoTracker } from "../lib/trackers/index.js";
|
|
22
|
+
import { setRepoLinearOrg } from "../lib/trackers/linear/index.js";
|
|
23
|
+
import { readLinearAuthStore } from "../lib/trackers/auth-store.js";
|
|
24
|
+
import { getAuthenticatedUser, getCurrentRepoNwo } from "../lib/trackers/github/auth.js";
|
|
22
25
|
import { openUrl } from "../lib/open-url.js";
|
|
23
26
|
import { parseUnifiedDiff } from "../lib/diff-parse.js";
|
|
24
27
|
import * as os from "os";
|
|
@@ -264,7 +267,6 @@ function ensureAltScreen() {
|
|
|
264
267
|
if (altScreenEntered)
|
|
265
268
|
return;
|
|
266
269
|
altScreenEntered = true;
|
|
267
|
-
getMultiplexer().renameWindow("", "santree");
|
|
268
270
|
process.stdout.write("\x1b[?1049h"); // Enter alternate screen buffer
|
|
269
271
|
process.stdout.write("\x1b[?25l"); // Hide cursor
|
|
270
272
|
}
|
|
@@ -387,6 +389,14 @@ export default function Dashboard() {
|
|
|
387
389
|
return;
|
|
388
390
|
}
|
|
389
391
|
repoRootRef.current = repoRoot;
|
|
392
|
+
// No tracker configured → show the selection flow instead of letting
|
|
393
|
+
// getIssueTracker silently fall back to GitHub and then fail auth on
|
|
394
|
+
// the dead-end error screen. Genuine auth/network failures of a
|
|
395
|
+
// *configured* tracker still hit the catch → error screen below.
|
|
396
|
+
if (!isRepoTrackerConfigured(repoRoot)) {
|
|
397
|
+
dispatch({ type: "TRACKER_SELECT_OPEN" });
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
390
400
|
try {
|
|
391
401
|
// Re-detect terminal theme alongside data fetch so light↔dark
|
|
392
402
|
// switches propagate within one refresh cycle (≤30s). Skip the
|
|
@@ -528,18 +538,22 @@ export default function Dashboard() {
|
|
|
528
538
|
}
|
|
529
539
|
return;
|
|
530
540
|
}
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
const
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
541
|
+
{
|
|
542
|
+
const isTreesTab = s.activeTab === "trees";
|
|
543
|
+
const flat = isTreesTab ? s.flatTrees : s.flatIssues;
|
|
544
|
+
const idx = isTreesTab ? s.treeSelectedIndex : s.selectedIndex;
|
|
545
|
+
const detailOff = isTreesTab ? s.treeDetailScrollOffset : s.detailScrollOffset;
|
|
546
|
+
if (inLeftPane) {
|
|
547
|
+
const maxIdx = flat.length - 1;
|
|
548
|
+
if (maxIdx < 0)
|
|
549
|
+
return;
|
|
550
|
+
const next = Math.max(0, Math.min(idx + delta, maxIdx));
|
|
551
|
+
dispatch({ type: isTreesTab ? "TREE_SELECT" : "SELECT", index: next });
|
|
552
|
+
}
|
|
553
|
+
else {
|
|
554
|
+
const next = Math.max(0, detailOff + delta);
|
|
555
|
+
dispatch({ type: isTreesTab ? "TREE_SCROLL_DETAIL" : "SCROLL_DETAIL", offset: next });
|
|
556
|
+
}
|
|
543
557
|
}
|
|
544
558
|
return;
|
|
545
559
|
}
|
|
@@ -611,12 +625,18 @@ export default function Dashboard() {
|
|
|
611
625
|
}
|
|
612
626
|
return;
|
|
613
627
|
}
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
628
|
+
{
|
|
629
|
+
const isTreesTab = s.activeTab === "trees";
|
|
630
|
+
const flat = isTreesTab ? s.flatTrees : s.flatIssues;
|
|
631
|
+
const grps = isTreesTab ? s.treeGroups : s.groups;
|
|
632
|
+
const scrollOff = isTreesTab ? s.treeListScrollOffset : s.listScrollOffset;
|
|
633
|
+
if (flat.length === 0)
|
|
634
|
+
return;
|
|
635
|
+
const listRow = scrollOff + contentRow;
|
|
636
|
+
const flatIdx = getFlatIndexForListRow(grps, flat, listRow);
|
|
637
|
+
if (flatIdx !== null && flatIdx >= 0 && flatIdx < flat.length) {
|
|
638
|
+
dispatch({ type: isTreesTab ? "TREE_SELECT" : "SELECT", index: flatIdx });
|
|
639
|
+
}
|
|
620
640
|
}
|
|
621
641
|
};
|
|
622
642
|
if (process.stdin.isTTY) {
|
|
@@ -663,6 +683,21 @@ export default function Dashboard() {
|
|
|
663
683
|
dispatch({ type: "SCROLL_LIST", offset });
|
|
664
684
|
}
|
|
665
685
|
}, [state.selectedIndex, state.groups, contentHeight, state.listScrollOffset]);
|
|
686
|
+
// ── Trees list scroll tracking ───────────────────────────────────
|
|
687
|
+
useEffect(() => {
|
|
688
|
+
const rowIdx = getRowIndexForFlatIndex(state.treeGroups, state.flatTrees, state.treeSelectedIndex);
|
|
689
|
+
const maxVisible = contentHeight - LIST_FOOTER_HEIGHT;
|
|
690
|
+
let offset = state.treeListScrollOffset;
|
|
691
|
+
if (rowIdx < offset) {
|
|
692
|
+
offset = Math.max(0, rowIdx - 1);
|
|
693
|
+
}
|
|
694
|
+
else if (rowIdx >= offset + maxVisible) {
|
|
695
|
+
offset = rowIdx - maxVisible + 2;
|
|
696
|
+
}
|
|
697
|
+
if (offset !== state.treeListScrollOffset) {
|
|
698
|
+
dispatch({ type: "TREE_SCROLL_LIST", offset });
|
|
699
|
+
}
|
|
700
|
+
}, [state.treeSelectedIndex, state.treeGroups, contentHeight, state.treeListScrollOffset]);
|
|
666
701
|
// ── Review list scroll tracking ──────────────────────────────────
|
|
667
702
|
useEffect(() => {
|
|
668
703
|
// Row index = 1 (column header) + flatIndex
|
|
@@ -686,6 +721,7 @@ export default function Dashboard() {
|
|
|
686
721
|
// Disable tracking while any text-input overlay is mounted; restore on exit.
|
|
687
722
|
useEffect(() => {
|
|
688
723
|
const needsMouseOff = state.overlay === "context-input" ||
|
|
724
|
+
(state.overlay === "issue-form" && state.issueFormPhase !== "saving") ||
|
|
689
725
|
(state.overlay === "pr-create" && state.prCreatePhase === "review") ||
|
|
690
726
|
(state.overlay === "commit" && state.commitPhase === "awaiting-message");
|
|
691
727
|
if (!needsMouseOff)
|
|
@@ -694,7 +730,7 @@ export default function Dashboard() {
|
|
|
694
730
|
return () => {
|
|
695
731
|
process.stdout.write("\x1b[?1002h\x1b[?1006h");
|
|
696
732
|
};
|
|
697
|
-
}, [state.overlay, state.prCreatePhase, state.commitPhase]);
|
|
733
|
+
}, [state.overlay, state.issueFormPhase, state.prCreatePhase, state.commitPhase]);
|
|
698
734
|
// ── Diff overlay: load file list when opened (gh pr diff path) ────
|
|
699
735
|
// Reviews-tab PRs without a local worktree shell out to `gh pr diff <n>`,
|
|
700
736
|
// parse the unified blob into per-file records, and stash the per-file
|
|
@@ -982,7 +1018,10 @@ export default function Dashboard() {
|
|
|
982
1018
|
}
|
|
983
1019
|
}, []);
|
|
984
1020
|
const createAndLaunch = useCallback(async (mode, runSetup, base, contextFile) => {
|
|
985
|
-
const
|
|
1021
|
+
const sref = stateRef.current;
|
|
1022
|
+
const di = sref.activeTab === "trees"
|
|
1023
|
+
? sref.flatTrees[sref.treeSelectedIndex]
|
|
1024
|
+
: sref.flatIssues[sref.selectedIndex];
|
|
986
1025
|
if (!di)
|
|
987
1026
|
return;
|
|
988
1027
|
const repoRoot = repoRootRef.current;
|
|
@@ -1091,7 +1130,9 @@ export default function Dashboard() {
|
|
|
1091
1130
|
createAndLaunch(mode, false, base, contextFile);
|
|
1092
1131
|
}, [createAndLaunch]);
|
|
1093
1132
|
const doWork = useCallback((mode, customContext) => {
|
|
1094
|
-
const di = state.
|
|
1133
|
+
const di = state.activeTab === "trees"
|
|
1134
|
+
? state.flatTrees[state.treeSelectedIndex]
|
|
1135
|
+
: state.flatIssues[state.selectedIndex];
|
|
1095
1136
|
if (!di)
|
|
1096
1137
|
return;
|
|
1097
1138
|
const repoRoot = repoRootRef.current;
|
|
@@ -1118,7 +1159,9 @@ export default function Dashboard() {
|
|
|
1118
1159
|
pendingContextFileRef.current = contextFile ?? null;
|
|
1119
1160
|
const defaultBranch = getDefaultBranch();
|
|
1120
1161
|
const baseOptions = [defaultBranch];
|
|
1121
|
-
|
|
1162
|
+
// Worktree branches live on the Trees tab now; scan there for
|
|
1163
|
+
// candidate base branches (stacked work).
|
|
1164
|
+
for (const fi of [...state.flatTrees, ...state.flatIssues]) {
|
|
1122
1165
|
if (fi.worktree && !baseOptions.includes(fi.worktree.branch)) {
|
|
1123
1166
|
baseOptions.push(fi.worktree.branch);
|
|
1124
1167
|
}
|
|
@@ -1136,11 +1179,153 @@ export default function Dashboard() {
|
|
|
1136
1179
|
}, [
|
|
1137
1180
|
state.flatIssues,
|
|
1138
1181
|
state.selectedIndex,
|
|
1182
|
+
state.flatTrees,
|
|
1183
|
+
state.treeSelectedIndex,
|
|
1184
|
+
state.activeTab,
|
|
1139
1185
|
exit,
|
|
1140
1186
|
launchWorkInTmux,
|
|
1141
1187
|
proceedAfterBaseSelect,
|
|
1142
1188
|
writeContextFile,
|
|
1143
1189
|
]);
|
|
1190
|
+
// ── Tracker selection ────────────────────────────────────────────
|
|
1191
|
+
// Mirrors `santree issue setup`. Local needs no account; Linear picks an
|
|
1192
|
+
// authenticated workspace (sub-list when >1); GitHub verifies `gh`.
|
|
1193
|
+
const chooseTracker = useCallback(async (kind) => {
|
|
1194
|
+
const root = repoRootRef.current ?? findMainRepoRoot();
|
|
1195
|
+
if (!root) {
|
|
1196
|
+
dispatch({ type: "SET_ERROR", error: "Not inside a git repository" });
|
|
1197
|
+
return;
|
|
1198
|
+
}
|
|
1199
|
+
repoRootRef.current = root;
|
|
1200
|
+
if (kind === "local") {
|
|
1201
|
+
setRepoTracker(root, "local");
|
|
1202
|
+
dispatch({ type: "TRACKER_SELECT_CLOSE" });
|
|
1203
|
+
refresh();
|
|
1204
|
+
return;
|
|
1205
|
+
}
|
|
1206
|
+
if (kind === "linear") {
|
|
1207
|
+
const store = readLinearAuthStore();
|
|
1208
|
+
const orgs = Object.entries(store).map(([slug, tokens]) => ({
|
|
1209
|
+
slug,
|
|
1210
|
+
name: tokens.org_name,
|
|
1211
|
+
}));
|
|
1212
|
+
if (orgs.length === 0) {
|
|
1213
|
+
dispatch({
|
|
1214
|
+
type: "TRACKER_SELECT_MESSAGE",
|
|
1215
|
+
message: "No authenticated Linear workspaces. Run: santree linear auth",
|
|
1216
|
+
});
|
|
1217
|
+
return;
|
|
1218
|
+
}
|
|
1219
|
+
if (orgs.length === 1) {
|
|
1220
|
+
setRepoLinearOrg(root, orgs[0].slug);
|
|
1221
|
+
setRepoTracker(root, "linear");
|
|
1222
|
+
dispatch({ type: "TRACKER_SELECT_CLOSE" });
|
|
1223
|
+
refresh();
|
|
1224
|
+
return;
|
|
1225
|
+
}
|
|
1226
|
+
dispatch({ type: "TRACKER_SELECT_PHASE", phase: "linear-org", orgs });
|
|
1227
|
+
return;
|
|
1228
|
+
}
|
|
1229
|
+
// github
|
|
1230
|
+
const user = await getAuthenticatedUser();
|
|
1231
|
+
if (!user) {
|
|
1232
|
+
dispatch({
|
|
1233
|
+
type: "TRACKER_SELECT_MESSAGE",
|
|
1234
|
+
message: "GitHub CLI not authenticated. Run: santree github auth",
|
|
1235
|
+
});
|
|
1236
|
+
return;
|
|
1237
|
+
}
|
|
1238
|
+
setRepoTracker(root, "github");
|
|
1239
|
+
await getCurrentRepoNwo(root);
|
|
1240
|
+
dispatch({ type: "TRACKER_SELECT_CLOSE" });
|
|
1241
|
+
refresh();
|
|
1242
|
+
}, [refresh]);
|
|
1243
|
+
const chooseLinearOrg = useCallback((slug) => {
|
|
1244
|
+
const root = repoRootRef.current ?? findMainRepoRoot();
|
|
1245
|
+
if (!root)
|
|
1246
|
+
return;
|
|
1247
|
+
setRepoLinearOrg(root, slug);
|
|
1248
|
+
setRepoTracker(root, "linear");
|
|
1249
|
+
dispatch({ type: "TRACKER_SELECT_CLOSE" });
|
|
1250
|
+
refresh();
|
|
1251
|
+
}, [refresh]);
|
|
1252
|
+
// ── Issue CRUD (built-in tracker only) ───────────────────────────
|
|
1253
|
+
const submitIssueForm = useCallback(async () => {
|
|
1254
|
+
const s = stateRef.current;
|
|
1255
|
+
const root = repoRootRef.current;
|
|
1256
|
+
if (!root)
|
|
1257
|
+
return;
|
|
1258
|
+
const tracker = getIssueTracker(root);
|
|
1259
|
+
const title = s.issueFormTitle.split("\n")[0]?.trim() ?? "";
|
|
1260
|
+
if (!title) {
|
|
1261
|
+
dispatch({ type: "ISSUE_FORM_ERROR", error: "Title is required" });
|
|
1262
|
+
return;
|
|
1263
|
+
}
|
|
1264
|
+
const description = s.issueFormDescription;
|
|
1265
|
+
dispatch({ type: "ISSUE_FORM_PHASE", phase: "saving" });
|
|
1266
|
+
try {
|
|
1267
|
+
if (s.issueFormMode === "edit" && s.issueFormId && tracker.updateIssue) {
|
|
1268
|
+
const res = await tracker.updateIssue(s.issueFormId, { title, description }, root);
|
|
1269
|
+
if (!res.ok) {
|
|
1270
|
+
dispatch({ type: "ISSUE_FORM_ERROR", error: res.message ?? "Update failed" });
|
|
1271
|
+
return;
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
else if (tracker.createIssue) {
|
|
1275
|
+
const res = await tracker.createIssue({ title, description }, root);
|
|
1276
|
+
if (!res.ok) {
|
|
1277
|
+
dispatch({ type: "ISSUE_FORM_ERROR", error: res.message ?? "Create failed" });
|
|
1278
|
+
return;
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
else {
|
|
1282
|
+
dispatch({ type: "ISSUE_FORM_ERROR", error: "Tracker does not support editing" });
|
|
1283
|
+
return;
|
|
1284
|
+
}
|
|
1285
|
+
dispatch({ type: "ISSUE_FORM_CLOSE" });
|
|
1286
|
+
dispatch({
|
|
1287
|
+
type: "SET_ACTION_MESSAGE",
|
|
1288
|
+
message: s.issueFormMode === "edit" ? "Issue updated" : "Issue created",
|
|
1289
|
+
});
|
|
1290
|
+
refresh();
|
|
1291
|
+
}
|
|
1292
|
+
catch (e) {
|
|
1293
|
+
dispatch({
|
|
1294
|
+
type: "ISSUE_FORM_ERROR",
|
|
1295
|
+
error: e instanceof Error ? e.message : "Failed to save issue",
|
|
1296
|
+
});
|
|
1297
|
+
}
|
|
1298
|
+
}, [refresh]);
|
|
1299
|
+
const deleteSelectedIssue = useCallback(async () => {
|
|
1300
|
+
const s = stateRef.current;
|
|
1301
|
+
const root = repoRootRef.current;
|
|
1302
|
+
if (!root)
|
|
1303
|
+
return;
|
|
1304
|
+
const di = s.flatIssues[s.selectedIndex];
|
|
1305
|
+
if (!di)
|
|
1306
|
+
return;
|
|
1307
|
+
const tracker = getIssueTracker(root);
|
|
1308
|
+
dispatch({ type: "ISSUE_DELETE_CLOSE" });
|
|
1309
|
+
if (!tracker.deleteIssue)
|
|
1310
|
+
return;
|
|
1311
|
+
try {
|
|
1312
|
+
const res = await tracker.deleteIssue(di.issue.identifier, root);
|
|
1313
|
+
dispatch({
|
|
1314
|
+
type: "SET_ACTION_MESSAGE",
|
|
1315
|
+
message: res.ok
|
|
1316
|
+
? `Deleted ${di.issue.identifier}`
|
|
1317
|
+
: `Failed: ${res.message ?? "delete failed"}`,
|
|
1318
|
+
});
|
|
1319
|
+
if (res.ok)
|
|
1320
|
+
refresh();
|
|
1321
|
+
}
|
|
1322
|
+
catch (e) {
|
|
1323
|
+
dispatch({
|
|
1324
|
+
type: "SET_ACTION_MESSAGE",
|
|
1325
|
+
message: e instanceof Error ? e.message : "Delete failed",
|
|
1326
|
+
});
|
|
1327
|
+
}
|
|
1328
|
+
}, [refresh]);
|
|
1144
1329
|
// ── Commit flow ──────────────────────────────────────────────────
|
|
1145
1330
|
const handleStageAll = useCallback(async () => {
|
|
1146
1331
|
const wtPath = stateRef.current.commitWorktreePath;
|
|
@@ -1782,7 +1967,8 @@ export default function Dashboard() {
|
|
|
1782
1967
|
if (state.overlay === "confirm-delete") {
|
|
1783
1968
|
if (input === "y") {
|
|
1784
1969
|
dispatch({ type: "SET_OVERLAY", overlay: null });
|
|
1785
|
-
|
|
1970
|
+
// Worktree removal is a Trees-tab action.
|
|
1971
|
+
const di = state.flatTrees[state.treeSelectedIndex];
|
|
1786
1972
|
if (di?.worktree) {
|
|
1787
1973
|
const repoRoot = repoRootRef.current;
|
|
1788
1974
|
if (repoRoot) {
|
|
@@ -1814,12 +2000,86 @@ export default function Dashboard() {
|
|
|
1814
2000
|
}
|
|
1815
2001
|
return;
|
|
1816
2002
|
}
|
|
2003
|
+
// Tracker-selection overlay (shown when no tracker is configured,
|
|
2004
|
+
// or reopened with `t`). Not a text input — outer useInput drives it.
|
|
2005
|
+
if (state.overlay === "tracker-select") {
|
|
2006
|
+
if (state.trackerSelectPhase === "linear-org") {
|
|
2007
|
+
const orgs = state.trackerSelectOrgs;
|
|
2008
|
+
if (input === "j" || key.downArrow) {
|
|
2009
|
+
dispatch({
|
|
2010
|
+
type: "TRACKER_SELECT_MOVE",
|
|
2011
|
+
index: Math.min(state.trackerSelectIndex + 1, orgs.length - 1),
|
|
2012
|
+
});
|
|
2013
|
+
return;
|
|
2014
|
+
}
|
|
2015
|
+
if (input === "k" || key.upArrow) {
|
|
2016
|
+
dispatch({
|
|
2017
|
+
type: "TRACKER_SELECT_MOVE",
|
|
2018
|
+
index: Math.max(state.trackerSelectIndex - 1, 0),
|
|
2019
|
+
});
|
|
2020
|
+
return;
|
|
2021
|
+
}
|
|
2022
|
+
if (key.return) {
|
|
2023
|
+
const org = orgs[state.trackerSelectIndex];
|
|
2024
|
+
if (org)
|
|
2025
|
+
chooseLinearOrg(org.slug);
|
|
2026
|
+
return;
|
|
2027
|
+
}
|
|
2028
|
+
if (key.escape) {
|
|
2029
|
+
dispatch({ type: "TRACKER_SELECT_PHASE", phase: "root" });
|
|
2030
|
+
return;
|
|
2031
|
+
}
|
|
2032
|
+
return;
|
|
2033
|
+
}
|
|
2034
|
+
const TRACKER_KINDS = ["local", "linear", "github"];
|
|
2035
|
+
if (input === "j" || key.downArrow) {
|
|
2036
|
+
dispatch({
|
|
2037
|
+
type: "TRACKER_SELECT_MOVE",
|
|
2038
|
+
index: Math.min(state.trackerSelectIndex + 1, TRACKER_KINDS.length - 1),
|
|
2039
|
+
});
|
|
2040
|
+
return;
|
|
2041
|
+
}
|
|
2042
|
+
if (input === "k" || key.upArrow) {
|
|
2043
|
+
dispatch({
|
|
2044
|
+
type: "TRACKER_SELECT_MOVE",
|
|
2045
|
+
index: Math.max(state.trackerSelectIndex - 1, 0),
|
|
2046
|
+
});
|
|
2047
|
+
return;
|
|
2048
|
+
}
|
|
2049
|
+
if (key.return) {
|
|
2050
|
+
void chooseTracker(TRACKER_KINDS[state.trackerSelectIndex] ?? "local");
|
|
2051
|
+
return;
|
|
2052
|
+
}
|
|
2053
|
+
if (input === "q" || key.escape) {
|
|
2054
|
+
// Can't proceed without a tracker — leave the dashboard.
|
|
2055
|
+
exit();
|
|
2056
|
+
return;
|
|
2057
|
+
}
|
|
2058
|
+
return;
|
|
2059
|
+
}
|
|
2060
|
+
// Confirm-delete-issue overlay (built-in tracker only)
|
|
2061
|
+
if (state.overlay === "confirm-delete-issue") {
|
|
2062
|
+
if (input === "y") {
|
|
2063
|
+
void deleteSelectedIssue();
|
|
2064
|
+
return;
|
|
2065
|
+
}
|
|
2066
|
+
if (input === "n" || key.escape || input === "q") {
|
|
2067
|
+
dispatch({ type: "ISSUE_DELETE_CLOSE" });
|
|
2068
|
+
return;
|
|
2069
|
+
}
|
|
2070
|
+
return;
|
|
2071
|
+
}
|
|
2072
|
+
// Issue create/edit form. Title/description phases are owned by
|
|
2073
|
+
// MultilineTextArea (outer useInput disabled via isActive); only
|
|
2074
|
+
// the "saving" phase reaches here — swallow all keys.
|
|
2075
|
+
if (state.overlay === "issue-form") {
|
|
2076
|
+
return;
|
|
2077
|
+
}
|
|
1817
2078
|
// Tab switching (only when no overlay is active)
|
|
2079
|
+
const TAB_ORDER = ["issues", "trees", "reviews"];
|
|
1818
2080
|
if (key.tab) {
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
tab: state.activeTab === "issues" ? "reviews" : "issues",
|
|
1822
|
-
});
|
|
2081
|
+
const idx = TAB_ORDER.indexOf(state.activeTab);
|
|
2082
|
+
dispatch({ type: "SET_TAB", tab: TAB_ORDER[(idx + 1) % TAB_ORDER.length] });
|
|
1823
2083
|
return;
|
|
1824
2084
|
}
|
|
1825
2085
|
if (input === "1") {
|
|
@@ -1827,9 +2087,18 @@ export default function Dashboard() {
|
|
|
1827
2087
|
return;
|
|
1828
2088
|
}
|
|
1829
2089
|
if (input === "2") {
|
|
2090
|
+
dispatch({ type: "SET_TAB", tab: "trees" });
|
|
2091
|
+
return;
|
|
2092
|
+
}
|
|
2093
|
+
if (input === "3") {
|
|
1830
2094
|
dispatch({ type: "SET_TAB", tab: "reviews" });
|
|
1831
2095
|
return;
|
|
1832
2096
|
}
|
|
2097
|
+
// Reopen tracker selection from any tab.
|
|
2098
|
+
if (input === "t") {
|
|
2099
|
+
dispatch({ type: "TRACKER_SELECT_OPEN" });
|
|
2100
|
+
return;
|
|
2101
|
+
}
|
|
1833
2102
|
// Quit
|
|
1834
2103
|
if (input === "q") {
|
|
1835
2104
|
exit();
|
|
@@ -2077,33 +2346,109 @@ export default function Dashboard() {
|
|
|
2077
2346
|
}
|
|
2078
2347
|
return; // prevent fallthrough to issues actions
|
|
2079
2348
|
}
|
|
2080
|
-
|
|
2349
|
+
// Issues tab = backlog/planning; Trees tab = worktrees in
|
|
2350
|
+
// progress. Both reuse the same list/detail UI and worktree action
|
|
2351
|
+
// handlers below — only the backing data + selection slice differ.
|
|
2352
|
+
const isTrees = state.activeTab === "trees";
|
|
2353
|
+
const viewFlat = isTrees ? state.flatTrees : state.flatIssues;
|
|
2354
|
+
const viewIndex = isTrees ? state.treeSelectedIndex : state.selectedIndex;
|
|
2355
|
+
const viewDetailScroll = isTrees ? state.treeDetailScrollOffset : state.detailScrollOffset;
|
|
2356
|
+
const selectAction = isTrees ? "TREE_SELECT" : "SELECT";
|
|
2357
|
+
const scrollDetailAction = isTrees
|
|
2358
|
+
? "TREE_SCROLL_DETAIL"
|
|
2359
|
+
: "SCROLL_DETAIL";
|
|
2360
|
+
const maxIndex = viewFlat.length - 1;
|
|
2081
2361
|
// Navigation
|
|
2082
2362
|
if (input === "j" || (key.downArrow && !key.shift)) {
|
|
2083
|
-
|
|
2084
|
-
dispatch({ type: "SELECT", index: next });
|
|
2363
|
+
dispatch({ type: selectAction, index: Math.min(viewIndex + 1, maxIndex) });
|
|
2085
2364
|
return;
|
|
2086
2365
|
}
|
|
2087
2366
|
if (input === "k" || (key.upArrow && !key.shift)) {
|
|
2088
|
-
|
|
2089
|
-
dispatch({ type: "SELECT", index: prev });
|
|
2367
|
+
dispatch({ type: selectAction, index: Math.max(viewIndex - 1, 0) });
|
|
2090
2368
|
return;
|
|
2091
2369
|
}
|
|
2092
2370
|
// Detail scroll
|
|
2093
2371
|
if (key.shift && key.downArrow) {
|
|
2094
|
-
dispatch({ type:
|
|
2372
|
+
dispatch({ type: scrollDetailAction, offset: viewDetailScroll + 3 });
|
|
2095
2373
|
return;
|
|
2096
2374
|
}
|
|
2097
2375
|
if (key.shift && key.upArrow) {
|
|
2098
|
-
dispatch({
|
|
2099
|
-
type: "SCROLL_DETAIL",
|
|
2100
|
-
offset: Math.max(0, state.detailScrollOffset - 3),
|
|
2101
|
-
});
|
|
2376
|
+
dispatch({ type: scrollDetailAction, offset: Math.max(0, viewDetailScroll - 3) });
|
|
2102
2377
|
return;
|
|
2103
2378
|
}
|
|
2104
|
-
const di =
|
|
2379
|
+
const di = viewFlat[viewIndex];
|
|
2105
2380
|
if (!di)
|
|
2106
2381
|
return;
|
|
2382
|
+
// ── Issues tab: backlog actions only (no worktree ops) ──────
|
|
2383
|
+
if (!isTrees) {
|
|
2384
|
+
const tracker = getIssueTracker(repoRootRef.current);
|
|
2385
|
+
const canMutate = tracker.canMutate === true;
|
|
2386
|
+
if (input === "w") {
|
|
2387
|
+
if (di.worktree?.sessionId) {
|
|
2388
|
+
dispatch({
|
|
2389
|
+
type: "SET_ACTION_MESSAGE",
|
|
2390
|
+
message: "Session active — switch to the Trees tab to resume.",
|
|
2391
|
+
});
|
|
2392
|
+
return;
|
|
2393
|
+
}
|
|
2394
|
+
dispatch({ type: "SET_OVERLAY", overlay: "mode-select" });
|
|
2395
|
+
return;
|
|
2396
|
+
}
|
|
2397
|
+
if (input === "n") {
|
|
2398
|
+
if (!canMutate) {
|
|
2399
|
+
dispatch({
|
|
2400
|
+
type: "SET_ACTION_MESSAGE",
|
|
2401
|
+
message: `${tracker.displayName} issues can't be created from santree`,
|
|
2402
|
+
});
|
|
2403
|
+
return;
|
|
2404
|
+
}
|
|
2405
|
+
dispatch({
|
|
2406
|
+
type: "ISSUE_FORM_OPEN",
|
|
2407
|
+
mode: "create",
|
|
2408
|
+
id: null,
|
|
2409
|
+
title: "",
|
|
2410
|
+
description: "",
|
|
2411
|
+
});
|
|
2412
|
+
return;
|
|
2413
|
+
}
|
|
2414
|
+
if (input === "e") {
|
|
2415
|
+
if (!canMutate) {
|
|
2416
|
+
dispatch({
|
|
2417
|
+
type: "SET_ACTION_MESSAGE",
|
|
2418
|
+
message: `${tracker.displayName} issues can't be edited from santree`,
|
|
2419
|
+
});
|
|
2420
|
+
return;
|
|
2421
|
+
}
|
|
2422
|
+
dispatch({
|
|
2423
|
+
type: "ISSUE_FORM_OPEN",
|
|
2424
|
+
mode: "edit",
|
|
2425
|
+
id: di.issue.identifier,
|
|
2426
|
+
title: di.issue.title,
|
|
2427
|
+
description: di.issue.description ?? "",
|
|
2428
|
+
});
|
|
2429
|
+
return;
|
|
2430
|
+
}
|
|
2431
|
+
if (input === "d") {
|
|
2432
|
+
if (!canMutate) {
|
|
2433
|
+
dispatch({ type: "SET_ACTION_MESSAGE", message: "Nothing to delete here" });
|
|
2434
|
+
return;
|
|
2435
|
+
}
|
|
2436
|
+
dispatch({ type: "ISSUE_DELETE_OPEN" });
|
|
2437
|
+
return;
|
|
2438
|
+
}
|
|
2439
|
+
if (input === "o") {
|
|
2440
|
+
if (!di.issue.url) {
|
|
2441
|
+
dispatch({ type: "SET_ACTION_MESSAGE", message: "No issue URL available" });
|
|
2442
|
+
return;
|
|
2443
|
+
}
|
|
2444
|
+
if (openUrl(di.issue.url)) {
|
|
2445
|
+
dispatch({ type: "SET_ACTION_MESSAGE", message: "Opened in browser" });
|
|
2446
|
+
}
|
|
2447
|
+
return;
|
|
2448
|
+
}
|
|
2449
|
+
return; // backlog has no worktree/PR actions
|
|
2450
|
+
}
|
|
2451
|
+
// ── Trees tab: worktree-in-progress actions ─────────────────
|
|
2107
2452
|
// Work
|
|
2108
2453
|
if (input === "w") {
|
|
2109
2454
|
if (di.worktree?.sessionId) {
|
|
@@ -2323,6 +2668,10 @@ export default function Dashboard() {
|
|
|
2323
2668
|
}
|
|
2324
2669
|
}, {
|
|
2325
2670
|
isActive: state.overlay !== "context-input" &&
|
|
2671
|
+
// Issue form title/description are owned by MultilineTextArea;
|
|
2672
|
+
// only the "saving" phase needs the outer handler (a no-op
|
|
2673
|
+
// swallow), so disabling it for the whole overlay is fine.
|
|
2674
|
+
state.overlay !== "issue-form" &&
|
|
2326
2675
|
(state.overlay !== "pr-create" || state.prCreatePhase !== "review") &&
|
|
2327
2676
|
(state.overlay !== "commit" || state.commitPhase !== "awaiting-message"),
|
|
2328
2677
|
});
|
|
@@ -2333,9 +2682,29 @@ export default function Dashboard() {
|
|
|
2333
2682
|
if (state.error) {
|
|
2334
2683
|
return (_jsx(Box, { width: columns, height: rows, flexDirection: "column", children: _jsxs(Box, { justifyContent: "center", alignItems: "center", flexGrow: 1, flexDirection: "column", children: [_jsxs(Text, { color: "red", bold: true, children: ["Error: ", state.error] }), _jsx(Text, { dimColor: true, children: "Press R to retry or q to quit" })] }) }));
|
|
2335
2684
|
}
|
|
2336
|
-
|
|
2685
|
+
// The active issue/tree row drives the detail pane and action row.
|
|
2686
|
+
const selectedIssue = (state.activeTab === "trees"
|
|
2687
|
+
? state.flatTrees[state.treeSelectedIndex]
|
|
2688
|
+
: state.flatIssues[state.selectedIndex]) ?? null;
|
|
2337
2689
|
const selectedReview = state.flatReviews[state.reviewSelectedIndex] ?? null;
|
|
2338
|
-
|
|
2690
|
+
const activeTracker = getIssueTracker(repoRootRef.current);
|
|
2691
|
+
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 ? _jsx(Text, { dimColor: true, children: " · 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) => {
|
|
2692
|
+
const sel = i === state.trackerSelectIndex;
|
|
2693
|
+
return (_jsxs(Text, { color: sel ? "cyan" : undefined, bold: sel, children: [sel ? "> " : " ", org.name, " (", org.slug, ")"] }, org.slug));
|
|
2694
|
+
}), _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: " " }), [
|
|
2695
|
+
{ label: "Local", hint: "built-in, file-based — no account needed" },
|
|
2696
|
+
{ label: "Linear", hint: "OAuth workspace" },
|
|
2697
|
+
{ label: "GitHub", hint: "GitHub Issues via gh CLI" },
|
|
2698
|
+
].map((t, i) => {
|
|
2699
|
+
const sel = i === state.trackerSelectIndex;
|
|
2700
|
+
return (_jsxs(Text, { children: [_jsxs(Text, { color: sel ? "cyan" : undefined, bold: sel, children: [sel ? "> " : " ", t.label] }), _jsxs(Text, { dimColor: true, children: [" \u2014 ", t.hint] })] }, t.label));
|
|
2701
|
+
}), _jsx(Text, { children: " " }), state.trackerSelectMessage ? (_jsx(Text, { color: "yellow", children: state.trackerSelectMessage })) : null, _jsx(Text, { dimColor: true, children: "j/k to navigate, Enter to select, q to quit" })] })) }) })) : state.overlay === "confirm-delete-issue" ? (_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: "Delete issue?" }), _jsx(Text, { children: " " }), _jsxs(Text, { children: [selectedIssue?.issue.identifier, " ", selectedIssue?.issue.title] }), _jsx(Text, { dimColor: true, children: "This removes the issue file from .santree/issues/" }), _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 === "issue-form" ? (_jsx(Box, { flexGrow: 1, justifyContent: "center", alignItems: "center", children: _jsxs(Box, { flexDirection: "column", paddingX: 2, width: Math.min(columns - 8, 100), children: [_jsxs(Text, { bold: true, color: "cyan", children: [state.issueFormMode === "edit" ? `Edit ${state.issueFormId}` : "New issue", " · ", state.issueFormPhase === "title"
|
|
2702
|
+
? "title"
|
|
2703
|
+
: state.issueFormPhase === "description"
|
|
2704
|
+
? "description"
|
|
2705
|
+
: "saving…"] }), state.issueFormError ? (_jsx(Text, { color: "red", children: state.issueFormError })) : (_jsx(Text, { dimColor: true, children: state.issueFormPhase === "title"
|
|
2706
|
+
? "First line is the title"
|
|
2707
|
+
: "Markdown body — Ctrl+D to save" })), _jsx(Text, { children: " " }), state.issueFormPhase === "saving" ? (_jsx(Text, { color: "cyan", children: "Saving\u2026" })) : state.issueFormPhase === "title" ? (_jsx(MultilineTextArea, { value: state.issueFormTitle, onChange: (v) => dispatch({ type: "ISSUE_FORM_TITLE", title: v }), onSubmit: () => dispatch({ type: "ISSUE_FORM_PHASE", phase: "description" }), onCancel: () => dispatch({ type: "ISSUE_FORM_CLOSE" }), width: Math.min(columns - 8, 100), height: 3, placeholder: "Issue title\u2026" })) : (_jsx(MultilineTextArea, { value: state.issueFormDescription, onChange: (v) => dispatch({ type: "ISSUE_FORM_DESC", description: v }), onSubmit: () => void submitIssueForm(), onCancel: () => dispatch({ type: "ISSUE_FORM_CLOSE" }), width: Math.min(columns - 8, 100), height: 10, placeholder: "Description (optional)\u2026" })), _jsx(Text, { children: " " }), _jsxs(Text, { dimColor: true, children: [_jsx(Text, { color: "cyan", bold: true, children: "Ctrl+D" }), state.issueFormPhase === "title" ? " next · " : " save · ", _jsx(Text, { color: "cyan", bold: true, children: "Ctrl+G" }), " cancel"] })] }) })) : state.overlay === "mode-select" ? (_jsx(Box, { flexGrow: 1, justifyContent: "center", alignItems: "center", children: _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 3, paddingY: 1, children: [_jsx(Text, { bold: true, children: "Select mode:" }), _jsx(Text, { children: " " }), _jsxs(Text, { children: [_jsx(Text, { color: "cyan", bold: true, children: "p" }), " Plan"] }), _jsxs(Text, { children: [_jsx(Text, { color: "cyan", bold: true, children: "i" }), " Implement"] }), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: "ESC to cancel" })] }) })) : state.overlay === "context-input" ? (_jsx(Box, { flexGrow: 1, justifyContent: "center", alignItems: "center", children: _jsxs(Box, { flexDirection: "column", paddingX: 2, width: Math.min(columns - 8, 100), children: [_jsxs(Text, { bold: true, color: "cyan", children: ["Extra context for ", state.contextInputMode] }), _jsx(Text, { dimColor: true, children: "Optional \u2014 appended to the prompt before launching Claude" }), _jsx(Text, { children: " " }), _jsx(MultilineTextArea, { value: state.contextInputValue, onChange: (v) => dispatch({ type: "CONTEXT_INPUT_CHANGE", value: v }), onSubmit: () => {
|
|
2339
2708
|
const mode = state.contextInputMode;
|
|
2340
2709
|
const ctx = state.contextInputValue;
|
|
2341
2710
|
dispatch({ type: "CONTEXT_INPUT_DONE" });
|
|
@@ -2346,17 +2715,22 @@ export default function Dashboard() {
|
|
|
2346
2715
|
const defaultBranch = getDefaultBranch();
|
|
2347
2716
|
const label = branch === defaultBranch ? `${branch} (default)` : branch;
|
|
2348
2717
|
return (_jsx(Text, { children: _jsxs(Text, { color: selected ? "cyan" : undefined, bold: selected, children: [selected ? "> " : " ", label] }) }, branch));
|
|
2349
|
-
}), _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.
|
|
2718
|
+
}), _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 ===
|
|
2719
|
+
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"
|
|
2720
|
+
? state.treeListScrollOffset
|
|
2721
|
+
: 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
|
|
2350
2722
|
.split("\n")
|
|
2351
2723
|
.slice(-(contentHeight - 1))
|
|
2352
|
-
.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, dispatch: dispatch })) : (_jsx(DetailPanel, { issue: selectedIssue, scrollOffset: state.
|
|
2724
|
+
.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, dispatch: dispatch })) : (_jsx(DetailPanel, { issue: selectedIssue, scrollOffset: state.activeTab === "trees"
|
|
2725
|
+
? state.treeDetailScrollOffset
|
|
2726
|
+
: 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 }) })] })) })] })] }));
|
|
2353
2727
|
}
|
|
2354
2728
|
/**
|
|
2355
2729
|
* Renders the per-issue action key hints (Resume / Editor / View diff / …)
|
|
2356
2730
|
* lifted out of the detail panels so they sit on the same row as the global
|
|
2357
2731
|
* command bar. Empty when nothing is selected.
|
|
2358
2732
|
*/
|
|
2359
|
-
function ActionRow({ activeTab, selectedIssue, selectedReview, overlay, trackerName, }) {
|
|
2733
|
+
function ActionRow({ activeTab, selectedIssue, selectedReview, overlay, trackerName, canMutate, }) {
|
|
2360
2734
|
// During the diff overlay, none of the per-issue actions apply (View diff
|
|
2361
2735
|
// is what got us here, Commit/PR/etc. need the detail panel context). Keep
|
|
2362
2736
|
// the row blank so the diff-specific CommandBar reads cleanly.
|
|
@@ -2367,7 +2741,7 @@ function ActionRow({ activeTab, selectedIssue, selectedReview, overlay, trackerN
|
|
|
2367
2741
|
? buildReviewActions(selectedReview)
|
|
2368
2742
|
: []
|
|
2369
2743
|
: selectedIssue
|
|
2370
|
-
? buildIssueActions(selectedIssue, trackerName)
|
|
2744
|
+
? buildIssueActions(selectedIssue, trackerName, { tab: activeTab, canMutate })
|
|
2371
2745
|
: [];
|
|
2372
2746
|
if (items.length === 0)
|
|
2373
2747
|
return _jsx(Text, { children: " " });
|