santree 0.2.8 → 0.2.9
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.
|
@@ -35,6 +35,15 @@ function slugify(title) {
|
|
|
35
35
|
.slice(0, 40);
|
|
36
36
|
}
|
|
37
37
|
// ── Scroll helpers ────────────────────────────────────────────────────
|
|
38
|
+
function countWithChildren(di) {
|
|
39
|
+
let count = 1;
|
|
40
|
+
if (di.children) {
|
|
41
|
+
for (const child of di.children) {
|
|
42
|
+
count += countWithChildren(child);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return count;
|
|
46
|
+
}
|
|
38
47
|
function getRowIndexForFlatIndex(groups, flatIndex) {
|
|
39
48
|
let row = 1; // skip column header row
|
|
40
49
|
let issuesSeen = 0;
|
|
@@ -42,11 +51,14 @@ function getRowIndexForFlatIndex(groups, flatIndex) {
|
|
|
42
51
|
row++; // project header
|
|
43
52
|
for (const sg of g.statusGroups) {
|
|
44
53
|
row++; // status header
|
|
45
|
-
for (
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
54
|
+
for (const di of sg.issues) {
|
|
55
|
+
const total = countWithChildren(di);
|
|
56
|
+
if (flatIndex >= issuesSeen && flatIndex < issuesSeen + total) {
|
|
57
|
+
// The target is within this issue or its children
|
|
58
|
+
return row + (flatIndex - issuesSeen);
|
|
59
|
+
}
|
|
60
|
+
row += total;
|
|
61
|
+
issuesSeen += total;
|
|
50
62
|
}
|
|
51
63
|
}
|
|
52
64
|
}
|
|
@@ -65,11 +77,13 @@ function getFlatIndexForListRow(groups, listRow) {
|
|
|
65
77
|
if (row === listRow)
|
|
66
78
|
return null; // status header row
|
|
67
79
|
row++;
|
|
68
|
-
for (
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
80
|
+
for (const di of sg.issues) {
|
|
81
|
+
const total = countWithChildren(di);
|
|
82
|
+
if (listRow >= row && listRow < row + total) {
|
|
83
|
+
return issuesSeen + (listRow - row);
|
|
84
|
+
}
|
|
85
|
+
row += total;
|
|
86
|
+
issuesSeen += total;
|
|
73
87
|
}
|
|
74
88
|
}
|
|
75
89
|
}
|
|
@@ -359,7 +373,7 @@ export default function Dashboard() {
|
|
|
359
373
|
exit();
|
|
360
374
|
}
|
|
361
375
|
}, [exit, refresh]);
|
|
362
|
-
const createAndLaunch = useCallback(async (mode, runSetup) => {
|
|
376
|
+
const createAndLaunch = useCallback(async (mode, runSetup, base) => {
|
|
363
377
|
const di = stateRef.current.flatIssues[stateRef.current.selectedIndex];
|
|
364
378
|
if (!di)
|
|
365
379
|
return;
|
|
@@ -373,24 +387,32 @@ export default function Dashboard() {
|
|
|
373
387
|
dispatch({ type: "CREATION_START", ticketId });
|
|
374
388
|
const slug = slugify(di.issue.title);
|
|
375
389
|
const branchName = `feature/${ticketId}-${slug}`;
|
|
376
|
-
const
|
|
390
|
+
const defaultBranch = getDefaultBranch();
|
|
391
|
+
const baseBranch = base ?? defaultBranch;
|
|
392
|
+
const isDefaultBase = baseBranch === defaultBranch;
|
|
377
393
|
// 1. Pull latest (async to avoid blocking the event loop)
|
|
378
394
|
dispatch({ type: "CREATION_LOG", logs: `Fetching origin...\n` });
|
|
379
395
|
try {
|
|
380
396
|
await execAsync("git fetch origin", { cwd: repoRoot });
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
397
|
+
if (isDefaultBase) {
|
|
398
|
+
// Only checkout + pull for default branch (can't checkout a branch with an active worktree)
|
|
399
|
+
dispatch({ type: "CREATION_LOG", logs: `Checking out ${baseBranch}...\n` });
|
|
400
|
+
await execAsync(`git checkout ${baseBranch}`, { cwd: repoRoot });
|
|
401
|
+
dispatch({ type: "CREATION_LOG", logs: `Pulling ${baseBranch}...\n` });
|
|
402
|
+
await execAsync(`git pull origin ${baseBranch}`, { cwd: repoRoot });
|
|
403
|
+
}
|
|
404
|
+
dispatch({ type: "CREATION_LOG", logs: `Pulled latest ${baseBranch}\n` });
|
|
386
405
|
}
|
|
387
406
|
catch (e) {
|
|
388
407
|
const msg = e instanceof Error ? e.message : "Failed to pull latest";
|
|
389
408
|
dispatch({ type: "CREATION_LOG", logs: `Warning: ${msg}\n` });
|
|
390
409
|
}
|
|
391
410
|
// 2. Create worktree
|
|
392
|
-
dispatch({
|
|
393
|
-
|
|
411
|
+
dispatch({
|
|
412
|
+
type: "CREATION_LOG",
|
|
413
|
+
logs: `Creating worktree ${branchName} from ${baseBranch}...\n`,
|
|
414
|
+
});
|
|
415
|
+
const result = await createWorktree(branchName, baseBranch, repoRoot);
|
|
394
416
|
if (!result.success || !result.path) {
|
|
395
417
|
dispatch({ type: "CREATION_ERROR", error: result.error ?? "Unknown error" });
|
|
396
418
|
dispatch({
|
|
@@ -446,6 +468,16 @@ export default function Dashboard() {
|
|
|
446
468
|
dispatch({ type: "CREATION_DONE" });
|
|
447
469
|
launchAfterCreation(mode, result.path, ticketId);
|
|
448
470
|
}, [launchAfterCreation]);
|
|
471
|
+
const proceedAfterBaseSelect = useCallback((mode, base) => {
|
|
472
|
+
const repoRoot = repoRootRef.current;
|
|
473
|
+
if (!repoRoot)
|
|
474
|
+
return;
|
|
475
|
+
if (hasInitScript(repoRoot)) {
|
|
476
|
+
dispatch({ type: "SETUP_CONFIRM_SHOW", mode });
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
createAndLaunch(mode, false, base);
|
|
480
|
+
}, [createAndLaunch]);
|
|
449
481
|
const doWork = useCallback((mode) => {
|
|
450
482
|
const di = state.flatIssues[state.selectedIndex];
|
|
451
483
|
if (!di)
|
|
@@ -467,15 +499,25 @@ export default function Dashboard() {
|
|
|
467
499
|
}
|
|
468
500
|
}
|
|
469
501
|
else {
|
|
470
|
-
// No worktree —
|
|
471
|
-
|
|
502
|
+
// No worktree — collect possible base branches
|
|
503
|
+
const defaultBranch = getDefaultBranch();
|
|
504
|
+
const baseOptions = [defaultBranch];
|
|
505
|
+
for (const fi of state.flatIssues) {
|
|
506
|
+
if (fi.worktree && !baseOptions.includes(fi.worktree.branch)) {
|
|
507
|
+
baseOptions.push(fi.worktree.branch);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
if (baseOptions.length > 1) {
|
|
511
|
+
// Store mode in setupMode so we can retrieve it after base selection
|
|
472
512
|
dispatch({ type: "SETUP_CONFIRM_SHOW", mode });
|
|
513
|
+
// Immediately replace overlay with base-select (setupMode is preserved)
|
|
514
|
+
dispatch({ type: "BASE_SELECT_SHOW", options: baseOptions });
|
|
473
515
|
return;
|
|
474
516
|
}
|
|
475
|
-
//
|
|
476
|
-
|
|
517
|
+
// Only default branch available — skip base select
|
|
518
|
+
proceedAfterBaseSelect(mode);
|
|
477
519
|
}
|
|
478
|
-
}, [state.flatIssues, state.selectedIndex, exit, launchWorkInTmux,
|
|
520
|
+
}, [state.flatIssues, state.selectedIndex, exit, launchWorkInTmux, proceedAfterBaseSelect]);
|
|
479
521
|
// ── Commit flow ──────────────────────────────────────────────────
|
|
480
522
|
const handleStageAll = useCallback(async () => {
|
|
481
523
|
const wtPath = stateRef.current.commitWorktreePath;
|
|
@@ -769,17 +811,48 @@ export default function Dashboard() {
|
|
|
769
811
|
}
|
|
770
812
|
return;
|
|
771
813
|
}
|
|
814
|
+
// Base select overlay
|
|
815
|
+
if (state.overlay === "base-select") {
|
|
816
|
+
const opts = state.baseSelectOptions;
|
|
817
|
+
if (input === "j" || key.downArrow) {
|
|
818
|
+
const next = Math.min(state.baseSelectIndex + 1, opts.length - 1);
|
|
819
|
+
dispatch({ type: "BASE_SELECT_MOVE", index: next });
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
if (input === "k" || key.upArrow) {
|
|
823
|
+
const prev = Math.max(state.baseSelectIndex - 1, 0);
|
|
824
|
+
dispatch({ type: "BASE_SELECT_MOVE", index: prev });
|
|
825
|
+
return;
|
|
826
|
+
}
|
|
827
|
+
if (key.return) {
|
|
828
|
+
const chosen = opts[state.baseSelectIndex];
|
|
829
|
+
if (chosen) {
|
|
830
|
+
dispatch({ type: "BASE_SELECT_CONFIRM", chosen });
|
|
831
|
+
const mode = state.setupMode;
|
|
832
|
+
if (mode) {
|
|
833
|
+
proceedAfterBaseSelect(mode, chosen);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
return;
|
|
837
|
+
}
|
|
838
|
+
if (key.escape) {
|
|
839
|
+
dispatch({ type: "BASE_SELECT_DONE" });
|
|
840
|
+
return;
|
|
841
|
+
}
|
|
842
|
+
return;
|
|
843
|
+
}
|
|
772
844
|
// Confirm setup overlay
|
|
773
845
|
if (state.overlay === "confirm-setup") {
|
|
774
846
|
const mode = state.setupMode;
|
|
847
|
+
const base = state.baseSelectChosen ?? undefined;
|
|
775
848
|
if (input === "y" && mode) {
|
|
776
849
|
dispatch({ type: "SETUP_CONFIRM_DONE" });
|
|
777
|
-
createAndLaunch(mode, true);
|
|
850
|
+
createAndLaunch(mode, true, base);
|
|
778
851
|
return;
|
|
779
852
|
}
|
|
780
853
|
if (input === "n" && mode) {
|
|
781
854
|
dispatch({ type: "SETUP_CONFIRM_DONE" });
|
|
782
|
-
createAndLaunch(mode, false);
|
|
855
|
+
createAndLaunch(mode, false, base);
|
|
783
856
|
return;
|
|
784
857
|
}
|
|
785
858
|
if (key.escape) {
|
|
@@ -1073,5 +1146,10 @@ export default function Dashboard() {
|
|
|
1073
1146
|
return (_jsx(Box, { width: columns, height: rows, flexDirection: "column", children: _jsxs(Box, { justifyContent: "center", alignItems: "center", flexGrow: 1, flexDirection: "column", children: [_jsx(Text, { color: "yellow", children: "No active issues assigned to you" }), _jsx(Text, { dimColor: true, children: "Press R to refresh or q to quit" })] }) }));
|
|
1074
1147
|
}
|
|
1075
1148
|
const selectedIssue = state.flatIssues[state.selectedIndex] ?? null;
|
|
1076
|
-
return (_jsxs(Box, { width: columns, height: rows, flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, color: "cyan", children: "Santree Dashboard" }), _jsxs(Text, { dimColor: true, children: [" v", version] }), _jsxs(Text, { dimColor: true, children: [" ", "(", state.flatIssues.length, " issues)", state.refreshing ? " refreshing..." : ""] }), state.actionMessage && (_jsxs(Text, { color: "yellow", children: [" ", state.actionMessage] }))] }), 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 === "
|
|
1149
|
+
return (_jsxs(Box, { width: columns, height: rows, flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, color: "cyan", children: "Santree Dashboard" }), _jsxs(Text, { dimColor: true, children: [" v", version] }), _jsxs(Text, { dimColor: true, children: [" ", "(", state.flatIssues.length, " issues)", state.refreshing ? " refreshing..." : ""] }), state.actionMessage && (_jsxs(Text, { color: "yellow", children: [" ", state.actionMessage] }))] }), 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 === "base-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 base branch:" }), _jsx(Text, { children: " " }), state.baseSelectOptions.map((branch, idx) => {
|
|
1150
|
+
const selected = idx === state.baseSelectIndex;
|
|
1151
|
+
const defaultBranch = getDefaultBranch();
|
|
1152
|
+
const label = branch === defaultBranch ? `${branch} (default)` : branch;
|
|
1153
|
+
return (_jsx(Text, { children: _jsxs(Text, { color: selected ? "cyan" : undefined, bold: selected, children: [selected ? "> " : " ", label] }) }, branch));
|
|
1154
|
+
}), _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 === "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: _jsx(IssueList, { groups: state.groups, flatIssues: state.flatIssues, selectedIndex: state.selectedIndex, scrollOffset: state.listScrollOffset, height: contentHeight, width: leftWidth, creatingForTicket: state.creatingForTicket, deletingForTicket: state.deletingForTicket }) }), _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.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, scrollOffset: state.detailScrollOffset })) : (_jsx(DetailPanel, { issue: selectedIssue, scrollOffset: state.detailScrollOffset, height: contentHeight, width: rightWidth, creatingForTicket: state.creatingForTicket, creationLogs: state.creationLogs })) })] }))] }));
|
|
1077
1155
|
}
|
|
@@ -79,13 +79,26 @@ function buildRows(groups, flatIssues) {
|
|
|
79
79
|
// Build a map from issue identifier to flat index
|
|
80
80
|
const indexMap = new Map();
|
|
81
81
|
flatIssues.forEach((di, i) => indexMap.set(di.issue.identifier, i));
|
|
82
|
+
function pushIssueWithChildren(di, depth) {
|
|
83
|
+
rows.push({
|
|
84
|
+
kind: "issue",
|
|
85
|
+
issue: di,
|
|
86
|
+
flatIndex: indexMap.get(di.issue.identifier) ?? -1,
|
|
87
|
+
depth,
|
|
88
|
+
});
|
|
89
|
+
if (di.children) {
|
|
90
|
+
for (const child of di.children) {
|
|
91
|
+
pushIssueWithChildren(child, depth + 1);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
82
95
|
for (const group of groups) {
|
|
83
96
|
const totalIssues = group.statusGroups.reduce((sum, sg) => sum + sg.issues.length, 0);
|
|
84
97
|
rows.push({ kind: "header", name: group.name, count: totalIssues });
|
|
85
98
|
for (const sg of group.statusGroups) {
|
|
86
99
|
rows.push({ kind: "status-header", name: sg.name, type: sg.type, count: sg.issues.length });
|
|
87
100
|
for (const di of sg.issues) {
|
|
88
|
-
|
|
101
|
+
pushIssueWithChildren(di, 0);
|
|
89
102
|
}
|
|
90
103
|
}
|
|
91
104
|
}
|
|
@@ -115,7 +128,7 @@ export default function IssueList({ groups, flatIssues, selectedIndex, scrollOff
|
|
|
115
128
|
if (row.kind === "status-header") {
|
|
116
129
|
return (_jsx(Box, { children: _jsxs(Text, { color: stateColor(row.type, row.name), dimColor: true, children: [" ", row.name, " (", row.count, ")"] }) }, `sh-${i}`));
|
|
117
130
|
}
|
|
118
|
-
const { issue, flatIndex } = row;
|
|
131
|
+
const { issue, flatIndex, depth } = row;
|
|
119
132
|
const selected = flatIndex === selectedIndex;
|
|
120
133
|
const di = issue;
|
|
121
134
|
const sc = stateColor(di.issue.state.type, di.issue.state.name);
|
|
@@ -126,10 +139,12 @@ export default function IssueList({ groups, flatIssues, selectedIndex, scrollOff
|
|
|
126
139
|
const pr = prIndicator(di.pr);
|
|
127
140
|
const prio = priorityIndicator(di.issue.priority);
|
|
128
141
|
const cursor = selected ? ">" : " ";
|
|
129
|
-
const
|
|
130
|
-
|
|
142
|
+
const nestPrefix = depth > 0 ? " ".repeat(depth - 1) + "└ " : "";
|
|
143
|
+
const adjustedTitleWidth = Math.max(titleMaxWidth - nestPrefix.length, 5);
|
|
144
|
+
const title = di.issue.title.length > adjustedTitleWidth
|
|
145
|
+
? di.issue.title.slice(0, adjustedTitleWidth - 1) + "…"
|
|
131
146
|
: di.issue.title;
|
|
132
147
|
const bg = selected ? "#1e3a5f" : undefined;
|
|
133
|
-
return (_jsxs(Box, { width: width, children: [_jsxs(Text, { backgroundColor: bg, color: selected ? "cyan" : undefined, bold: selected, children: [cursor, " "] }), _jsx(Text, { backgroundColor: bg, color: sc, children: "\u25CF" }), _jsxs(Text, { backgroundColor: bg, color: prio.color, children: [" ", prio.text] }),
|
|
148
|
+
return (_jsxs(Box, { width: width, children: [_jsxs(Text, { backgroundColor: bg, color: selected ? "cyan" : undefined, bold: selected, children: [cursor, " "] }), _jsx(Text, { backgroundColor: bg, color: sc, children: "\u25CF" }), _jsxs(Text, { backgroundColor: bg, color: prio.color, children: [" ", prio.text] }), _jsxs(Text, { backgroundColor: bg, color: selected ? "cyan" : undefined, bold: selected, children: [nestPrefix, di.issue.identifier.padEnd(10)] }), _jsx(Text, { backgroundColor: bg, color: selected ? "white" : undefined, bold: selected, children: title.padEnd(adjustedTitleWidth) }), _jsx(Text, { backgroundColor: bg, color: selected ? (sess.color === "gray" ? "gray" : sess.color) : sess.color, children: sess.text.padStart(sessionColWidth) }), _jsx(Text, { backgroundColor: bg, children: " " }), _jsx(Text, { backgroundColor: bg, color: selected ? (pr.color === "gray" ? "gray" : pr.color) : pr.color, children: pr.text.padStart(prColWidth) }), _jsx(Text, { backgroundColor: bg, children: " " }), _jsx(Text, { backgroundColor: bg, color: selected ? (ci.color === "gray" ? "gray" : ci.color) : ci.color, children: ci.text.padStart(checksColWidth) })] }, di.issue.identifier));
|
|
134
149
|
}) }), _jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: footerRule }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", bold: true, children: "j/k" }), _jsx(Text, { color: "white", children: " Navigate" }), " ", _jsx(Text, { color: "cyan", bold: true, children: "Shift + \u2191\u2193" }), _jsx(Text, { color: "white", children: " Scroll detail" }), " ", _jsx(Text, { color: "cyan", bold: true, children: "E" }), _jsx(Text, { color: "white", children: " Workspace" }), " ", _jsx(Text, { color: "cyan", bold: true, children: "R" }), _jsx(Text, { color: "white", children: " Refresh" }), " ", _jsx(Text, { color: "cyan", bold: true, children: "q" }), _jsx(Text, { color: "white", children: " Quit" })] })] })] }));
|
|
135
150
|
}
|
|
@@ -129,9 +129,37 @@ export async function loadDashboardData(repoRoot) {
|
|
|
129
129
|
reviews: reviewsInfo,
|
|
130
130
|
};
|
|
131
131
|
}));
|
|
132
|
-
//
|
|
132
|
+
// ── Compute parent-child relationships ──────────────────────────
|
|
133
|
+
// Build a map from worktree branch → DashboardIssue
|
|
134
|
+
const allIssues = [...enriched, ...orphans];
|
|
135
|
+
const branchToIssue = new Map();
|
|
136
|
+
for (const di of allIssues) {
|
|
137
|
+
if (di.worktree)
|
|
138
|
+
branchToIssue.set(di.worktree.branch, di);
|
|
139
|
+
}
|
|
140
|
+
// For each issue with a worktree, check if its base_branch matches another issue's branch
|
|
141
|
+
const childTicketIds = new Set();
|
|
142
|
+
for (const di of allIssues) {
|
|
143
|
+
if (!di.worktree)
|
|
144
|
+
continue;
|
|
145
|
+
const ticketId = di.issue.identifier;
|
|
146
|
+
const baseBranch = metadata[ticketId]?.base_branch;
|
|
147
|
+
if (!baseBranch)
|
|
148
|
+
continue; // no custom base = branched from default, not a child
|
|
149
|
+
const parent = branchToIssue.get(baseBranch);
|
|
150
|
+
if (!parent || parent === di)
|
|
151
|
+
continue;
|
|
152
|
+
di.parentTicketId = parent.issue.identifier;
|
|
153
|
+
if (!parent.children)
|
|
154
|
+
parent.children = [];
|
|
155
|
+
parent.children.push(di);
|
|
156
|
+
childTicketIds.add(ticketId);
|
|
157
|
+
}
|
|
158
|
+
// Group by project (excluding children — they'll appear nested under parents)
|
|
133
159
|
const groupMap = new Map();
|
|
134
160
|
for (const di of enriched) {
|
|
161
|
+
if (childTicketIds.has(di.issue.identifier))
|
|
162
|
+
continue;
|
|
135
163
|
const key = di.issue.projectName ?? "No Project";
|
|
136
164
|
const list = groupMap.get(key) ?? [];
|
|
137
165
|
list.push(di);
|
|
@@ -169,8 +197,9 @@ export async function loadDashboardData(repoRoot) {
|
|
|
169
197
|
statusGroups,
|
|
170
198
|
};
|
|
171
199
|
});
|
|
172
|
-
// Append orphaned worktrees as a separate group at the bottom
|
|
173
|
-
|
|
200
|
+
// Append orphaned worktrees as a separate group at the bottom (excluding children)
|
|
201
|
+
const topLevelOrphans = orphans.filter((di) => !childTicketIds.has(di.issue.identifier));
|
|
202
|
+
if (topLevelOrphans.length > 0) {
|
|
174
203
|
groups.push({
|
|
175
204
|
name: "Orphaned Worktrees",
|
|
176
205
|
id: null,
|
|
@@ -178,11 +207,21 @@ export async function loadDashboardData(repoRoot) {
|
|
|
178
207
|
{
|
|
179
208
|
name: "Orphaned",
|
|
180
209
|
type: "orphaned",
|
|
181
|
-
issues:
|
|
210
|
+
issues: topLevelOrphans,
|
|
182
211
|
},
|
|
183
212
|
],
|
|
184
213
|
});
|
|
185
214
|
}
|
|
186
|
-
|
|
215
|
+
// Flatten with children inserted right after their parent
|
|
216
|
+
function flattenWithChildren(di) {
|
|
217
|
+
const result = [di];
|
|
218
|
+
if (di.children) {
|
|
219
|
+
for (const child of di.children) {
|
|
220
|
+
result.push(...flattenWithChildren(child));
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return result;
|
|
224
|
+
}
|
|
225
|
+
const flatIssues = groups.flatMap((g) => g.statusGroups.flatMap((sg) => sg.issues.flatMap(flattenWithChildren)));
|
|
187
226
|
return { groups, flatIssues };
|
|
188
227
|
}
|
|
@@ -30,6 +30,8 @@ export interface DashboardIssue {
|
|
|
30
30
|
pr: PRInfo | null;
|
|
31
31
|
checks: PRCheck[] | null;
|
|
32
32
|
reviews: PRReview[] | null;
|
|
33
|
+
parentTicketId?: string;
|
|
34
|
+
children?: DashboardIssue[];
|
|
33
35
|
}
|
|
34
36
|
export interface StatusGroup {
|
|
35
37
|
name: string;
|
|
@@ -41,7 +43,7 @@ export interface ProjectGroup {
|
|
|
41
43
|
id: string | null;
|
|
42
44
|
statusGroups: StatusGroup[];
|
|
43
45
|
}
|
|
44
|
-
export type ActionOverlay = "mode-select" | "confirm-delete" | "confirm-setup" | "commit" | "pr-create" | null;
|
|
46
|
+
export type ActionOverlay = "mode-select" | "base-select" | "confirm-delete" | "confirm-setup" | "commit" | "pr-create" | null;
|
|
45
47
|
export type CommitPhase = "idle" | "confirm-stage" | "awaiting-message" | "committing" | "pushing" | "done" | "error";
|
|
46
48
|
export type PrCreatePhase = "idle" | "choose-mode" | "pushing" | "filling" | "review" | "creating" | "done" | "error";
|
|
47
49
|
export interface DashboardState {
|
|
@@ -75,6 +77,9 @@ export interface DashboardState {
|
|
|
75
77
|
prCreateBody: string | null;
|
|
76
78
|
prCreateTitle: string | null;
|
|
77
79
|
setupMode: "plan" | "implement" | null;
|
|
80
|
+
baseSelectOptions: string[];
|
|
81
|
+
baseSelectIndex: number;
|
|
82
|
+
baseSelectChosen: string | null;
|
|
78
83
|
}
|
|
79
84
|
export type DashboardAction = {
|
|
80
85
|
type: "SET_DATA";
|
|
@@ -164,6 +169,17 @@ export type DashboardAction = {
|
|
|
164
169
|
mode: "plan" | "implement";
|
|
165
170
|
} | {
|
|
166
171
|
type: "SETUP_CONFIRM_DONE";
|
|
172
|
+
} | {
|
|
173
|
+
type: "BASE_SELECT_SHOW";
|
|
174
|
+
options: string[];
|
|
175
|
+
} | {
|
|
176
|
+
type: "BASE_SELECT_MOVE";
|
|
177
|
+
index: number;
|
|
178
|
+
} | {
|
|
179
|
+
type: "BASE_SELECT_CONFIRM";
|
|
180
|
+
chosen: string;
|
|
181
|
+
} | {
|
|
182
|
+
type: "BASE_SELECT_DONE";
|
|
167
183
|
};
|
|
168
184
|
export declare const initialState: DashboardState;
|
|
169
185
|
export declare function reducer(state: DashboardState, action: DashboardAction): DashboardState;
|
|
@@ -30,6 +30,9 @@ export const initialState = {
|
|
|
30
30
|
prCreateBody: null,
|
|
31
31
|
prCreateTitle: null,
|
|
32
32
|
setupMode: null,
|
|
33
|
+
baseSelectOptions: [],
|
|
34
|
+
baseSelectIndex: 0,
|
|
35
|
+
baseSelectChosen: null,
|
|
33
36
|
};
|
|
34
37
|
export function reducer(state, action) {
|
|
35
38
|
switch (action.type) {
|
|
@@ -81,9 +84,21 @@ export function reducer(state, action) {
|
|
|
81
84
|
case "CREATION_LOG":
|
|
82
85
|
return { ...state, creationLogs: state.creationLogs + action.logs };
|
|
83
86
|
case "CREATION_DONE":
|
|
84
|
-
return {
|
|
87
|
+
return {
|
|
88
|
+
...state,
|
|
89
|
+
creatingForTicket: null,
|
|
90
|
+
creationLogs: "",
|
|
91
|
+
creationError: null,
|
|
92
|
+
baseSelectChosen: null,
|
|
93
|
+
};
|
|
85
94
|
case "CREATION_ERROR":
|
|
86
|
-
return {
|
|
95
|
+
return {
|
|
96
|
+
...state,
|
|
97
|
+
creationError: action.error,
|
|
98
|
+
creatingForTicket: null,
|
|
99
|
+
creationLogs: "",
|
|
100
|
+
baseSelectChosen: null,
|
|
101
|
+
};
|
|
87
102
|
case "DELETE_START":
|
|
88
103
|
return { ...state, deletingForTicket: action.ticketId };
|
|
89
104
|
case "DELETE_DONE":
|
|
@@ -176,6 +191,29 @@ export function reducer(state, action) {
|
|
|
176
191
|
overlay: null,
|
|
177
192
|
setupMode: null,
|
|
178
193
|
};
|
|
194
|
+
case "BASE_SELECT_SHOW":
|
|
195
|
+
return {
|
|
196
|
+
...state,
|
|
197
|
+
overlay: "base-select",
|
|
198
|
+
baseSelectOptions: action.options,
|
|
199
|
+
baseSelectIndex: 0,
|
|
200
|
+
baseSelectChosen: null,
|
|
201
|
+
};
|
|
202
|
+
case "BASE_SELECT_MOVE":
|
|
203
|
+
return { ...state, baseSelectIndex: action.index };
|
|
204
|
+
case "BASE_SELECT_CONFIRM":
|
|
205
|
+
return {
|
|
206
|
+
...state,
|
|
207
|
+
overlay: null,
|
|
208
|
+
baseSelectChosen: action.chosen,
|
|
209
|
+
};
|
|
210
|
+
case "BASE_SELECT_DONE":
|
|
211
|
+
return {
|
|
212
|
+
...state,
|
|
213
|
+
overlay: null,
|
|
214
|
+
baseSelectOptions: [],
|
|
215
|
+
baseSelectIndex: 0,
|
|
216
|
+
};
|
|
179
217
|
default:
|
|
180
218
|
return state;
|
|
181
219
|
}
|