santree 0.6.2 → 0.7.0
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 +465 -54
- 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 +24 -3
- package/dist/lib/dashboard/IssueList.d.ts +2 -0
- package/dist/lib/dashboard/IssueList.js +9 -1
- package/dist/lib/dashboard/Overlays.d.ts +2 -1
- package/dist/lib/dashboard/Overlays.js +17 -3
- package/dist/lib/dashboard/data.d.ts +2 -0
- package/dist/lib/dashboard/data.js +56 -54
- package/dist/lib/dashboard/types.d.ts +80 -2
- package/dist/lib/dashboard/types.js +97 -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
|
@@ -15,10 +15,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";
|
|
17
17
|
import { shellEscape } from "../lib/multiplexer/types.js";
|
|
18
|
+
import Spinner from "ink-spinner";
|
|
18
19
|
import SquirrelLoader from "../lib/squirrel-loader.js";
|
|
19
20
|
import { getPRTemplate } from "../lib/github.js";
|
|
20
21
|
import { renderPrompt, renderDiff, renderTicket } from "../lib/prompts.js";
|
|
21
|
-
import { getIssueTracker } from "../lib/trackers/index.js";
|
|
22
|
+
import { getIssueTracker, isRepoTrackerConfigured, setRepoTracker } from "../lib/trackers/index.js";
|
|
23
|
+
import { setRepoLinearOrg } from "../lib/trackers/linear/index.js";
|
|
24
|
+
import { readLinearAuthStore } from "../lib/trackers/auth-store.js";
|
|
25
|
+
import { getAuthenticatedUser, getCurrentRepoNwo } from "../lib/trackers/github/auth.js";
|
|
22
26
|
import { openUrl } from "../lib/open-url.js";
|
|
23
27
|
import { parseUnifiedDiff } from "../lib/diff-parse.js";
|
|
24
28
|
import * as os from "os";
|
|
@@ -264,7 +268,6 @@ function ensureAltScreen() {
|
|
|
264
268
|
if (altScreenEntered)
|
|
265
269
|
return;
|
|
266
270
|
altScreenEntered = true;
|
|
267
|
-
getMultiplexer().renameWindow("", "santree");
|
|
268
271
|
process.stdout.write("\x1b[?1049h"); // Enter alternate screen buffer
|
|
269
272
|
process.stdout.write("\x1b[?25l"); // Hide cursor
|
|
270
273
|
}
|
|
@@ -387,9 +390,18 @@ export default function Dashboard() {
|
|
|
387
390
|
return;
|
|
388
391
|
}
|
|
389
392
|
repoRootRef.current = repoRoot;
|
|
393
|
+
// No tracker configured → show the selection flow instead of letting
|
|
394
|
+
// getIssueTracker silently fall back to GitHub and then fail auth on
|
|
395
|
+
// the dead-end error screen. Genuine auth/network failures of a
|
|
396
|
+
// *configured* tracker still hit the catch → error screen below.
|
|
397
|
+
if (!isRepoTrackerConfigured(repoRoot)) {
|
|
398
|
+
dispatch({ type: "TRACKER_SELECT_OPEN" });
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
390
401
|
try {
|
|
391
402
|
// Re-detect terminal theme alongside data fetch so light↔dark
|
|
392
|
-
// switches propagate within one refresh cycle (≤
|
|
403
|
+
// switches propagate within one refresh cycle (≤5min, or sooner
|
|
404
|
+
// on a manual `R`). Skip the
|
|
393
405
|
// OSC 11 query when a text-input overlay is active — the
|
|
394
406
|
// terminal's response would otherwise leak into the user's
|
|
395
407
|
// commit/PR/context message via Ink's stdin handler.
|
|
@@ -528,18 +540,22 @@ export default function Dashboard() {
|
|
|
528
540
|
}
|
|
529
541
|
return;
|
|
530
542
|
}
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
const
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
+
{
|
|
544
|
+
const isTreesTab = s.activeTab === "trees";
|
|
545
|
+
const flat = isTreesTab ? s.flatTrees : s.flatIssues;
|
|
546
|
+
const idx = isTreesTab ? s.treeSelectedIndex : s.selectedIndex;
|
|
547
|
+
const detailOff = isTreesTab ? s.treeDetailScrollOffset : s.detailScrollOffset;
|
|
548
|
+
if (inLeftPane) {
|
|
549
|
+
const maxIdx = flat.length - 1;
|
|
550
|
+
if (maxIdx < 0)
|
|
551
|
+
return;
|
|
552
|
+
const next = Math.max(0, Math.min(idx + delta, maxIdx));
|
|
553
|
+
dispatch({ type: isTreesTab ? "TREE_SELECT" : "SELECT", index: next });
|
|
554
|
+
}
|
|
555
|
+
else {
|
|
556
|
+
const next = Math.max(0, detailOff + delta);
|
|
557
|
+
dispatch({ type: isTreesTab ? "TREE_SCROLL_DETAIL" : "SCROLL_DETAIL", offset: next });
|
|
558
|
+
}
|
|
543
559
|
}
|
|
544
560
|
return;
|
|
545
561
|
}
|
|
@@ -611,12 +627,18 @@ export default function Dashboard() {
|
|
|
611
627
|
}
|
|
612
628
|
return;
|
|
613
629
|
}
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
630
|
+
{
|
|
631
|
+
const isTreesTab = s.activeTab === "trees";
|
|
632
|
+
const flat = isTreesTab ? s.flatTrees : s.flatIssues;
|
|
633
|
+
const grps = isTreesTab ? s.treeGroups : s.groups;
|
|
634
|
+
const scrollOff = isTreesTab ? s.treeListScrollOffset : s.listScrollOffset;
|
|
635
|
+
if (flat.length === 0)
|
|
636
|
+
return;
|
|
637
|
+
const listRow = scrollOff + contentRow;
|
|
638
|
+
const flatIdx = getFlatIndexForListRow(grps, flat, listRow);
|
|
639
|
+
if (flatIdx !== null && flatIdx >= 0 && flatIdx < flat.length) {
|
|
640
|
+
dispatch({ type: isTreesTab ? "TREE_SELECT" : "SELECT", index: flatIdx });
|
|
641
|
+
}
|
|
620
642
|
}
|
|
621
643
|
};
|
|
622
644
|
if (process.stdin.isTTY) {
|
|
@@ -627,16 +649,21 @@ export default function Dashboard() {
|
|
|
627
649
|
await refresh(true);
|
|
628
650
|
};
|
|
629
651
|
init();
|
|
630
|
-
// Auto-refresh every
|
|
631
|
-
//
|
|
632
|
-
//
|
|
633
|
-
//
|
|
652
|
+
// Auto-refresh every 5 minutes. Each refresh fans out into several
|
|
653
|
+
// `gh pr view`/`gh pr checks` calls per worktree-PR plus the reviews
|
|
654
|
+
// tab, all on the GraphQL API (5000-point/hour budget) — a 30s cadence
|
|
655
|
+
// drained it within the hour when the dashboard was left open. Press
|
|
656
|
+
// `R` for an on-demand refresh between cycles. While the diff overlay
|
|
657
|
+
// is open, also bump the diff refresh tick so new/removed files
|
|
658
|
+
// (created or deleted outside the dashboard) eventually show up.
|
|
659
|
+
// Stage/unstage already patch XY in place, so this is purely about
|
|
660
|
+
// file-set drift.
|
|
634
661
|
refreshTimerRef.current = setInterval(() => {
|
|
635
662
|
refresh();
|
|
636
663
|
if (stateRef.current.overlay === "diff") {
|
|
637
664
|
dispatch({ type: "DIFF_REFRESH_FILES" });
|
|
638
665
|
}
|
|
639
|
-
},
|
|
666
|
+
}, 300_000);
|
|
640
667
|
return () => {
|
|
641
668
|
if (refreshTimerRef.current)
|
|
642
669
|
clearInterval(refreshTimerRef.current);
|
|
@@ -663,6 +690,21 @@ export default function Dashboard() {
|
|
|
663
690
|
dispatch({ type: "SCROLL_LIST", offset });
|
|
664
691
|
}
|
|
665
692
|
}, [state.selectedIndex, state.groups, contentHeight, state.listScrollOffset]);
|
|
693
|
+
// ── Trees list scroll tracking ───────────────────────────────────
|
|
694
|
+
useEffect(() => {
|
|
695
|
+
const rowIdx = getRowIndexForFlatIndex(state.treeGroups, state.flatTrees, state.treeSelectedIndex);
|
|
696
|
+
const maxVisible = contentHeight - LIST_FOOTER_HEIGHT;
|
|
697
|
+
let offset = state.treeListScrollOffset;
|
|
698
|
+
if (rowIdx < offset) {
|
|
699
|
+
offset = Math.max(0, rowIdx - 1);
|
|
700
|
+
}
|
|
701
|
+
else if (rowIdx >= offset + maxVisible) {
|
|
702
|
+
offset = rowIdx - maxVisible + 2;
|
|
703
|
+
}
|
|
704
|
+
if (offset !== state.treeListScrollOffset) {
|
|
705
|
+
dispatch({ type: "TREE_SCROLL_LIST", offset });
|
|
706
|
+
}
|
|
707
|
+
}, [state.treeSelectedIndex, state.treeGroups, contentHeight, state.treeListScrollOffset]);
|
|
666
708
|
// ── Review list scroll tracking ──────────────────────────────────
|
|
667
709
|
useEffect(() => {
|
|
668
710
|
// Row index = 1 (column header) + flatIndex
|
|
@@ -686,6 +728,7 @@ export default function Dashboard() {
|
|
|
686
728
|
// Disable tracking while any text-input overlay is mounted; restore on exit.
|
|
687
729
|
useEffect(() => {
|
|
688
730
|
const needsMouseOff = state.overlay === "context-input" ||
|
|
731
|
+
(state.overlay === "issue-form" && state.issueFormPhase !== "saving") ||
|
|
689
732
|
(state.overlay === "pr-create" && state.prCreatePhase === "review") ||
|
|
690
733
|
(state.overlay === "commit" && state.commitPhase === "awaiting-message");
|
|
691
734
|
if (!needsMouseOff)
|
|
@@ -694,7 +737,7 @@ export default function Dashboard() {
|
|
|
694
737
|
return () => {
|
|
695
738
|
process.stdout.write("\x1b[?1002h\x1b[?1006h");
|
|
696
739
|
};
|
|
697
|
-
}, [state.overlay, state.prCreatePhase, state.commitPhase]);
|
|
740
|
+
}, [state.overlay, state.issueFormPhase, state.prCreatePhase, state.commitPhase]);
|
|
698
741
|
// ── Diff overlay: load file list when opened (gh pr diff path) ────
|
|
699
742
|
// Reviews-tab PRs without a local worktree shell out to `gh pr diff <n>`,
|
|
700
743
|
// parse the unified blob into per-file records, and stash the per-file
|
|
@@ -982,7 +1025,10 @@ export default function Dashboard() {
|
|
|
982
1025
|
}
|
|
983
1026
|
}, []);
|
|
984
1027
|
const createAndLaunch = useCallback(async (mode, runSetup, base, contextFile) => {
|
|
985
|
-
const
|
|
1028
|
+
const sref = stateRef.current;
|
|
1029
|
+
const di = sref.activeTab === "trees"
|
|
1030
|
+
? sref.flatTrees[sref.treeSelectedIndex]
|
|
1031
|
+
: sref.flatIssues[sref.selectedIndex];
|
|
986
1032
|
if (!di)
|
|
987
1033
|
return;
|
|
988
1034
|
const repoRoot = repoRootRef.current;
|
|
@@ -1091,7 +1137,9 @@ export default function Dashboard() {
|
|
|
1091
1137
|
createAndLaunch(mode, false, base, contextFile);
|
|
1092
1138
|
}, [createAndLaunch]);
|
|
1093
1139
|
const doWork = useCallback((mode, customContext) => {
|
|
1094
|
-
const di = state.
|
|
1140
|
+
const di = state.activeTab === "trees"
|
|
1141
|
+
? state.flatTrees[state.treeSelectedIndex]
|
|
1142
|
+
: state.flatIssues[state.selectedIndex];
|
|
1095
1143
|
if (!di)
|
|
1096
1144
|
return;
|
|
1097
1145
|
const repoRoot = repoRootRef.current;
|
|
@@ -1118,7 +1166,9 @@ export default function Dashboard() {
|
|
|
1118
1166
|
pendingContextFileRef.current = contextFile ?? null;
|
|
1119
1167
|
const defaultBranch = getDefaultBranch();
|
|
1120
1168
|
const baseOptions = [defaultBranch];
|
|
1121
|
-
|
|
1169
|
+
// Worktree branches live on the Trees tab now; scan there for
|
|
1170
|
+
// candidate base branches (stacked work).
|
|
1171
|
+
for (const fi of [...state.flatTrees, ...state.flatIssues]) {
|
|
1122
1172
|
if (fi.worktree && !baseOptions.includes(fi.worktree.branch)) {
|
|
1123
1173
|
baseOptions.push(fi.worktree.branch);
|
|
1124
1174
|
}
|
|
@@ -1136,11 +1186,153 @@ export default function Dashboard() {
|
|
|
1136
1186
|
}, [
|
|
1137
1187
|
state.flatIssues,
|
|
1138
1188
|
state.selectedIndex,
|
|
1189
|
+
state.flatTrees,
|
|
1190
|
+
state.treeSelectedIndex,
|
|
1191
|
+
state.activeTab,
|
|
1139
1192
|
exit,
|
|
1140
1193
|
launchWorkInTmux,
|
|
1141
1194
|
proceedAfterBaseSelect,
|
|
1142
1195
|
writeContextFile,
|
|
1143
1196
|
]);
|
|
1197
|
+
// ── Tracker selection ────────────────────────────────────────────
|
|
1198
|
+
// Mirrors `santree issue setup`. Local needs no account; Linear picks an
|
|
1199
|
+
// authenticated workspace (sub-list when >1); GitHub verifies `gh`.
|
|
1200
|
+
const chooseTracker = useCallback(async (kind) => {
|
|
1201
|
+
const root = repoRootRef.current ?? findMainRepoRoot();
|
|
1202
|
+
if (!root) {
|
|
1203
|
+
dispatch({ type: "SET_ERROR", error: "Not inside a git repository" });
|
|
1204
|
+
return;
|
|
1205
|
+
}
|
|
1206
|
+
repoRootRef.current = root;
|
|
1207
|
+
if (kind === "local") {
|
|
1208
|
+
setRepoTracker(root, "local");
|
|
1209
|
+
dispatch({ type: "TRACKER_SELECT_CLOSE" });
|
|
1210
|
+
refresh();
|
|
1211
|
+
return;
|
|
1212
|
+
}
|
|
1213
|
+
if (kind === "linear") {
|
|
1214
|
+
const store = readLinearAuthStore();
|
|
1215
|
+
const orgs = Object.entries(store).map(([slug, tokens]) => ({
|
|
1216
|
+
slug,
|
|
1217
|
+
name: tokens.org_name,
|
|
1218
|
+
}));
|
|
1219
|
+
if (orgs.length === 0) {
|
|
1220
|
+
dispatch({
|
|
1221
|
+
type: "TRACKER_SELECT_MESSAGE",
|
|
1222
|
+
message: "No authenticated Linear workspaces. Run: santree linear auth",
|
|
1223
|
+
});
|
|
1224
|
+
return;
|
|
1225
|
+
}
|
|
1226
|
+
if (orgs.length === 1) {
|
|
1227
|
+
setRepoLinearOrg(root, orgs[0].slug);
|
|
1228
|
+
setRepoTracker(root, "linear");
|
|
1229
|
+
dispatch({ type: "TRACKER_SELECT_CLOSE" });
|
|
1230
|
+
refresh();
|
|
1231
|
+
return;
|
|
1232
|
+
}
|
|
1233
|
+
dispatch({ type: "TRACKER_SELECT_PHASE", phase: "linear-org", orgs });
|
|
1234
|
+
return;
|
|
1235
|
+
}
|
|
1236
|
+
// github
|
|
1237
|
+
const user = await getAuthenticatedUser();
|
|
1238
|
+
if (!user) {
|
|
1239
|
+
dispatch({
|
|
1240
|
+
type: "TRACKER_SELECT_MESSAGE",
|
|
1241
|
+
message: "GitHub CLI not authenticated. Run: santree github auth",
|
|
1242
|
+
});
|
|
1243
|
+
return;
|
|
1244
|
+
}
|
|
1245
|
+
setRepoTracker(root, "github");
|
|
1246
|
+
await getCurrentRepoNwo(root);
|
|
1247
|
+
dispatch({ type: "TRACKER_SELECT_CLOSE" });
|
|
1248
|
+
refresh();
|
|
1249
|
+
}, [refresh]);
|
|
1250
|
+
const chooseLinearOrg = useCallback((slug) => {
|
|
1251
|
+
const root = repoRootRef.current ?? findMainRepoRoot();
|
|
1252
|
+
if (!root)
|
|
1253
|
+
return;
|
|
1254
|
+
setRepoLinearOrg(root, slug);
|
|
1255
|
+
setRepoTracker(root, "linear");
|
|
1256
|
+
dispatch({ type: "TRACKER_SELECT_CLOSE" });
|
|
1257
|
+
refresh();
|
|
1258
|
+
}, [refresh]);
|
|
1259
|
+
// ── Issue CRUD (built-in tracker only) ───────────────────────────
|
|
1260
|
+
const submitIssueForm = useCallback(async () => {
|
|
1261
|
+
const s = stateRef.current;
|
|
1262
|
+
const root = repoRootRef.current;
|
|
1263
|
+
if (!root)
|
|
1264
|
+
return;
|
|
1265
|
+
const tracker = getIssueTracker(root);
|
|
1266
|
+
const title = s.issueFormTitle.split("\n")[0]?.trim() ?? "";
|
|
1267
|
+
if (!title) {
|
|
1268
|
+
dispatch({ type: "ISSUE_FORM_ERROR", error: "Title is required" });
|
|
1269
|
+
return;
|
|
1270
|
+
}
|
|
1271
|
+
const description = s.issueFormDescription;
|
|
1272
|
+
dispatch({ type: "ISSUE_FORM_PHASE", phase: "saving" });
|
|
1273
|
+
try {
|
|
1274
|
+
if (s.issueFormMode === "edit" && s.issueFormId && tracker.updateIssue) {
|
|
1275
|
+
const res = await tracker.updateIssue(s.issueFormId, { title, description }, root);
|
|
1276
|
+
if (!res.ok) {
|
|
1277
|
+
dispatch({ type: "ISSUE_FORM_ERROR", error: res.message ?? "Update failed" });
|
|
1278
|
+
return;
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
else if (tracker.createIssue) {
|
|
1282
|
+
const res = await tracker.createIssue({ title, description }, root);
|
|
1283
|
+
if (!res.ok) {
|
|
1284
|
+
dispatch({ type: "ISSUE_FORM_ERROR", error: res.message ?? "Create failed" });
|
|
1285
|
+
return;
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
else {
|
|
1289
|
+
dispatch({ type: "ISSUE_FORM_ERROR", error: "Tracker does not support editing" });
|
|
1290
|
+
return;
|
|
1291
|
+
}
|
|
1292
|
+
dispatch({ type: "ISSUE_FORM_CLOSE" });
|
|
1293
|
+
dispatch({
|
|
1294
|
+
type: "SET_ACTION_MESSAGE",
|
|
1295
|
+
message: s.issueFormMode === "edit" ? "Issue updated" : "Issue created",
|
|
1296
|
+
});
|
|
1297
|
+
refresh();
|
|
1298
|
+
}
|
|
1299
|
+
catch (e) {
|
|
1300
|
+
dispatch({
|
|
1301
|
+
type: "ISSUE_FORM_ERROR",
|
|
1302
|
+
error: e instanceof Error ? e.message : "Failed to save issue",
|
|
1303
|
+
});
|
|
1304
|
+
}
|
|
1305
|
+
}, [refresh]);
|
|
1306
|
+
const deleteSelectedIssue = useCallback(async () => {
|
|
1307
|
+
const s = stateRef.current;
|
|
1308
|
+
const root = repoRootRef.current;
|
|
1309
|
+
if (!root)
|
|
1310
|
+
return;
|
|
1311
|
+
const di = s.flatIssues[s.selectedIndex];
|
|
1312
|
+
if (!di)
|
|
1313
|
+
return;
|
|
1314
|
+
const tracker = getIssueTracker(root);
|
|
1315
|
+
dispatch({ type: "ISSUE_DELETE_CLOSE" });
|
|
1316
|
+
if (!tracker.deleteIssue)
|
|
1317
|
+
return;
|
|
1318
|
+
try {
|
|
1319
|
+
const res = await tracker.deleteIssue(di.issue.identifier, root);
|
|
1320
|
+
dispatch({
|
|
1321
|
+
type: "SET_ACTION_MESSAGE",
|
|
1322
|
+
message: res.ok
|
|
1323
|
+
? `Deleted ${di.issue.identifier}`
|
|
1324
|
+
: `Failed: ${res.message ?? "delete failed"}`,
|
|
1325
|
+
});
|
|
1326
|
+
if (res.ok)
|
|
1327
|
+
refresh();
|
|
1328
|
+
}
|
|
1329
|
+
catch (e) {
|
|
1330
|
+
dispatch({
|
|
1331
|
+
type: "SET_ACTION_MESSAGE",
|
|
1332
|
+
message: e instanceof Error ? e.message : "Delete failed",
|
|
1333
|
+
});
|
|
1334
|
+
}
|
|
1335
|
+
}, [refresh]);
|
|
1144
1336
|
// ── Commit flow ──────────────────────────────────────────────────
|
|
1145
1337
|
const handleStageAll = useCallback(async () => {
|
|
1146
1338
|
const wtPath = stateRef.current.commitWorktreePath;
|
|
@@ -1382,7 +1574,8 @@ export default function Dashboard() {
|
|
|
1382
1574
|
try {
|
|
1383
1575
|
const bodyFile = path.join(os.tmpdir(), `santree-pr-${Date.now()}.md`);
|
|
1384
1576
|
fs.writeFileSync(bodyFile, s.prCreateBody);
|
|
1385
|
-
const
|
|
1577
|
+
const draftFlag = s.prCreateDraft ? " --draft" : "";
|
|
1578
|
+
const { stdout } = await execAsync(`gh pr create --title "${s.prCreateTitle.replace(/"/g, '\\"')}" --base "${base}" --head "${s.prCreateBranch}" --body-file "${bodyFile}"${draftFlag}`, { cwd });
|
|
1386
1579
|
try {
|
|
1387
1580
|
fs.unlinkSync(bodyFile);
|
|
1388
1581
|
}
|
|
@@ -1404,8 +1597,25 @@ export default function Dashboard() {
|
|
|
1404
1597
|
return;
|
|
1405
1598
|
const base = getBaseBranch(s.prCreateBranch);
|
|
1406
1599
|
const cwd = s.prCreateWorktreePath;
|
|
1600
|
+
// Carry the edited title/body into GitHub's compose page so the browser
|
|
1601
|
+
// opens pre-filled (gh passes them as URL query params). Without this,
|
|
1602
|
+
// the fill→"open in browser" path would drop everything the user just
|
|
1603
|
+
// reviewed. Note: very long bodies can be truncated by GitHub's URL
|
|
1604
|
+
// length limit — gh's documented `--web` behavior; the editable compose
|
|
1605
|
+
// page is the fallback. Draft selection lives in the browser dropdown,
|
|
1606
|
+
// since `gh --web` doesn't accept `--draft`.
|
|
1607
|
+
let bodyFile = null;
|
|
1407
1608
|
try {
|
|
1408
|
-
|
|
1609
|
+
let cmd = `gh pr create --web --base "${base}" --head "${s.prCreateBranch}"`;
|
|
1610
|
+
if (s.prCreateTitle) {
|
|
1611
|
+
cmd += ` --title "${s.prCreateTitle.replace(/"/g, '\\"')}"`;
|
|
1612
|
+
}
|
|
1613
|
+
if (s.prCreateBody) {
|
|
1614
|
+
bodyFile = path.join(os.tmpdir(), `santree-pr-${Date.now()}.md`);
|
|
1615
|
+
fs.writeFileSync(bodyFile, s.prCreateBody);
|
|
1616
|
+
cmd += ` --body-file "${bodyFile}"`;
|
|
1617
|
+
}
|
|
1618
|
+
await execAsync(cmd, { cwd });
|
|
1409
1619
|
dispatch({ type: "PR_CREATE_DONE", url: "" });
|
|
1410
1620
|
setTimeout(() => {
|
|
1411
1621
|
dispatch({ type: "PR_CREATE_CANCEL" });
|
|
@@ -1416,6 +1626,14 @@ export default function Dashboard() {
|
|
|
1416
1626
|
const msg = e?.stderr?.trim() || e?.message || "Failed to open in browser";
|
|
1417
1627
|
dispatch({ type: "PR_CREATE_ERROR", error: msg });
|
|
1418
1628
|
}
|
|
1629
|
+
finally {
|
|
1630
|
+
if (bodyFile) {
|
|
1631
|
+
try {
|
|
1632
|
+
fs.unlinkSync(bodyFile);
|
|
1633
|
+
}
|
|
1634
|
+
catch { }
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1419
1637
|
}, [refresh]);
|
|
1420
1638
|
// ── Keyboard ──────────────────────────────────────────────────────
|
|
1421
1639
|
useInput((input, key) => {
|
|
@@ -1497,6 +1715,10 @@ export default function Dashboard() {
|
|
|
1497
1715
|
confirmPrCreate();
|
|
1498
1716
|
return;
|
|
1499
1717
|
}
|
|
1718
|
+
if (input === "d") {
|
|
1719
|
+
dispatch({ type: "PR_CREATE_TOGGLE_DRAFT" });
|
|
1720
|
+
return;
|
|
1721
|
+
}
|
|
1500
1722
|
if (input === "e") {
|
|
1501
1723
|
dispatch({ type: "PR_CREATE_EDIT" });
|
|
1502
1724
|
return;
|
|
@@ -1782,7 +2004,8 @@ export default function Dashboard() {
|
|
|
1782
2004
|
if (state.overlay === "confirm-delete") {
|
|
1783
2005
|
if (input === "y") {
|
|
1784
2006
|
dispatch({ type: "SET_OVERLAY", overlay: null });
|
|
1785
|
-
|
|
2007
|
+
// Worktree removal is a Trees-tab action.
|
|
2008
|
+
const di = state.flatTrees[state.treeSelectedIndex];
|
|
1786
2009
|
if (di?.worktree) {
|
|
1787
2010
|
const repoRoot = repoRootRef.current;
|
|
1788
2011
|
if (repoRoot) {
|
|
@@ -1814,12 +2037,86 @@ export default function Dashboard() {
|
|
|
1814
2037
|
}
|
|
1815
2038
|
return;
|
|
1816
2039
|
}
|
|
2040
|
+
// Tracker-selection overlay (shown when no tracker is configured,
|
|
2041
|
+
// or reopened with `t`). Not a text input — outer useInput drives it.
|
|
2042
|
+
if (state.overlay === "tracker-select") {
|
|
2043
|
+
if (state.trackerSelectPhase === "linear-org") {
|
|
2044
|
+
const orgs = state.trackerSelectOrgs;
|
|
2045
|
+
if (input === "j" || key.downArrow) {
|
|
2046
|
+
dispatch({
|
|
2047
|
+
type: "TRACKER_SELECT_MOVE",
|
|
2048
|
+
index: Math.min(state.trackerSelectIndex + 1, orgs.length - 1),
|
|
2049
|
+
});
|
|
2050
|
+
return;
|
|
2051
|
+
}
|
|
2052
|
+
if (input === "k" || key.upArrow) {
|
|
2053
|
+
dispatch({
|
|
2054
|
+
type: "TRACKER_SELECT_MOVE",
|
|
2055
|
+
index: Math.max(state.trackerSelectIndex - 1, 0),
|
|
2056
|
+
});
|
|
2057
|
+
return;
|
|
2058
|
+
}
|
|
2059
|
+
if (key.return) {
|
|
2060
|
+
const org = orgs[state.trackerSelectIndex];
|
|
2061
|
+
if (org)
|
|
2062
|
+
chooseLinearOrg(org.slug);
|
|
2063
|
+
return;
|
|
2064
|
+
}
|
|
2065
|
+
if (key.escape) {
|
|
2066
|
+
dispatch({ type: "TRACKER_SELECT_PHASE", phase: "root" });
|
|
2067
|
+
return;
|
|
2068
|
+
}
|
|
2069
|
+
return;
|
|
2070
|
+
}
|
|
2071
|
+
const TRACKER_KINDS = ["local", "linear", "github"];
|
|
2072
|
+
if (input === "j" || key.downArrow) {
|
|
2073
|
+
dispatch({
|
|
2074
|
+
type: "TRACKER_SELECT_MOVE",
|
|
2075
|
+
index: Math.min(state.trackerSelectIndex + 1, TRACKER_KINDS.length - 1),
|
|
2076
|
+
});
|
|
2077
|
+
return;
|
|
2078
|
+
}
|
|
2079
|
+
if (input === "k" || key.upArrow) {
|
|
2080
|
+
dispatch({
|
|
2081
|
+
type: "TRACKER_SELECT_MOVE",
|
|
2082
|
+
index: Math.max(state.trackerSelectIndex - 1, 0),
|
|
2083
|
+
});
|
|
2084
|
+
return;
|
|
2085
|
+
}
|
|
2086
|
+
if (key.return) {
|
|
2087
|
+
void chooseTracker(TRACKER_KINDS[state.trackerSelectIndex] ?? "local");
|
|
2088
|
+
return;
|
|
2089
|
+
}
|
|
2090
|
+
if (input === "q" || key.escape) {
|
|
2091
|
+
// Can't proceed without a tracker — leave the dashboard.
|
|
2092
|
+
exit();
|
|
2093
|
+
return;
|
|
2094
|
+
}
|
|
2095
|
+
return;
|
|
2096
|
+
}
|
|
2097
|
+
// Confirm-delete-issue overlay (built-in tracker only)
|
|
2098
|
+
if (state.overlay === "confirm-delete-issue") {
|
|
2099
|
+
if (input === "y") {
|
|
2100
|
+
void deleteSelectedIssue();
|
|
2101
|
+
return;
|
|
2102
|
+
}
|
|
2103
|
+
if (input === "n" || key.escape || input === "q") {
|
|
2104
|
+
dispatch({ type: "ISSUE_DELETE_CLOSE" });
|
|
2105
|
+
return;
|
|
2106
|
+
}
|
|
2107
|
+
return;
|
|
2108
|
+
}
|
|
2109
|
+
// Issue create/edit form. Title/description phases are owned by
|
|
2110
|
+
// MultilineTextArea (outer useInput disabled via isActive); only
|
|
2111
|
+
// the "saving" phase reaches here — swallow all keys.
|
|
2112
|
+
if (state.overlay === "issue-form") {
|
|
2113
|
+
return;
|
|
2114
|
+
}
|
|
1817
2115
|
// Tab switching (only when no overlay is active)
|
|
2116
|
+
const TAB_ORDER = ["issues", "trees", "reviews"];
|
|
1818
2117
|
if (key.tab) {
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
tab: state.activeTab === "issues" ? "reviews" : "issues",
|
|
1822
|
-
});
|
|
2118
|
+
const idx = TAB_ORDER.indexOf(state.activeTab);
|
|
2119
|
+
dispatch({ type: "SET_TAB", tab: TAB_ORDER[(idx + 1) % TAB_ORDER.length] });
|
|
1823
2120
|
return;
|
|
1824
2121
|
}
|
|
1825
2122
|
if (input === "1") {
|
|
@@ -1827,9 +2124,18 @@ export default function Dashboard() {
|
|
|
1827
2124
|
return;
|
|
1828
2125
|
}
|
|
1829
2126
|
if (input === "2") {
|
|
2127
|
+
dispatch({ type: "SET_TAB", tab: "trees" });
|
|
2128
|
+
return;
|
|
2129
|
+
}
|
|
2130
|
+
if (input === "3") {
|
|
1830
2131
|
dispatch({ type: "SET_TAB", tab: "reviews" });
|
|
1831
2132
|
return;
|
|
1832
2133
|
}
|
|
2134
|
+
// Reopen tracker selection from any tab.
|
|
2135
|
+
if (input === "t") {
|
|
2136
|
+
dispatch({ type: "TRACKER_SELECT_OPEN" });
|
|
2137
|
+
return;
|
|
2138
|
+
}
|
|
1833
2139
|
// Quit
|
|
1834
2140
|
if (input === "q") {
|
|
1835
2141
|
exit();
|
|
@@ -2077,33 +2383,109 @@ export default function Dashboard() {
|
|
|
2077
2383
|
}
|
|
2078
2384
|
return; // prevent fallthrough to issues actions
|
|
2079
2385
|
}
|
|
2080
|
-
|
|
2386
|
+
// Issues tab = backlog/planning; Trees tab = worktrees in
|
|
2387
|
+
// progress. Both reuse the same list/detail UI and worktree action
|
|
2388
|
+
// handlers below — only the backing data + selection slice differ.
|
|
2389
|
+
const isTrees = state.activeTab === "trees";
|
|
2390
|
+
const viewFlat = isTrees ? state.flatTrees : state.flatIssues;
|
|
2391
|
+
const viewIndex = isTrees ? state.treeSelectedIndex : state.selectedIndex;
|
|
2392
|
+
const viewDetailScroll = isTrees ? state.treeDetailScrollOffset : state.detailScrollOffset;
|
|
2393
|
+
const selectAction = isTrees ? "TREE_SELECT" : "SELECT";
|
|
2394
|
+
const scrollDetailAction = isTrees
|
|
2395
|
+
? "TREE_SCROLL_DETAIL"
|
|
2396
|
+
: "SCROLL_DETAIL";
|
|
2397
|
+
const maxIndex = viewFlat.length - 1;
|
|
2081
2398
|
// Navigation
|
|
2082
2399
|
if (input === "j" || (key.downArrow && !key.shift)) {
|
|
2083
|
-
|
|
2084
|
-
dispatch({ type: "SELECT", index: next });
|
|
2400
|
+
dispatch({ type: selectAction, index: Math.min(viewIndex + 1, maxIndex) });
|
|
2085
2401
|
return;
|
|
2086
2402
|
}
|
|
2087
2403
|
if (input === "k" || (key.upArrow && !key.shift)) {
|
|
2088
|
-
|
|
2089
|
-
dispatch({ type: "SELECT", index: prev });
|
|
2404
|
+
dispatch({ type: selectAction, index: Math.max(viewIndex - 1, 0) });
|
|
2090
2405
|
return;
|
|
2091
2406
|
}
|
|
2092
2407
|
// Detail scroll
|
|
2093
2408
|
if (key.shift && key.downArrow) {
|
|
2094
|
-
dispatch({ type:
|
|
2409
|
+
dispatch({ type: scrollDetailAction, offset: viewDetailScroll + 3 });
|
|
2095
2410
|
return;
|
|
2096
2411
|
}
|
|
2097
2412
|
if (key.shift && key.upArrow) {
|
|
2098
|
-
dispatch({
|
|
2099
|
-
type: "SCROLL_DETAIL",
|
|
2100
|
-
offset: Math.max(0, state.detailScrollOffset - 3),
|
|
2101
|
-
});
|
|
2413
|
+
dispatch({ type: scrollDetailAction, offset: Math.max(0, viewDetailScroll - 3) });
|
|
2102
2414
|
return;
|
|
2103
2415
|
}
|
|
2104
|
-
const di =
|
|
2416
|
+
const di = viewFlat[viewIndex];
|
|
2105
2417
|
if (!di)
|
|
2106
2418
|
return;
|
|
2419
|
+
// ── Issues tab: backlog actions only (no worktree ops) ──────
|
|
2420
|
+
if (!isTrees) {
|
|
2421
|
+
const tracker = getIssueTracker(repoRootRef.current);
|
|
2422
|
+
const canMutate = tracker.canMutate === true;
|
|
2423
|
+
if (input === "w") {
|
|
2424
|
+
if (di.worktree?.sessionId) {
|
|
2425
|
+
dispatch({
|
|
2426
|
+
type: "SET_ACTION_MESSAGE",
|
|
2427
|
+
message: "Session active — switch to the Trees tab to resume.",
|
|
2428
|
+
});
|
|
2429
|
+
return;
|
|
2430
|
+
}
|
|
2431
|
+
dispatch({ type: "SET_OVERLAY", overlay: "mode-select" });
|
|
2432
|
+
return;
|
|
2433
|
+
}
|
|
2434
|
+
if (input === "n") {
|
|
2435
|
+
if (!canMutate) {
|
|
2436
|
+
dispatch({
|
|
2437
|
+
type: "SET_ACTION_MESSAGE",
|
|
2438
|
+
message: `${tracker.displayName} issues can't be created from santree`,
|
|
2439
|
+
});
|
|
2440
|
+
return;
|
|
2441
|
+
}
|
|
2442
|
+
dispatch({
|
|
2443
|
+
type: "ISSUE_FORM_OPEN",
|
|
2444
|
+
mode: "create",
|
|
2445
|
+
id: null,
|
|
2446
|
+
title: "",
|
|
2447
|
+
description: "",
|
|
2448
|
+
});
|
|
2449
|
+
return;
|
|
2450
|
+
}
|
|
2451
|
+
if (input === "e") {
|
|
2452
|
+
if (!canMutate) {
|
|
2453
|
+
dispatch({
|
|
2454
|
+
type: "SET_ACTION_MESSAGE",
|
|
2455
|
+
message: `${tracker.displayName} issues can't be edited from santree`,
|
|
2456
|
+
});
|
|
2457
|
+
return;
|
|
2458
|
+
}
|
|
2459
|
+
dispatch({
|
|
2460
|
+
type: "ISSUE_FORM_OPEN",
|
|
2461
|
+
mode: "edit",
|
|
2462
|
+
id: di.issue.identifier,
|
|
2463
|
+
title: di.issue.title,
|
|
2464
|
+
description: di.issue.description ?? "",
|
|
2465
|
+
});
|
|
2466
|
+
return;
|
|
2467
|
+
}
|
|
2468
|
+
if (input === "d") {
|
|
2469
|
+
if (!canMutate) {
|
|
2470
|
+
dispatch({ type: "SET_ACTION_MESSAGE", message: "Nothing to delete here" });
|
|
2471
|
+
return;
|
|
2472
|
+
}
|
|
2473
|
+
dispatch({ type: "ISSUE_DELETE_OPEN" });
|
|
2474
|
+
return;
|
|
2475
|
+
}
|
|
2476
|
+
if (input === "o") {
|
|
2477
|
+
if (!di.issue.url) {
|
|
2478
|
+
dispatch({ type: "SET_ACTION_MESSAGE", message: "No issue URL available" });
|
|
2479
|
+
return;
|
|
2480
|
+
}
|
|
2481
|
+
if (openUrl(di.issue.url)) {
|
|
2482
|
+
dispatch({ type: "SET_ACTION_MESSAGE", message: "Opened in browser" });
|
|
2483
|
+
}
|
|
2484
|
+
return;
|
|
2485
|
+
}
|
|
2486
|
+
return; // backlog has no worktree/PR actions
|
|
2487
|
+
}
|
|
2488
|
+
// ── Trees tab: worktree-in-progress actions ─────────────────
|
|
2107
2489
|
// Work
|
|
2108
2490
|
if (input === "w") {
|
|
2109
2491
|
if (di.worktree?.sessionId) {
|
|
@@ -2323,6 +2705,10 @@ export default function Dashboard() {
|
|
|
2323
2705
|
}
|
|
2324
2706
|
}, {
|
|
2325
2707
|
isActive: state.overlay !== "context-input" &&
|
|
2708
|
+
// Issue form title/description are owned by MultilineTextArea;
|
|
2709
|
+
// only the "saving" phase needs the outer handler (a no-op
|
|
2710
|
+
// swallow), so disabling it for the whole overlay is fine.
|
|
2711
|
+
state.overlay !== "issue-form" &&
|
|
2326
2712
|
(state.overlay !== "pr-create" || state.prCreatePhase !== "review") &&
|
|
2327
2713
|
(state.overlay !== "commit" || state.commitPhase !== "awaiting-message"),
|
|
2328
2714
|
});
|
|
@@ -2333,9 +2719,29 @@ export default function Dashboard() {
|
|
|
2333
2719
|
if (state.error) {
|
|
2334
2720
|
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
2721
|
}
|
|
2336
|
-
|
|
2722
|
+
// The active issue/tree row drives the detail pane and action row.
|
|
2723
|
+
const selectedIssue = (state.activeTab === "trees"
|
|
2724
|
+
? state.flatTrees[state.treeSelectedIndex]
|
|
2725
|
+
: state.flatIssues[state.selectedIndex]) ?? null;
|
|
2337
2726
|
const selectedReview = state.flatReviews[state.reviewSelectedIndex] ?? null;
|
|
2338
|
-
|
|
2727
|
+
const activeTracker = getIssueTracker(repoRootRef.current);
|
|
2728
|
+
return (_jsxs(Box, { width: columns, height: rows, flexDirection: "column", children: [_jsxs(Box, { paddingX: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "santree" }), _jsxs(Text, { dimColor: true, children: [" ", "v", version] }), updateAvailable && latestVersion ? (_jsxs(Text, { color: "yellow", children: [" ⬆ v", latestVersion, " available — `santree update`"] })) : null, CLAUDE_VERSION ? (_jsxs(Text, { dimColor: true, children: [" · claude ", CLAUDE_VERSION] })) : null, claudeUpdateAvailable && latestClaudeVersion ? (_jsxs(Text, { color: "yellow", children: [" ⬆ ", latestClaudeVersion] })) : null, state.refreshing ? (_jsxs(Text, { dimColor: true, children: [" · ", _jsx(Spinner, { type: "dots" }), " refreshing"] })) : null, state.actionMessage ? (_jsxs(Text, { color: "yellow", children: [" · ", state.actionMessage] })) : null] }), _jsxs(Box, { paddingX: 1, children: [_jsx(Tab, { active: state.activeTab === "issues", label: `1 Issues (${state.flatIssues.length})`, mode: theme.mode }), _jsx(Text, { children: " " }), _jsx(Tab, { active: state.activeTab === "trees", label: `2 Trees (${state.flatTrees.length})`, mode: theme.mode }), _jsx(Text, { children: " " }), _jsx(Tab, { active: state.activeTab === "reviews", label: `3 Reviews (${state.flatReviews.length})`, mode: theme.mode })] }), _jsxs(Box, { flexGrow: 1, borderStyle: "round", borderColor: "cyan", flexDirection: "column", children: [state.overlay === "help" ? (_jsx(HelpOverlay, { width: innerWidth, height: contentHeight })) : state.overlay === "tracker-select" ? (_jsx(Box, { flexGrow: 1, justifyContent: "center", alignItems: "center", children: _jsx(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 3, paddingY: 1, children: state.trackerSelectPhase === "linear-org" ? (_jsxs(_Fragment, { children: [_jsx(Text, { bold: true, children: "Select a Linear workspace:" }), _jsx(Text, { children: " " }), state.trackerSelectOrgs.map((org, i) => {
|
|
2729
|
+
const sel = i === state.trackerSelectIndex;
|
|
2730
|
+
return (_jsxs(Text, { color: sel ? "cyan" : undefined, bold: sel, children: [sel ? "> " : " ", org.name, " (", org.slug, ")"] }, org.slug));
|
|
2731
|
+
}), _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: " " }), [
|
|
2732
|
+
{ label: "Local", hint: "built-in, file-based — no account needed" },
|
|
2733
|
+
{ label: "Linear", hint: "OAuth workspace" },
|
|
2734
|
+
{ label: "GitHub", hint: "GitHub Issues via gh CLI" },
|
|
2735
|
+
].map((t, i) => {
|
|
2736
|
+
const sel = i === state.trackerSelectIndex;
|
|
2737
|
+
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));
|
|
2738
|
+
}), _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"
|
|
2739
|
+
? "title"
|
|
2740
|
+
: state.issueFormPhase === "description"
|
|
2741
|
+
? "description"
|
|
2742
|
+
: "saving…"] }), state.issueFormError ? (_jsx(Text, { color: "red", children: state.issueFormError })) : (_jsx(Text, { dimColor: true, children: state.issueFormPhase === "title"
|
|
2743
|
+
? "First line is the title"
|
|
2744
|
+
: "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
2745
|
const mode = state.contextInputMode;
|
|
2340
2746
|
const ctx = state.contextInputValue;
|
|
2341
2747
|
dispatch({ type: "CONTEXT_INPUT_DONE" });
|
|
@@ -2346,17 +2752,22 @@ export default function Dashboard() {
|
|
|
2346
2752
|
const defaultBranch = getDefaultBranch();
|
|
2347
2753
|
const label = branch === defaultBranch ? `${branch} (default)` : branch;
|
|
2348
2754
|
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.
|
|
2755
|
+
}), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: "j/k to navigate, Enter to select, ESC to cancel" })] }) })) : state.overlay === "confirm-delete" ? (_jsx(Box, { flexGrow: 1, justifyContent: "center", alignItems: "center", children: _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "red", paddingX: 3, paddingY: 1, children: [_jsx(Text, { bold: true, color: "red", children: "Remove worktree?" }), _jsx(Text, { children: " " }), _jsx(Text, { children: selectedIssue?.worktree?.branch ?? "" }), selectedIssue?.worktree?.dirty && (_jsx(Text, { color: "yellow", children: "Warning: worktree has uncommitted changes" })), _jsx(Text, { children: " " }), _jsxs(Text, { children: [_jsx(Text, { color: "red", bold: true, children: "y" }), " Confirm"] }), _jsxs(Text, { children: [_jsx(Text, { color: "cyan", bold: true, children: "n" }), " Cancel"] })] }) })) : state.overlay === "diff" ? (_jsx(DiffOverlay, { width: innerWidth, height: contentHeight, ticketId: state.diffTicketId ?? "", baseBranch: state.diffBaseBranch ?? "", files: state.diffFiles, fileIndex: state.diffFileIndex, fileScrollOffset: state.diffFileScrollOffset, content: state.diffContent, contentScrollOffset: state.diffContentScrollOffset, loadingFiles: state.diffLoadingFiles, loadingContent: state.diffLoadingContent, error: state.diffError, selectionBg: theme.selectionBg, leftWidthOverride: diffLeftWidth ?? undefined, pendingDiscard: state.diffPendingDiscard })) : state.overlay === "confirm-setup" ? (_jsx(Box, { flexGrow: 1, justifyContent: "center", alignItems: "center", children: _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 3, paddingY: 1, children: [_jsx(Text, { bold: true, children: "Run setup script?" }), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: ".santree/init.sh" }), _jsx(Text, { children: " " }), _jsxs(Text, { children: [_jsx(Text, { color: "green", bold: true, children: "y" }), " Run setup"] }), _jsxs(Text, { children: [_jsx(Text, { color: "yellow", bold: true, children: "n" }), " Skip"] })] }) })) : (_jsxs(Box, { flexGrow: 1, children: [_jsx(Box, { width: leftWidth, children: state.activeTab === "reviews" ? (_jsx(ReviewList, { flatReviews: state.flatReviews, selectedIndex: state.reviewSelectedIndex, scrollOffset: state.reviewListScrollOffset, height: contentHeight, width: leftWidth, selectionBg: theme.selectionBg })) : (state.activeTab === "trees" ? state.flatTrees : state.flatIssues).length ===
|
|
2756
|
+
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
|
+
? 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
|
|
2350
2759
|
.split("\n")
|
|
2351
2760
|
.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.
|
|
2761
|
+
.map((line, i) => (_jsx(Box, { children: _jsx(Text, { dimColor: true, children: line }) }, i)))] })) : state.activeTab === "reviews" ? (_jsx(ReviewDetailPanel, { item: selectedReview, scrollOffset: state.reviewDetailScrollOffset, height: contentHeight, width: rightWidth })) : state.overlay === "commit" ? (_jsx(CommitOverlay, { width: rightWidth, height: contentHeight, branch: state.commitBranch, ticketId: state.commitTicketId, gitStatus: state.commitGitStatus, phase: state.commitPhase, message: state.commitMessage, error: state.commitError, dispatch: dispatch, onSubmit: handleCommitSubmit })) : state.overlay === "pr-create" ? (_jsx(PrCreateOverlay, { width: rightWidth, height: contentHeight, branch: state.prCreateBranch, ticketId: state.prCreateTicketId, phase: state.prCreatePhase, error: state.prCreateError, url: state.prCreateUrl, body: state.prCreateBody, title: state.prCreateTitle, draft: state.prCreateDraft, dispatch: dispatch })) : (_jsx(DetailPanel, { issue: selectedIssue, scrollOffset: state.activeTab === "trees"
|
|
2762
|
+
? state.treeDetailScrollOffset
|
|
2763
|
+
: state.detailScrollOffset, height: contentHeight, width: rightWidth, creatingForTicket: state.creatingForTicket, creationLogs: state.creationLogs })) })] })), _jsx(Box, { children: state.overlay === "diff" ? (_jsx(Box, { width: innerWidth, paddingX: 1, children: _jsx(CommandBar, { showWorkspace: hasWorkspaceFile, mode: "diff" }) })) : (_jsxs(_Fragment, { children: [_jsx(Box, { width: leftWidth + separatorWidth, paddingX: 1, children: _jsx(CommandBar, { showWorkspace: hasWorkspaceFile, mode: "default" }) }), _jsx(Box, { width: rightWidth, children: _jsx(ActionRow, { activeTab: state.activeTab, selectedIssue: selectedIssue, selectedReview: selectedReview, overlay: state.overlay, trackerName: activeTracker.displayName, canMutate: activeTracker.canMutate === true }) })] })) })] })] }));
|
|
2353
2764
|
}
|
|
2354
2765
|
/**
|
|
2355
2766
|
* Renders the per-issue action key hints (Resume / Editor / View diff / …)
|
|
2356
2767
|
* lifted out of the detail panels so they sit on the same row as the global
|
|
2357
2768
|
* command bar. Empty when nothing is selected.
|
|
2358
2769
|
*/
|
|
2359
|
-
function ActionRow({ activeTab, selectedIssue, selectedReview, overlay, trackerName, }) {
|
|
2770
|
+
function ActionRow({ activeTab, selectedIssue, selectedReview, overlay, trackerName, canMutate, }) {
|
|
2360
2771
|
// During the diff overlay, none of the per-issue actions apply (View diff
|
|
2361
2772
|
// is what got us here, Commit/PR/etc. need the detail panel context). Keep
|
|
2362
2773
|
// the row blank so the diff-specific CommandBar reads cleanly.
|
|
@@ -2367,7 +2778,7 @@ function ActionRow({ activeTab, selectedIssue, selectedReview, overlay, trackerN
|
|
|
2367
2778
|
? buildReviewActions(selectedReview)
|
|
2368
2779
|
: []
|
|
2369
2780
|
: selectedIssue
|
|
2370
|
-
? buildIssueActions(selectedIssue, trackerName)
|
|
2781
|
+
? buildIssueActions(selectedIssue, trackerName, { tab: activeTab, canMutate })
|
|
2371
2782
|
: [];
|
|
2372
2783
|
if (items.length === 0)
|
|
2373
2784
|
return _jsx(Text, { children: " " });
|