santree 0.2.8 → 0.2.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/dashboard.js +390 -40
- package/dist/lib/ai.js +5 -1
- package/dist/lib/dashboard/IssueList.js +20 -5
- package/dist/lib/dashboard/ReviewDetailPanel.d.ts +9 -0
- package/dist/lib/dashboard/ReviewDetailPanel.js +166 -0
- package/dist/lib/dashboard/ReviewList.d.ts +11 -0
- package/dist/lib/dashboard/ReviewList.js +53 -0
- package/dist/lib/dashboard/data.d.ts +4 -1
- package/dist/lib/dashboard/data.js +113 -6
- package/dist/lib/dashboard/types.d.ts +53 -2
- package/dist/lib/dashboard/types.js +68 -2
- package/dist/lib/github.d.ts +41 -0
- package/dist/lib/github.js +55 -0
- package/package.json +1 -1
|
@@ -17,12 +17,25 @@ import { getPRTemplate } from "../lib/github.js";
|
|
|
17
17
|
import { renderPrompt, renderDiff } from "../lib/prompts.js";
|
|
18
18
|
import * as os from "os";
|
|
19
19
|
import { initialState, reducer } from "../lib/dashboard/types.js";
|
|
20
|
-
import { loadDashboardData } from "../lib/dashboard/data.js";
|
|
20
|
+
import { loadDashboardData, loadReviewsData } from "../lib/dashboard/data.js";
|
|
21
21
|
import IssueList from "../lib/dashboard/IssueList.js";
|
|
22
22
|
import DetailPanel from "../lib/dashboard/DetailPanel.js";
|
|
23
|
+
import ReviewList from "../lib/dashboard/ReviewList.js";
|
|
24
|
+
import ReviewDetailPanel from "../lib/dashboard/ReviewDetailPanel.js";
|
|
23
25
|
import { CommitOverlay, PrCreateOverlay } from "../lib/dashboard/Overlays.js";
|
|
24
26
|
export const description = "Interactive dashboard of your Linear issues";
|
|
25
27
|
const execAsync = promisify(exec);
|
|
28
|
+
const CLAUDE_VERSION = (() => {
|
|
29
|
+
const bin = path.join(os.homedir(), ".claude", "local", "claude");
|
|
30
|
+
try {
|
|
31
|
+
return (execSync(`${bin} --version`, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] })
|
|
32
|
+
.trim()
|
|
33
|
+
.split(" ")[0] ?? "");
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return "";
|
|
37
|
+
}
|
|
38
|
+
})();
|
|
26
39
|
// ── Helpers ───────────────────────────────────────────────────────────
|
|
27
40
|
function isInTmux() {
|
|
28
41
|
return !!process.env.TMUX;
|
|
@@ -35,6 +48,15 @@ function slugify(title) {
|
|
|
35
48
|
.slice(0, 40);
|
|
36
49
|
}
|
|
37
50
|
// ── Scroll helpers ────────────────────────────────────────────────────
|
|
51
|
+
function countWithChildren(di) {
|
|
52
|
+
let count = 1;
|
|
53
|
+
if (di.children) {
|
|
54
|
+
for (const child of di.children) {
|
|
55
|
+
count += countWithChildren(child);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return count;
|
|
59
|
+
}
|
|
38
60
|
function getRowIndexForFlatIndex(groups, flatIndex) {
|
|
39
61
|
let row = 1; // skip column header row
|
|
40
62
|
let issuesSeen = 0;
|
|
@@ -42,11 +64,14 @@ function getRowIndexForFlatIndex(groups, flatIndex) {
|
|
|
42
64
|
row++; // project header
|
|
43
65
|
for (const sg of g.statusGroups) {
|
|
44
66
|
row++; // status header
|
|
45
|
-
for (
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
67
|
+
for (const di of sg.issues) {
|
|
68
|
+
const total = countWithChildren(di);
|
|
69
|
+
if (flatIndex >= issuesSeen && flatIndex < issuesSeen + total) {
|
|
70
|
+
// The target is within this issue or its children
|
|
71
|
+
return row + (flatIndex - issuesSeen);
|
|
72
|
+
}
|
|
73
|
+
row += total;
|
|
74
|
+
issuesSeen += total;
|
|
50
75
|
}
|
|
51
76
|
}
|
|
52
77
|
}
|
|
@@ -65,11 +90,13 @@ function getFlatIndexForListRow(groups, listRow) {
|
|
|
65
90
|
if (row === listRow)
|
|
66
91
|
return null; // status header row
|
|
67
92
|
row++;
|
|
68
|
-
for (
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
93
|
+
for (const di of sg.issues) {
|
|
94
|
+
const total = countWithChildren(di);
|
|
95
|
+
if (listRow >= row && listRow < row + total) {
|
|
96
|
+
return issuesSeen + (listRow - row);
|
|
97
|
+
}
|
|
98
|
+
row += total;
|
|
99
|
+
issuesSeen += total;
|
|
73
100
|
}
|
|
74
101
|
}
|
|
75
102
|
}
|
|
@@ -155,7 +182,7 @@ export default function Dashboard() {
|
|
|
155
182
|
const leftWidthRef = useRef(leftWidth);
|
|
156
183
|
leftWidthRef.current = leftWidth;
|
|
157
184
|
const rightWidth = columns - leftWidth - separatorWidth;
|
|
158
|
-
const contentHeight = rows -
|
|
185
|
+
const contentHeight = rows - 2; // 2 header rows (tabs + version)
|
|
159
186
|
const LIST_FOOTER_HEIGHT = 2;
|
|
160
187
|
// ── Data loading ──────────────────────────────────────────────────
|
|
161
188
|
const refresh = useCallback(async (isInitial = false) => {
|
|
@@ -168,8 +195,12 @@ export default function Dashboard() {
|
|
|
168
195
|
}
|
|
169
196
|
repoRootRef.current = repoRoot;
|
|
170
197
|
try {
|
|
171
|
-
const data = await
|
|
198
|
+
const [data, reviewData] = await Promise.all([
|
|
199
|
+
loadDashboardData(repoRoot),
|
|
200
|
+
loadReviewsData(repoRoot),
|
|
201
|
+
]);
|
|
172
202
|
dispatch({ type: "SET_DATA", ...data });
|
|
203
|
+
dispatch({ type: "SET_REVIEWS_DATA", flatReviews: reviewData.flatReviews });
|
|
173
204
|
}
|
|
174
205
|
catch (e) {
|
|
175
206
|
dispatch({
|
|
@@ -215,6 +246,20 @@ export default function Dashboard() {
|
|
|
215
246
|
const s = stateRef.current;
|
|
216
247
|
const lw = leftWidthRef.current;
|
|
217
248
|
const delta = button === 65 ? 3 : -3;
|
|
249
|
+
if (s.activeTab === "reviews") {
|
|
250
|
+
if (col <= lw) {
|
|
251
|
+
const maxIdx = s.flatReviews.length - 1;
|
|
252
|
+
if (maxIdx < 0)
|
|
253
|
+
return;
|
|
254
|
+
const next = Math.max(0, Math.min(s.reviewSelectedIndex + delta, maxIdx));
|
|
255
|
+
dispatch({ type: "REVIEW_SELECT", index: next });
|
|
256
|
+
}
|
|
257
|
+
else {
|
|
258
|
+
const next = Math.max(0, s.reviewDetailScrollOffset + delta);
|
|
259
|
+
dispatch({ type: "REVIEW_SCROLL_DETAIL", offset: next });
|
|
260
|
+
}
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
218
263
|
if (col <= lw) {
|
|
219
264
|
// Scroll left pane (issue list)
|
|
220
265
|
const maxIdx = s.flatIssues.length - 1;
|
|
@@ -240,9 +285,9 @@ export default function Dashboard() {
|
|
|
240
285
|
draggingRef.current = true;
|
|
241
286
|
return;
|
|
242
287
|
}
|
|
243
|
-
// Left-click press: select
|
|
288
|
+
// Left-click press: select item in left pane
|
|
244
289
|
const s = stateRef.current;
|
|
245
|
-
if (s.loading || s.error
|
|
290
|
+
if (s.loading || s.error)
|
|
246
291
|
return;
|
|
247
292
|
if (col > lw)
|
|
248
293
|
return;
|
|
@@ -250,6 +295,19 @@ export default function Dashboard() {
|
|
|
250
295
|
const contentRow = row - 2; // 0-based row within content area
|
|
251
296
|
if (contentRow < 0)
|
|
252
297
|
return;
|
|
298
|
+
if (s.activeTab === "reviews") {
|
|
299
|
+
if (s.flatReviews.length === 0)
|
|
300
|
+
return;
|
|
301
|
+
// Row 0 is column header, PRs start at row 1
|
|
302
|
+
const listRow = s.reviewListScrollOffset + contentRow;
|
|
303
|
+
const flatIdx = listRow - 1; // subtract column header
|
|
304
|
+
if (flatIdx >= 0 && flatIdx < s.flatReviews.length) {
|
|
305
|
+
dispatch({ type: "REVIEW_SELECT", index: flatIdx });
|
|
306
|
+
}
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
if (s.flatIssues.length === 0)
|
|
310
|
+
return;
|
|
253
311
|
const listRow = s.listScrollOffset + contentRow;
|
|
254
312
|
const flatIdx = getFlatIndexForListRow(s.groups, listRow);
|
|
255
313
|
if (flatIdx !== null && flatIdx >= 0 && flatIdx < s.flatIssues.length) {
|
|
@@ -292,6 +350,22 @@ export default function Dashboard() {
|
|
|
292
350
|
dispatch({ type: "SCROLL_LIST", offset });
|
|
293
351
|
}
|
|
294
352
|
}, [state.selectedIndex, state.groups, contentHeight, state.listScrollOffset]);
|
|
353
|
+
// ── Review list scroll tracking ──────────────────────────────────
|
|
354
|
+
useEffect(() => {
|
|
355
|
+
// Row index = 1 (column header) + flatIndex
|
|
356
|
+
const rowIdx = 1 + state.reviewSelectedIndex;
|
|
357
|
+
const maxVisible = contentHeight - LIST_FOOTER_HEIGHT;
|
|
358
|
+
let offset = state.reviewListScrollOffset;
|
|
359
|
+
if (rowIdx < offset) {
|
|
360
|
+
offset = Math.max(0, rowIdx - 1);
|
|
361
|
+
}
|
|
362
|
+
else if (rowIdx >= offset + maxVisible) {
|
|
363
|
+
offset = rowIdx - maxVisible + 2;
|
|
364
|
+
}
|
|
365
|
+
if (offset !== state.reviewListScrollOffset) {
|
|
366
|
+
dispatch({ type: "REVIEW_SCROLL_LIST", offset });
|
|
367
|
+
}
|
|
368
|
+
}, [state.reviewSelectedIndex, state.flatReviews, contentHeight, state.reviewListScrollOffset]);
|
|
295
369
|
// ── Actions ───────────────────────────────────────────────────────
|
|
296
370
|
const launchWorkInTmux = useCallback((di, mode, worktreePath) => {
|
|
297
371
|
const windowName = di.issue.identifier;
|
|
@@ -359,7 +433,7 @@ export default function Dashboard() {
|
|
|
359
433
|
exit();
|
|
360
434
|
}
|
|
361
435
|
}, [exit, refresh]);
|
|
362
|
-
const createAndLaunch = useCallback(async (mode, runSetup) => {
|
|
436
|
+
const createAndLaunch = useCallback(async (mode, runSetup, base) => {
|
|
363
437
|
const di = stateRef.current.flatIssues[stateRef.current.selectedIndex];
|
|
364
438
|
if (!di)
|
|
365
439
|
return;
|
|
@@ -373,24 +447,32 @@ export default function Dashboard() {
|
|
|
373
447
|
dispatch({ type: "CREATION_START", ticketId });
|
|
374
448
|
const slug = slugify(di.issue.title);
|
|
375
449
|
const branchName = `feature/${ticketId}-${slug}`;
|
|
376
|
-
const
|
|
450
|
+
const defaultBranch = getDefaultBranch();
|
|
451
|
+
const baseBranch = base ?? defaultBranch;
|
|
452
|
+
const isDefaultBase = baseBranch === defaultBranch;
|
|
377
453
|
// 1. Pull latest (async to avoid blocking the event loop)
|
|
378
454
|
dispatch({ type: "CREATION_LOG", logs: `Fetching origin...\n` });
|
|
379
455
|
try {
|
|
380
456
|
await execAsync("git fetch origin", { cwd: repoRoot });
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
457
|
+
if (isDefaultBase) {
|
|
458
|
+
// Only checkout + pull for default branch (can't checkout a branch with an active worktree)
|
|
459
|
+
dispatch({ type: "CREATION_LOG", logs: `Checking out ${baseBranch}...\n` });
|
|
460
|
+
await execAsync(`git checkout ${baseBranch}`, { cwd: repoRoot });
|
|
461
|
+
dispatch({ type: "CREATION_LOG", logs: `Pulling ${baseBranch}...\n` });
|
|
462
|
+
await execAsync(`git pull origin ${baseBranch}`, { cwd: repoRoot });
|
|
463
|
+
}
|
|
464
|
+
dispatch({ type: "CREATION_LOG", logs: `Pulled latest ${baseBranch}\n` });
|
|
386
465
|
}
|
|
387
466
|
catch (e) {
|
|
388
467
|
const msg = e instanceof Error ? e.message : "Failed to pull latest";
|
|
389
468
|
dispatch({ type: "CREATION_LOG", logs: `Warning: ${msg}\n` });
|
|
390
469
|
}
|
|
391
470
|
// 2. Create worktree
|
|
392
|
-
dispatch({
|
|
393
|
-
|
|
471
|
+
dispatch({
|
|
472
|
+
type: "CREATION_LOG",
|
|
473
|
+
logs: `Creating worktree ${branchName} from ${baseBranch}...\n`,
|
|
474
|
+
});
|
|
475
|
+
const result = await createWorktree(branchName, baseBranch, repoRoot);
|
|
394
476
|
if (!result.success || !result.path) {
|
|
395
477
|
dispatch({ type: "CREATION_ERROR", error: result.error ?? "Unknown error" });
|
|
396
478
|
dispatch({
|
|
@@ -446,6 +528,16 @@ export default function Dashboard() {
|
|
|
446
528
|
dispatch({ type: "CREATION_DONE" });
|
|
447
529
|
launchAfterCreation(mode, result.path, ticketId);
|
|
448
530
|
}, [launchAfterCreation]);
|
|
531
|
+
const proceedAfterBaseSelect = useCallback((mode, base) => {
|
|
532
|
+
const repoRoot = repoRootRef.current;
|
|
533
|
+
if (!repoRoot)
|
|
534
|
+
return;
|
|
535
|
+
if (hasInitScript(repoRoot)) {
|
|
536
|
+
dispatch({ type: "SETUP_CONFIRM_SHOW", mode });
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
createAndLaunch(mode, false, base);
|
|
540
|
+
}, [createAndLaunch]);
|
|
449
541
|
const doWork = useCallback((mode) => {
|
|
450
542
|
const di = state.flatIssues[state.selectedIndex];
|
|
451
543
|
if (!di)
|
|
@@ -467,15 +559,25 @@ export default function Dashboard() {
|
|
|
467
559
|
}
|
|
468
560
|
}
|
|
469
561
|
else {
|
|
470
|
-
// No worktree —
|
|
471
|
-
|
|
562
|
+
// No worktree — collect possible base branches
|
|
563
|
+
const defaultBranch = getDefaultBranch();
|
|
564
|
+
const baseOptions = [defaultBranch];
|
|
565
|
+
for (const fi of state.flatIssues) {
|
|
566
|
+
if (fi.worktree && !baseOptions.includes(fi.worktree.branch)) {
|
|
567
|
+
baseOptions.push(fi.worktree.branch);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
if (baseOptions.length > 1) {
|
|
571
|
+
// Store mode in setupMode so we can retrieve it after base selection
|
|
472
572
|
dispatch({ type: "SETUP_CONFIRM_SHOW", mode });
|
|
573
|
+
// Immediately replace overlay with base-select (setupMode is preserved)
|
|
574
|
+
dispatch({ type: "BASE_SELECT_SHOW", options: baseOptions });
|
|
473
575
|
return;
|
|
474
576
|
}
|
|
475
|
-
//
|
|
476
|
-
|
|
577
|
+
// Only default branch available — skip base select
|
|
578
|
+
proceedAfterBaseSelect(mode);
|
|
477
579
|
}
|
|
478
|
-
}, [state.flatIssues, state.selectedIndex, exit, launchWorkInTmux,
|
|
580
|
+
}, [state.flatIssues, state.selectedIndex, exit, launchWorkInTmux, proceedAfterBaseSelect]);
|
|
479
581
|
// ── Commit flow ──────────────────────────────────────────────────
|
|
480
582
|
const handleStageAll = useCallback(async () => {
|
|
481
583
|
const wtPath = stateRef.current.commitWorktreePath;
|
|
@@ -769,17 +871,48 @@ export default function Dashboard() {
|
|
|
769
871
|
}
|
|
770
872
|
return;
|
|
771
873
|
}
|
|
874
|
+
// Base select overlay
|
|
875
|
+
if (state.overlay === "base-select") {
|
|
876
|
+
const opts = state.baseSelectOptions;
|
|
877
|
+
if (input === "j" || key.downArrow) {
|
|
878
|
+
const next = Math.min(state.baseSelectIndex + 1, opts.length - 1);
|
|
879
|
+
dispatch({ type: "BASE_SELECT_MOVE", index: next });
|
|
880
|
+
return;
|
|
881
|
+
}
|
|
882
|
+
if (input === "k" || key.upArrow) {
|
|
883
|
+
const prev = Math.max(state.baseSelectIndex - 1, 0);
|
|
884
|
+
dispatch({ type: "BASE_SELECT_MOVE", index: prev });
|
|
885
|
+
return;
|
|
886
|
+
}
|
|
887
|
+
if (key.return) {
|
|
888
|
+
const chosen = opts[state.baseSelectIndex];
|
|
889
|
+
if (chosen) {
|
|
890
|
+
dispatch({ type: "BASE_SELECT_CONFIRM", chosen });
|
|
891
|
+
const mode = state.setupMode;
|
|
892
|
+
if (mode) {
|
|
893
|
+
proceedAfterBaseSelect(mode, chosen);
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
return;
|
|
897
|
+
}
|
|
898
|
+
if (key.escape) {
|
|
899
|
+
dispatch({ type: "BASE_SELECT_DONE" });
|
|
900
|
+
return;
|
|
901
|
+
}
|
|
902
|
+
return;
|
|
903
|
+
}
|
|
772
904
|
// Confirm setup overlay
|
|
773
905
|
if (state.overlay === "confirm-setup") {
|
|
774
906
|
const mode = state.setupMode;
|
|
907
|
+
const base = state.baseSelectChosen ?? undefined;
|
|
775
908
|
if (input === "y" && mode) {
|
|
776
909
|
dispatch({ type: "SETUP_CONFIRM_DONE" });
|
|
777
|
-
createAndLaunch(mode, true);
|
|
910
|
+
createAndLaunch(mode, true, base);
|
|
778
911
|
return;
|
|
779
912
|
}
|
|
780
913
|
if (input === "n" && mode) {
|
|
781
914
|
dispatch({ type: "SETUP_CONFIRM_DONE" });
|
|
782
|
-
createAndLaunch(mode, false);
|
|
915
|
+
createAndLaunch(mode, false, base);
|
|
783
916
|
return;
|
|
784
917
|
}
|
|
785
918
|
if (key.escape) {
|
|
@@ -840,11 +973,227 @@ export default function Dashboard() {
|
|
|
840
973
|
}
|
|
841
974
|
return;
|
|
842
975
|
}
|
|
976
|
+
// Tab switching (only when no overlay is active)
|
|
977
|
+
if (key.tab) {
|
|
978
|
+
dispatch({
|
|
979
|
+
type: "SET_TAB",
|
|
980
|
+
tab: state.activeTab === "issues" ? "reviews" : "issues",
|
|
981
|
+
});
|
|
982
|
+
return;
|
|
983
|
+
}
|
|
984
|
+
if (input === "1") {
|
|
985
|
+
dispatch({ type: "SET_TAB", tab: "issues" });
|
|
986
|
+
return;
|
|
987
|
+
}
|
|
988
|
+
if (input === "2") {
|
|
989
|
+
dispatch({ type: "SET_TAB", tab: "reviews" });
|
|
990
|
+
return;
|
|
991
|
+
}
|
|
843
992
|
// Quit
|
|
844
993
|
if (input === "q") {
|
|
845
994
|
exit();
|
|
846
995
|
return;
|
|
847
996
|
}
|
|
997
|
+
// Refresh (shared across tabs)
|
|
998
|
+
if (input === "R") {
|
|
999
|
+
refresh();
|
|
1000
|
+
return;
|
|
1001
|
+
}
|
|
1002
|
+
// ── Reviews tab keyboard ─────────────────────────────────
|
|
1003
|
+
if (state.activeTab === "reviews") {
|
|
1004
|
+
const maxReviewIdx = state.flatReviews.length - 1;
|
|
1005
|
+
if (input === "j" || (key.downArrow && !key.shift)) {
|
|
1006
|
+
const next = Math.min(state.reviewSelectedIndex + 1, maxReviewIdx);
|
|
1007
|
+
dispatch({ type: "REVIEW_SELECT", index: next });
|
|
1008
|
+
return;
|
|
1009
|
+
}
|
|
1010
|
+
if (input === "k" || (key.upArrow && !key.shift)) {
|
|
1011
|
+
const prev = Math.max(state.reviewSelectedIndex - 1, 0);
|
|
1012
|
+
dispatch({ type: "REVIEW_SELECT", index: prev });
|
|
1013
|
+
return;
|
|
1014
|
+
}
|
|
1015
|
+
if (key.shift && key.downArrow) {
|
|
1016
|
+
dispatch({
|
|
1017
|
+
type: "REVIEW_SCROLL_DETAIL",
|
|
1018
|
+
offset: state.reviewDetailScrollOffset + 3,
|
|
1019
|
+
});
|
|
1020
|
+
return;
|
|
1021
|
+
}
|
|
1022
|
+
if (key.shift && key.upArrow) {
|
|
1023
|
+
dispatch({
|
|
1024
|
+
type: "REVIEW_SCROLL_DETAIL",
|
|
1025
|
+
offset: Math.max(0, state.reviewDetailScrollOffset - 3),
|
|
1026
|
+
});
|
|
1027
|
+
return;
|
|
1028
|
+
}
|
|
1029
|
+
const ri = state.flatReviews[state.reviewSelectedIndex];
|
|
1030
|
+
if (!ri)
|
|
1031
|
+
return;
|
|
1032
|
+
// Open PR in browser
|
|
1033
|
+
if (input === "o") {
|
|
1034
|
+
if (ri.pr.url) {
|
|
1035
|
+
const openCmd = process.platform === "darwin" ? "open" : "xdg-open";
|
|
1036
|
+
execSync(`${openCmd} "${ri.pr.url}"`, { stdio: "ignore" });
|
|
1037
|
+
dispatch({ type: "SET_ACTION_MESSAGE", message: "Opened PR in browser" });
|
|
1038
|
+
}
|
|
1039
|
+
return;
|
|
1040
|
+
}
|
|
1041
|
+
// Create worktree from PR branch (checkout for local testing)
|
|
1042
|
+
if (input === "w") {
|
|
1043
|
+
if (ri.worktree) {
|
|
1044
|
+
dispatch({ type: "SET_ACTION_MESSAGE", message: "Worktree already exists" });
|
|
1045
|
+
return;
|
|
1046
|
+
}
|
|
1047
|
+
if (!ri.branch) {
|
|
1048
|
+
dispatch({ type: "SET_ACTION_MESSAGE", message: "No branch info" });
|
|
1049
|
+
return;
|
|
1050
|
+
}
|
|
1051
|
+
const repoRoot = repoRootRef.current;
|
|
1052
|
+
if (!repoRoot)
|
|
1053
|
+
return;
|
|
1054
|
+
if (state.creatingForTicket)
|
|
1055
|
+
return;
|
|
1056
|
+
const ticketId = extractTicketId(ri.branch);
|
|
1057
|
+
if (!ticketId) {
|
|
1058
|
+
dispatch({ type: "SET_ACTION_MESSAGE", message: "No ticket ID in branch" });
|
|
1059
|
+
return;
|
|
1060
|
+
}
|
|
1061
|
+
dispatch({ type: "CREATION_START", ticketId });
|
|
1062
|
+
(async () => {
|
|
1063
|
+
try {
|
|
1064
|
+
dispatch({ type: "CREATION_LOG", logs: `Fetching ${ri.branch}...\n` });
|
|
1065
|
+
await execAsync(`git fetch origin ${ri.branch}`, { cwd: repoRoot });
|
|
1066
|
+
dispatch({ type: "CREATION_LOG", logs: `Creating worktree...\n` });
|
|
1067
|
+
const result = await createWorktree(ri.branch, ri.baseBranch ?? getDefaultBranch(), repoRoot);
|
|
1068
|
+
if (!result.success || !result.path) {
|
|
1069
|
+
dispatch({ type: "CREATION_ERROR", error: result.error ?? "Unknown error" });
|
|
1070
|
+
dispatch({ type: "SET_ACTION_MESSAGE", message: `Failed: ${result.error}` });
|
|
1071
|
+
return;
|
|
1072
|
+
}
|
|
1073
|
+
dispatch({ type: "CREATION_LOG", logs: `Worktree at ${result.path}\n` });
|
|
1074
|
+
// Run init script if available
|
|
1075
|
+
if (hasInitScript(repoRoot)) {
|
|
1076
|
+
const initScript = getInitScriptPath(repoRoot);
|
|
1077
|
+
let canExecute = true;
|
|
1078
|
+
try {
|
|
1079
|
+
fs.accessSync(initScript, fs.constants.X_OK);
|
|
1080
|
+
}
|
|
1081
|
+
catch {
|
|
1082
|
+
dispatch({ type: "CREATION_LOG", logs: "init.sh not executable, skipping\n" });
|
|
1083
|
+
canExecute = false;
|
|
1084
|
+
}
|
|
1085
|
+
if (canExecute) {
|
|
1086
|
+
dispatch({ type: "CREATION_LOG", logs: "Running init.sh...\n" });
|
|
1087
|
+
let lastLen = 0;
|
|
1088
|
+
const initResult = await spawnAsync(initScript, [], {
|
|
1089
|
+
cwd: result.path,
|
|
1090
|
+
env: {
|
|
1091
|
+
...process.env,
|
|
1092
|
+
SANTREE_WORKTREE_PATH: result.path,
|
|
1093
|
+
SANTREE_REPO_ROOT: repoRoot,
|
|
1094
|
+
},
|
|
1095
|
+
onOutput: (output) => {
|
|
1096
|
+
const delta = output.slice(lastLen);
|
|
1097
|
+
if (delta)
|
|
1098
|
+
dispatch({ type: "CREATION_LOG", logs: delta });
|
|
1099
|
+
lastLen = output.length;
|
|
1100
|
+
},
|
|
1101
|
+
});
|
|
1102
|
+
if (initResult.code !== 0) {
|
|
1103
|
+
dispatch({
|
|
1104
|
+
type: "CREATION_LOG",
|
|
1105
|
+
logs: `\ninit.sh exited with code ${initResult.code}\n`,
|
|
1106
|
+
});
|
|
1107
|
+
}
|
|
1108
|
+
else {
|
|
1109
|
+
dispatch({ type: "CREATION_LOG", logs: "\nSetup complete!\n" });
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
dispatch({ type: "CREATION_DONE" });
|
|
1114
|
+
dispatch({ type: "SET_ACTION_MESSAGE", message: `Worktree created for ${ticketId}` });
|
|
1115
|
+
// Open in editor automatically
|
|
1116
|
+
const editor = process.env.SANTREE_EDITOR || "code";
|
|
1117
|
+
spawn(editor, [result.path], { detached: true, stdio: "ignore" }).unref();
|
|
1118
|
+
refresh();
|
|
1119
|
+
}
|
|
1120
|
+
catch (e) {
|
|
1121
|
+
dispatch({ type: "CREATION_ERROR", error: e?.message ?? "Failed" });
|
|
1122
|
+
}
|
|
1123
|
+
})();
|
|
1124
|
+
return;
|
|
1125
|
+
}
|
|
1126
|
+
// Open in editor
|
|
1127
|
+
if (input === "e") {
|
|
1128
|
+
if (!ri.worktree) {
|
|
1129
|
+
dispatch({ type: "SET_ACTION_MESSAGE", message: "No worktree (press w to checkout)" });
|
|
1130
|
+
return;
|
|
1131
|
+
}
|
|
1132
|
+
const editor = process.env.SANTREE_EDITOR || "code";
|
|
1133
|
+
spawn(editor, [ri.worktree.path], { detached: true, stdio: "ignore" }).unref();
|
|
1134
|
+
dispatch({ type: "SET_ACTION_MESSAGE", message: `Opened in ${editor}` });
|
|
1135
|
+
return;
|
|
1136
|
+
}
|
|
1137
|
+
// AI Review in tmux
|
|
1138
|
+
if (input === "r") {
|
|
1139
|
+
if (!ri.worktree) {
|
|
1140
|
+
dispatch({
|
|
1141
|
+
type: "SET_ACTION_MESSAGE",
|
|
1142
|
+
message: "No worktree (press w to checkout first)",
|
|
1143
|
+
});
|
|
1144
|
+
return;
|
|
1145
|
+
}
|
|
1146
|
+
if (isInTmux()) {
|
|
1147
|
+
const windowName = `review-${extractTicketId(ri.branch ?? "") ?? ri.pr.number}`;
|
|
1148
|
+
try {
|
|
1149
|
+
execSync(`tmux new-window -n "${windowName}" -c "${ri.worktree.path}"`, {
|
|
1150
|
+
stdio: "ignore",
|
|
1151
|
+
});
|
|
1152
|
+
execSync("sleep 0.1", { stdio: "ignore" });
|
|
1153
|
+
execSync(`tmux send-keys -t "${windowName}" "st pr review" Enter`, {
|
|
1154
|
+
stdio: "ignore",
|
|
1155
|
+
});
|
|
1156
|
+
dispatch({ type: "SET_ACTION_MESSAGE", message: "Launched AI review in tmux" });
|
|
1157
|
+
}
|
|
1158
|
+
catch {
|
|
1159
|
+
dispatch({ type: "SET_ACTION_MESSAGE", message: "Failed to launch review" });
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
else {
|
|
1163
|
+
leaveAltScreen();
|
|
1164
|
+
console.log(`SANTREE_CD:${ri.worktree.path}`);
|
|
1165
|
+
exit();
|
|
1166
|
+
}
|
|
1167
|
+
return;
|
|
1168
|
+
}
|
|
1169
|
+
// Delete worktree
|
|
1170
|
+
if (input === "d") {
|
|
1171
|
+
if (!ri.worktree || !ri.branch) {
|
|
1172
|
+
dispatch({ type: "SET_ACTION_MESSAGE", message: "No worktree to remove" });
|
|
1173
|
+
return;
|
|
1174
|
+
}
|
|
1175
|
+
const repoRoot = repoRootRef.current;
|
|
1176
|
+
if (!repoRoot)
|
|
1177
|
+
return;
|
|
1178
|
+
const ticketId = extractTicketId(ri.branch);
|
|
1179
|
+
if (!ticketId)
|
|
1180
|
+
return;
|
|
1181
|
+
dispatch({ type: "DELETE_START", ticketId });
|
|
1182
|
+
const force = ri.worktree.dirty;
|
|
1183
|
+
removeWorktree(ri.branch, repoRoot, force).then((result) => {
|
|
1184
|
+
dispatch({ type: "DELETE_DONE" });
|
|
1185
|
+
if (result.success) {
|
|
1186
|
+
dispatch({ type: "SET_ACTION_MESSAGE", message: `Removed worktree` });
|
|
1187
|
+
refresh();
|
|
1188
|
+
}
|
|
1189
|
+
else {
|
|
1190
|
+
dispatch({ type: "SET_ACTION_MESSAGE", message: `Failed: ${result.error}` });
|
|
1191
|
+
}
|
|
1192
|
+
});
|
|
1193
|
+
return;
|
|
1194
|
+
}
|
|
1195
|
+
return; // prevent fallthrough to issues actions
|
|
1196
|
+
}
|
|
848
1197
|
const maxIndex = state.flatIssues.length - 1;
|
|
849
1198
|
// Navigation
|
|
850
1199
|
if (input === "j" || (key.downArrow && !key.shift)) {
|
|
@@ -1056,11 +1405,6 @@ export default function Dashboard() {
|
|
|
1056
1405
|
dispatch({ type: "SET_OVERLAY", overlay: "confirm-delete" });
|
|
1057
1406
|
return;
|
|
1058
1407
|
}
|
|
1059
|
-
// Refresh
|
|
1060
|
-
if (input === "R") {
|
|
1061
|
-
refresh();
|
|
1062
|
-
return;
|
|
1063
|
-
}
|
|
1064
1408
|
}, { isActive: state.overlay !== "commit" || state.commitPhase !== "awaiting-message" });
|
|
1065
1409
|
// ── Render ─────────────────────────────────────────────────────────
|
|
1066
1410
|
if (state.loading) {
|
|
@@ -1069,9 +1413,15 @@ export default function Dashboard() {
|
|
|
1069
1413
|
if (state.error) {
|
|
1070
1414
|
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" })] }) }));
|
|
1071
1415
|
}
|
|
1072
|
-
if (state.flatIssues.length === 0) {
|
|
1073
|
-
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
|
-
}
|
|
1075
1416
|
const selectedIssue = state.flatIssues[state.selectedIndex] ?? null;
|
|
1076
|
-
|
|
1417
|
+
const selectedReview = state.flatReviews[state.reviewSelectedIndex] ?? null;
|
|
1418
|
+
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] }), CLAUDE_VERSION ? (_jsxs(Text, { dimColor: true, children: [" ", "claude ", CLAUDE_VERSION] })) : null, _jsx(Text, { dimColor: true, children: state.refreshing ? " refreshing..." : "" }), state.actionMessage && (_jsxs(Text, { color: "yellow", children: [" ", state.actionMessage] }))] }), _jsxs(Box, { children: [_jsxs(Text, { bold: state.activeTab === "issues", color: state.activeTab === "issues" ? "cyan" : undefined, dimColor: state.activeTab !== "issues", children: [state.activeTab === "issues" ? "\u25b8 " : " ", "1 Issues (", state.flatIssues.length, ")"] }), _jsx(Text, { children: " " }), _jsxs(Text, { bold: state.activeTab === "reviews", color: state.activeTab === "reviews" ? "cyan" : undefined, dimColor: state.activeTab !== "reviews", children: [state.activeTab === "reviews" ? "\u25b8 " : " ", "2 Reviews (", state.flatReviews.length, ")"] })] }), 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) => {
|
|
1419
|
+
const selected = idx === state.baseSelectIndex;
|
|
1420
|
+
const defaultBranch = getDefaultBranch();
|
|
1421
|
+
const label = branch === defaultBranch ? `${branch} (default)` : branch;
|
|
1422
|
+
return (_jsx(Text, { children: _jsxs(Text, { color: selected ? "cyan" : undefined, bold: selected, children: [selected ? "> " : " ", label] }) }, branch));
|
|
1423
|
+
}), _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: state.activeTab === "reviews" ? (_jsx(ReviewList, { flatReviews: state.flatReviews, selectedIndex: state.reviewSelectedIndex, scrollOffset: state.reviewListScrollOffset, height: contentHeight, width: leftWidth })) : state.flatIssues.length === 0 ? (_jsx(Box, { width: leftWidth, height: contentHeight, justifyContent: "center", alignItems: "center", children: _jsx(Text, { color: "yellow", children: "No active issues" }) })) : (_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.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
|
|
1424
|
+
.split("\n")
|
|
1425
|
+
.slice(-(contentHeight - 1))
|
|
1426
|
+
.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, scrollOffset: state.detailScrollOffset })) : (_jsx(DetailPanel, { issue: selectedIssue, scrollOffset: state.detailScrollOffset, height: contentHeight, width: rightWidth, creatingForTicket: state.creatingForTicket, creationLogs: state.creationLogs })) })] }))] }));
|
|
1077
1427
|
}
|
package/dist/lib/ai.js
CHANGED
|
@@ -143,6 +143,9 @@ export function launchAgent(prompt, opts) {
|
|
|
143
143
|
throw new Error("Claude CLI not found. Install: npm install -g @anthropic-ai/claude-code");
|
|
144
144
|
}
|
|
145
145
|
const args = [];
|
|
146
|
+
if (process.env.SANTREE_SKIP_PERMISSIONS) {
|
|
147
|
+
args.push("--dangerously-skip-permissions");
|
|
148
|
+
}
|
|
146
149
|
if (opts?.planMode) {
|
|
147
150
|
args.push("--permission-mode", "plan");
|
|
148
151
|
}
|
|
@@ -167,7 +170,8 @@ export function runAgent(prompt) {
|
|
|
167
170
|
if (!bin) {
|
|
168
171
|
throw new Error("Claude CLI not found. Install: npm install -g @anthropic-ai/claude-code");
|
|
169
172
|
}
|
|
170
|
-
const
|
|
173
|
+
const skipPerms = process.env.SANTREE_SKIP_PERMISSIONS ? ["--dangerously-skip-permissions"] : [];
|
|
174
|
+
const result = spawnSync(bin, [...skipPerms, "-p", "--output-format", "text", "--", promptArg(prompt)], {
|
|
171
175
|
encoding: "utf-8",
|
|
172
176
|
maxBuffer: 10 * 1024 * 1024,
|
|
173
177
|
});
|