mintree 0.3.2 → 0.4.1
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.
|
@@ -10,7 +10,7 @@ import { resolveClaudeBinary } from "../lib/claude.js";
|
|
|
10
10
|
import { tryExec } from "../lib/exec.js";
|
|
11
11
|
import { getLatestVersion, isNewerVersion } from "../lib/version.js";
|
|
12
12
|
import { ALLOWED_TYPES } from "../lib/branch.js";
|
|
13
|
-
import { runCreate, runCreateDetached } from "../lib/worktreeCreate.js";
|
|
13
|
+
import { runCreate, runCreateDetached, } from "../lib/worktreeCreate.js";
|
|
14
14
|
import { runRemove, runRemoveByPath } from "../lib/worktreeRemove.js";
|
|
15
15
|
import { buildCreateMarkers, emitMarkers } from "../lib/markers.js";
|
|
16
16
|
import { readMetadata } from "../lib/metadata.js";
|
|
@@ -19,6 +19,20 @@ import { loadDashboard } from "../lib/dashboard.js";
|
|
|
19
19
|
const require = createRequire(import.meta.url);
|
|
20
20
|
const { version: mintreeVersion } = require("../../package.json");
|
|
21
21
|
export const description = "Interactive dashboard listing open issues assigned to you with worktree + session state";
|
|
22
|
+
function isOrphan(d) {
|
|
23
|
+
return d.orphan === true;
|
|
24
|
+
}
|
|
25
|
+
function tabIssues(issues, tab) {
|
|
26
|
+
return issues.filter((d) => (tab === "issues" ? !isOrphan(d) : isOrphan(d)));
|
|
27
|
+
}
|
|
28
|
+
function currentSelected(s) {
|
|
29
|
+
const displayed = tabIssues(s.issues, s.activeTab);
|
|
30
|
+
const selectedIndex = s.activeTab === "issues" ? s.issuesIndex : s.worktreesIndex;
|
|
31
|
+
return { displayed, selectedIndex };
|
|
32
|
+
}
|
|
33
|
+
function withSelectedIndex(s, next) {
|
|
34
|
+
return s.activeTab === "issues" ? { ...s, issuesIndex: next } : { ...s, worktreesIndex: next };
|
|
35
|
+
}
|
|
22
36
|
// xterm/iTerm/etc switch to the alternate screen buffer with these escape
|
|
23
37
|
// codes. Using the buffer means the dashboard owns the whole window for its
|
|
24
38
|
// lifetime, and the previous shell content reappears unchanged the moment
|
|
@@ -152,8 +166,10 @@ function useTerminalSize() {
|
|
|
152
166
|
}, [stdout]);
|
|
153
167
|
return size;
|
|
154
168
|
}
|
|
155
|
-
function HeaderRow({ repoName, claudeVersion, issueCount, updateAvailable, }) {
|
|
156
|
-
|
|
169
|
+
function HeaderRow({ repoName, claudeVersion, issueCount, worktreeCount, activeTab, updateAvailable, }) {
|
|
170
|
+
const issuesLabel = ` Issues (${issueCount}) `;
|
|
171
|
+
const worktreesLabel = ` Worktrees (${worktreeCount}) `;
|
|
172
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, color: "green", children: "mintree" }), _jsx(Text, { dimColor: true, children: ` v${mintreeVersion}` }), updateAvailable && _jsx(Text, { color: "yellow", children: " (*)" }), claudeVersion && _jsx(Text, { dimColor: true, children: ` · claude ${claudeVersion}` }), repoName && _jsx(Text, { dimColor: true, children: ` · ${repoName}` })] }), _jsxs(Box, { children: [activeTab === "issues" ? (_jsx(Text, { bold: true, backgroundColor: "cyan", color: "black", children: issuesLabel })) : (_jsx(Text, { dimColor: true, children: issuesLabel })), _jsx(Text, { children: " " }), activeTab === "worktrees" ? (_jsx(Text, { bold: true, backgroundColor: "cyan", color: "black", children: worktreesLabel })) : (_jsx(Text, { dimColor: true, children: worktreesLabel })), _jsx(Text, { dimColor: true, children: " ← / → switch tab" })] })] }));
|
|
157
173
|
}
|
|
158
174
|
function FooterRow({ phase, overlayKind, latestVersion, listWidth, }) {
|
|
159
175
|
if (phase === "error") {
|
|
@@ -186,7 +202,14 @@ function CreateOverlayView({ overlay, onDescChange, onPromptChange, }) {
|
|
|
186
202
|
const dirPreview = isNewBranch
|
|
187
203
|
? `${overlay.issue.issue.id}-${overlay.desc}`
|
|
188
204
|
: `${overlay.issue.issue.id}-${detachedDesc}`;
|
|
189
|
-
return (_jsxs(Box, { flexGrow: 1, flexDirection: "column", paddingX: 1, children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, color: "cyan", children: "Create worktree" }), _jsx(Text, { dimColor: true, children: ` for ${overlay.issue.issue.id}` })] }), _jsx(Box, { marginTop: 0, children: _jsx(Text, { children: overlay.issue.issue.title }) }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Box, { width: labelWidth, children: _jsx(Text, { bold: overlay.field === "branchMode", children: overlay.field === "branchMode" ? "▸ Branch:" : " Branch:" }) }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "< " }), _jsx(Text, { color: overlay.field === "branchMode" ? "cyan" : undefined, bold: overlay.field === "branchMode", children: isNewBranch ? "new" : `current (${overlay.currentBranch ?? "?"})` }), _jsx(Text, { dimColor: true, children: " >" })] }), overlay.field === "branchMode" && _jsx(Text, { dimColor: true, children: " (use ← / → to toggle)" })] }), isNewBranch && (_jsxs(_Fragment, { children: [_jsxs(Box, { marginTop: 0, children: [_jsx(Box, { width: labelWidth, children: _jsx(Text, { bold: overlay.field === "type", children: overlay.field === "type" ? "▸ Type:" : " Type:" }) }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "< " }), _jsx(Text, { color: overlay.field === "type" ? "cyan" : undefined, bold: overlay.field === "type", children: overlay.type }), _jsx(Text, { dimColor: true, children: " >" })] }), overlay.field === "type" && _jsx(Text, { dimColor: true, children: " (use ← / → to cycle)" })] }), _jsxs(Box, { marginTop: 0, children: [_jsx(Box, { width: labelWidth, children: _jsx(Text, { bold: overlay.field === "desc", children: overlay.field === "desc" ? "▸ Description:" : " Description:" }) }), _jsx(Box, { children: overlay.field === "desc" ? (_jsx(TextInput, { value: overlay.desc, onChange: onDescChange, placeholder: "kebab-case" })) : (_jsx(Text, { children: overlay.desc || "(empty)" })) })] })] })), _jsxs(Box, { marginTop: 0, children: [_jsx(Box, { width: labelWidth, children: _jsx(Text, { bold: overlay.field === "prompt", children: overlay.field === "prompt" ? "▸ Prompt:" : " Prompt:" }) }), _jsx(Box, { children: overlay.field === "prompt" ? (_jsx(TextInput, { value: overlay.prompt, onChange: onPromptChange, placeholder: "(empty = no initial message)" })) : (_jsx(Text, { dimColor: true, children: overlay.prompt || "(empty — Claude starts with no message)" })) })] })] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Box, { width: labelWidth, children: _jsx(Text, { dimColor: true, children: " Checkout:" }) }), _jsx(Text, { color: "green", children: branchPreview })] }), _jsxs(Box, { children: [_jsx(Box, { width: labelWidth, children: _jsx(Text, { dimColor: true, children: " Worktree:" }) }), _jsxs(Text, { dimColor: true, children: [".mintree/worktrees/", dirPreview] })] }), _jsxs(Box, { children: [_jsx(Box, { width: labelWidth, children: _jsx(Text, { dimColor: true, children: " Mode:" }) }), _jsx(Text, { dimColor: true, children: "--work (Claude launches in the new worktree)" })] })] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [isNewBranch ? (_jsxs(Text, { dimColor: true, children: ["Suggestion is a kebab of the title (capped at ", SUGGESTED_DESC_MAX_WORDS, " words). Edit it to match your repo's branch conventions."] })) : (_jsxs(Text, { dimColor: true, children: ["Detached HEAD at the tip of ", overlay.currentBranch ?? "the current branch", ". No new branch is created \u2014 commit on a new one with `git switch -c` when ready."] })), isNewBranch && overlay.conventionDoc && (_jsx(Text, { dimColor: true, children: `This repo has \`${overlay.conventionDoc}\` — review it before creating.` }))] }), overlay.error && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: "red", bold: true, children: ["\u2717 ", overlay.error] }) })), overlay.pending && (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), _jsxs(Text, { children: [" ", overlay.pending] })] }))] }));
|
|
205
|
+
return (_jsxs(Box, { flexGrow: 1, flexDirection: "column", paddingX: 1, children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, color: "cyan", children: "Create worktree" }), _jsx(Text, { dimColor: true, children: ` for ${overlay.issue.issue.id}` })] }), _jsx(Box, { marginTop: 0, children: _jsx(Text, { children: overlay.issue.issue.title }) }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Box, { width: labelWidth, children: _jsx(Text, { bold: overlay.field === "branchMode", children: overlay.field === "branchMode" ? "▸ Branch:" : " Branch:" }) }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "< " }), _jsx(Text, { color: overlay.field === "branchMode" ? "cyan" : undefined, bold: overlay.field === "branchMode", children: isNewBranch ? "new" : `current (${overlay.currentBranch ?? "?"})` }), _jsx(Text, { dimColor: true, children: " >" })] }), overlay.field === "branchMode" && _jsx(Text, { dimColor: true, children: " (use ← / → to toggle)" })] }), isNewBranch && (_jsxs(_Fragment, { children: [_jsxs(Box, { marginTop: 0, children: [_jsx(Box, { width: labelWidth, children: _jsx(Text, { bold: overlay.field === "type", children: overlay.field === "type" ? "▸ Type:" : " Type:" }) }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "< " }), _jsx(Text, { color: overlay.field === "type" ? "cyan" : undefined, bold: overlay.field === "type", children: overlay.type }), _jsx(Text, { dimColor: true, children: " >" })] }), overlay.field === "type" && _jsx(Text, { dimColor: true, children: " (use ← / → to cycle)" })] }), _jsxs(Box, { marginTop: 0, children: [_jsx(Box, { width: labelWidth, children: _jsx(Text, { bold: overlay.field === "desc", children: overlay.field === "desc" ? "▸ Description:" : " Description:" }) }), _jsx(Box, { children: overlay.field === "desc" ? (_jsx(TextInput, { value: overlay.desc, onChange: onDescChange, placeholder: "kebab-case" })) : (_jsx(Text, { children: overlay.desc || "(empty)" })) })] })] })), _jsxs(Box, { marginTop: 0, children: [_jsx(Box, { width: labelWidth, children: _jsx(Text, { bold: overlay.field === "prompt", children: overlay.field === "prompt" ? "▸ Prompt:" : " Prompt:" }) }), _jsx(Box, { children: overlay.field === "prompt" ? (_jsx(TextInput, { value: overlay.prompt, onChange: onPromptChange, placeholder: "(empty = no initial message)" })) : (_jsx(Text, { dimColor: true, children: overlay.prompt || "(empty — Claude starts with no message)" })) })] })] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Box, { width: labelWidth, children: _jsx(Text, { dimColor: true, children: " Checkout:" }) }), _jsx(Text, { color: "green", children: branchPreview })] }), _jsxs(Box, { children: [_jsx(Box, { width: labelWidth, children: _jsx(Text, { dimColor: true, children: " Worktree:" }) }), _jsxs(Text, { dimColor: true, children: [".mintree/worktrees/", dirPreview] })] }), _jsxs(Box, { children: [_jsx(Box, { width: labelWidth, children: _jsx(Text, { dimColor: true, children: " Mode:" }) }), _jsx(Text, { dimColor: true, children: "--work (Claude launches in the new worktree)" })] })] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [isNewBranch ? (_jsxs(Text, { dimColor: true, children: ["Suggestion is a kebab of the title (capped at ", SUGGESTED_DESC_MAX_WORDS, " words). Edit it to match your repo's branch conventions."] })) : (_jsxs(Text, { dimColor: true, children: ["Detached HEAD at the tip of ", overlay.currentBranch ?? "the current branch", ". No new branch is created \u2014 commit on a new one with `git switch -c` when ready."] })), isNewBranch && overlay.conventionDoc && (_jsx(Text, { dimColor: true, children: `This repo has \`${overlay.conventionDoc}\` — review it before creating.` }))] }), overlay.error && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: "red", bold: true, children: ["\u2717 ", overlay.error] }) })), overlay.steps.length > 0 && (_jsx(Box, { marginTop: 1, flexDirection: "column", children: overlay.steps.map((step, i) => (_jsxs(Box, { children: [_jsx(CreateStepIcon, { kind: step.kind }), _jsx(Text, { children: " " }), _jsx(Text, { children: step.label }), step.detail && _jsxs(Text, { dimColor: true, children: [" (", step.detail, ")"] })] }, i))) })), overlay.pending && (_jsxs(Box, { marginTop: overlay.steps.length > 0 ? 0 : 1, children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), _jsxs(Text, { children: [" ", overlay.pending] })] }))] }));
|
|
206
|
+
}
|
|
207
|
+
function CreateStepIcon({ kind }) {
|
|
208
|
+
if (kind === "ok")
|
|
209
|
+
return _jsx(Text, { color: "green", children: "\u2713" });
|
|
210
|
+
if (kind === "warn")
|
|
211
|
+
return _jsx(Text, { color: "yellow", children: "!" });
|
|
212
|
+
return _jsx(Text, { color: "cyan", children: "\u25CB" });
|
|
190
213
|
}
|
|
191
214
|
function IssueListRow({ d, selected, identifierWidth, rowWidth, }) {
|
|
192
215
|
// Display the issue id raw (e.g. "FE-123", "100"). The `#` prefix is a
|
|
@@ -639,18 +662,32 @@ export default function Dashboard() {
|
|
|
639
662
|
return;
|
|
640
663
|
}
|
|
641
664
|
setState((prev) => {
|
|
642
|
-
const
|
|
643
|
-
const
|
|
644
|
-
const
|
|
645
|
-
const
|
|
646
|
-
const
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
const
|
|
665
|
+
const prevReady = prev.phase === "ready" ? prev : null;
|
|
666
|
+
const activeTab = prevReady?.activeTab ?? "issues";
|
|
667
|
+
const previousIssuesIndex = prevReady?.issuesIndex ?? 0;
|
|
668
|
+
const previousWorktreesIndex = prevReady?.worktreesIndex ?? 0;
|
|
669
|
+
const previousOverlay = prevReady?.overlay ?? null;
|
|
670
|
+
const previousToast = prevReady?.toast ?? null;
|
|
671
|
+
const previousScroll = prevReady?.detailScrollOffset ?? 0;
|
|
672
|
+
const issuesList = tabIssues(issues, "issues");
|
|
673
|
+
const worktreesList = tabIssues(issues, "worktrees");
|
|
674
|
+
const issuesIndex = Math.min(previousIssuesIndex, Math.max(0, issuesList.length - 1));
|
|
675
|
+
const worktreesIndex = Math.min(previousWorktreesIndex, Math.max(0, worktreesList.length - 1));
|
|
676
|
+
// Preserve scroll only when the active tab's selected issue still
|
|
677
|
+
// resolves to the same row — clamping or list churn means the user
|
|
678
|
+
// is now reading something else.
|
|
679
|
+
const prevDisplayed = prevReady ? tabIssues(prevReady.issues, activeTab) : [];
|
|
680
|
+
const nextDisplayed = activeTab === "issues" ? issuesList : worktreesList;
|
|
681
|
+
const prevSelectedId = prevDisplayed[activeTab === "issues" ? previousIssuesIndex : previousWorktreesIndex]?.issue
|
|
682
|
+
.id ?? null;
|
|
683
|
+
const nextSelectedId = nextDisplayed[activeTab === "issues" ? issuesIndex : worktreesIndex]?.issue.id ?? null;
|
|
684
|
+
const detailScrollOffset = prevSelectedId !== null && prevSelectedId === nextSelectedId ? previousScroll : 0;
|
|
650
685
|
return {
|
|
651
686
|
phase: "ready",
|
|
652
687
|
issues,
|
|
653
|
-
|
|
688
|
+
activeTab,
|
|
689
|
+
issuesIndex,
|
|
690
|
+
worktreesIndex,
|
|
654
691
|
detailScrollOffset,
|
|
655
692
|
refreshing: false,
|
|
656
693
|
overlay: previousOverlay,
|
|
@@ -707,10 +744,11 @@ export default function Dashboard() {
|
|
|
707
744
|
if (prev.overlay)
|
|
708
745
|
return prev; // overlay pauses scroll routing
|
|
709
746
|
if (inLeftPane) {
|
|
710
|
-
const
|
|
711
|
-
|
|
747
|
+
const { displayed, selectedIndex } = currentSelected(prev);
|
|
748
|
+
const next = Math.max(0, Math.min(displayed.length - 1, selectedIndex + delta));
|
|
749
|
+
if (next === selectedIndex)
|
|
712
750
|
return prev;
|
|
713
|
-
return { ...prev,
|
|
751
|
+
return { ...withSelectedIndex(prev, next), detailScrollOffset: 0 };
|
|
714
752
|
}
|
|
715
753
|
const next = Math.max(0, prev.detailScrollOffset + delta);
|
|
716
754
|
if (next === prev.detailScrollOffset)
|
|
@@ -781,20 +819,23 @@ export default function Dashboard() {
|
|
|
781
819
|
}
|
|
782
820
|
if (state.phase !== "ready")
|
|
783
821
|
return;
|
|
822
|
+
if (key.leftArrow || key.rightArrow) {
|
|
823
|
+
// Two tabs only — either arrow toggles. Per-tab indices are
|
|
824
|
+
// preserved, so the user returns to the row they left.
|
|
825
|
+
const next = state.activeTab === "issues" ? "worktrees" : "issues";
|
|
826
|
+
setState({ ...state, activeTab: next, detailScrollOffset: 0 });
|
|
827
|
+
return;
|
|
828
|
+
}
|
|
784
829
|
if (key.upArrow || input === "k") {
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
detailScrollOffset: 0,
|
|
789
|
-
});
|
|
830
|
+
const { selectedIndex } = currentSelected(state);
|
|
831
|
+
const nextIndex = Math.max(0, selectedIndex - 1);
|
|
832
|
+
setState({ ...withSelectedIndex(state, nextIndex), detailScrollOffset: 0 });
|
|
790
833
|
return;
|
|
791
834
|
}
|
|
792
835
|
if (key.downArrow || input === "j") {
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
detailScrollOffset: 0,
|
|
797
|
-
});
|
|
836
|
+
const { displayed, selectedIndex } = currentSelected(state);
|
|
837
|
+
const nextIndex = Math.min(Math.max(0, displayed.length - 1), selectedIndex + 1);
|
|
838
|
+
setState({ ...withSelectedIndex(state, nextIndex), detailScrollOffset: 0 });
|
|
798
839
|
return;
|
|
799
840
|
}
|
|
800
841
|
if (key.pageUp) {
|
|
@@ -817,7 +858,8 @@ export default function Dashboard() {
|
|
|
817
858
|
return;
|
|
818
859
|
}
|
|
819
860
|
if (input === "o") {
|
|
820
|
-
const
|
|
861
|
+
const { displayed, selectedIndex } = currentSelected(state);
|
|
862
|
+
const issue = displayed[selectedIndex];
|
|
821
863
|
// Orphan rows carry an empty URL — nothing to open. Skip silently
|
|
822
864
|
// rather than asking the OS to open an empty string.
|
|
823
865
|
if (issue && issue.issue.url)
|
|
@@ -825,7 +867,8 @@ export default function Dashboard() {
|
|
|
825
867
|
return;
|
|
826
868
|
}
|
|
827
869
|
if (input === "w") {
|
|
828
|
-
const
|
|
870
|
+
const { displayed, selectedIndex } = currentSelected(state);
|
|
871
|
+
const issue = displayed[selectedIndex];
|
|
829
872
|
if (!issue)
|
|
830
873
|
return;
|
|
831
874
|
if (issue.worktree) {
|
|
@@ -836,7 +879,8 @@ export default function Dashboard() {
|
|
|
836
879
|
return;
|
|
837
880
|
}
|
|
838
881
|
if (key.return) {
|
|
839
|
-
const
|
|
882
|
+
const { displayed, selectedIndex } = currentSelected(state);
|
|
883
|
+
const issue = displayed[selectedIndex];
|
|
840
884
|
if (!issue)
|
|
841
885
|
return;
|
|
842
886
|
if (issue.worktree) {
|
|
@@ -852,7 +896,8 @@ export default function Dashboard() {
|
|
|
852
896
|
return;
|
|
853
897
|
}
|
|
854
898
|
if (input === "d") {
|
|
855
|
-
const
|
|
899
|
+
const { displayed, selectedIndex } = currentSelected(state);
|
|
900
|
+
const issue = displayed[selectedIndex];
|
|
856
901
|
if (!issue || !issue.worktree)
|
|
857
902
|
return;
|
|
858
903
|
setState({
|
|
@@ -888,6 +933,7 @@ export default function Dashboard() {
|
|
|
888
933
|
error: null,
|
|
889
934
|
conventionDoc: root ? findBranchConventionDoc(root) : null,
|
|
890
935
|
pending: null,
|
|
936
|
+
steps: [],
|
|
891
937
|
},
|
|
892
938
|
toast: null,
|
|
893
939
|
});
|
|
@@ -985,22 +1031,37 @@ export default function Dashboard() {
|
|
|
985
1031
|
});
|
|
986
1032
|
return;
|
|
987
1033
|
}
|
|
988
|
-
//
|
|
989
|
-
//
|
|
990
|
-
//
|
|
991
|
-
// slow remotes or a repo with a real init script). Without yielding
|
|
992
|
-
// here, Ink wouldn't get to paint the spinner before execSync blocks
|
|
993
|
-
// the event loop, leaving the user staring at a frozen overlay.
|
|
994
|
-
//
|
|
995
|
-
// A single setImmediate isn't enough: React 19 commits the state on
|
|
996
|
-
// the next microtask, then Ink schedules its stdout write on a later
|
|
997
|
-
// macrotask. A ~32ms setTimeout (two frames at 60fps) covers both
|
|
998
|
-
// phases reliably without being perceptible.
|
|
1034
|
+
// Enter the live-setup view: clear input chrome, reset the step log,
|
|
1035
|
+
// show a starting spinner. The actual progress updates come through
|
|
1036
|
+
// the runCreate/runCreateDetached callbacks below.
|
|
999
1037
|
setState({
|
|
1000
1038
|
...state,
|
|
1001
|
-
overlay: { ...overlay, error: null, pending: "
|
|
1039
|
+
overlay: { ...overlay, error: null, pending: "Starting...", steps: [] },
|
|
1002
1040
|
});
|
|
1003
1041
|
await new Promise((resolve) => setTimeout(resolve, 32));
|
|
1042
|
+
// Progress callbacks invoked from inside runCreate/runCreateDetached.
|
|
1043
|
+
// Use functional setState so we don't clobber concurrent updates and
|
|
1044
|
+
// don't rely on the stale closure of `state`.
|
|
1045
|
+
const onStep = (step) => {
|
|
1046
|
+
setState((prev) => {
|
|
1047
|
+
if (prev.phase !== "ready" || prev.overlay?.kind !== "create")
|
|
1048
|
+
return prev;
|
|
1049
|
+
return {
|
|
1050
|
+
...prev,
|
|
1051
|
+
overlay: { ...prev.overlay, steps: [...prev.overlay.steps, step] },
|
|
1052
|
+
};
|
|
1053
|
+
});
|
|
1054
|
+
};
|
|
1055
|
+
const onPending = (label) => {
|
|
1056
|
+
setState((prev) => {
|
|
1057
|
+
if (prev.phase !== "ready" || prev.overlay?.kind !== "create")
|
|
1058
|
+
return prev;
|
|
1059
|
+
return {
|
|
1060
|
+
...prev,
|
|
1061
|
+
overlay: { ...prev.overlay, pending: label },
|
|
1062
|
+
};
|
|
1063
|
+
});
|
|
1064
|
+
};
|
|
1004
1065
|
const prompt = overlay.prompt.trim();
|
|
1005
1066
|
const issueId = overlay.issue.issue.id;
|
|
1006
1067
|
let result;
|
|
@@ -1009,29 +1070,35 @@ export default function Dashboard() {
|
|
|
1009
1070
|
// from the issue title (kebabized), not user input — keeping the
|
|
1010
1071
|
// "current branch" flow as low-friction as possible.
|
|
1011
1072
|
const descKebab = kebabize(overlay.issue.issue.title) || `issue-${issueId}`;
|
|
1012
|
-
result = runCreateDetached({
|
|
1073
|
+
result = await runCreateDetached({
|
|
1013
1074
|
issueId,
|
|
1014
1075
|
descKebab,
|
|
1015
1076
|
work: true,
|
|
1077
|
+
progress: { onStep, onPending },
|
|
1016
1078
|
...(prompt.length > 0 ? { prompt } : {}),
|
|
1017
1079
|
});
|
|
1018
1080
|
}
|
|
1019
1081
|
else {
|
|
1020
1082
|
const desc = overlay.desc.trim();
|
|
1021
1083
|
const branch = `${overlay.type}/${issueId}-${desc}`;
|
|
1022
|
-
result = runCreate(branch, {
|
|
1084
|
+
result = await runCreate(branch, {
|
|
1023
1085
|
work: true,
|
|
1086
|
+
progress: { onStep, onPending },
|
|
1024
1087
|
...(prompt.length > 0 ? { prompt } : {}),
|
|
1025
1088
|
});
|
|
1026
1089
|
}
|
|
1027
1090
|
if (!result.ok) {
|
|
1028
|
-
setState({
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1091
|
+
setState((prev) => {
|
|
1092
|
+
if (prev.phase !== "ready" || prev.overlay?.kind !== "create")
|
|
1093
|
+
return prev;
|
|
1094
|
+
return {
|
|
1095
|
+
...prev,
|
|
1096
|
+
overlay: {
|
|
1097
|
+
...prev.overlay,
|
|
1098
|
+
pending: null,
|
|
1099
|
+
error: result.ok ? null : result.message + (result.hint ? ` — ${result.hint}` : ""),
|
|
1100
|
+
},
|
|
1101
|
+
};
|
|
1035
1102
|
});
|
|
1036
1103
|
return;
|
|
1037
1104
|
}
|
|
@@ -1039,9 +1106,15 @@ export default function Dashboard() {
|
|
|
1039
1106
|
// to In Progress on its project. Errors from the GraphQL call don't
|
|
1040
1107
|
// block the worktree hand-off; we swallow them and let `mintree doctor`
|
|
1041
1108
|
// surface persistent issues (missing `project` scope, etc.).
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1109
|
+
// Functional update preserves the accumulated `steps` list from the
|
|
1110
|
+
// progress callbacks; using the stale `overlay` closure would wipe it.
|
|
1111
|
+
setState((prev) => {
|
|
1112
|
+
if (prev.phase !== "ready" || prev.overlay?.kind !== "create")
|
|
1113
|
+
return prev;
|
|
1114
|
+
return {
|
|
1115
|
+
...prev,
|
|
1116
|
+
overlay: { ...prev.overlay, error: null, pending: "Updating issue status..." },
|
|
1117
|
+
};
|
|
1045
1118
|
});
|
|
1046
1119
|
const repoRoot = findMainRepoRoot();
|
|
1047
1120
|
if (repoRoot) {
|
|
@@ -1094,8 +1167,11 @@ export default function Dashboard() {
|
|
|
1094
1167
|
if (state.phase === "error") {
|
|
1095
1168
|
return (_jsxs(Box, { width: columns, height: rows, flexDirection: "column", borderStyle: "round", borderColor: "red", paddingX: 1, children: [_jsxs(Text, { color: "red", bold: true, children: ["\u2717 ", state.message] }), state.hint && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: "yellow", children: ["\u21B3 ", state.hint] }) })), _jsx(Box, { marginTop: 1, children: _jsx(FooterRow, { phase: "error" }) })] }));
|
|
1096
1169
|
}
|
|
1097
|
-
const { issues,
|
|
1098
|
-
const
|
|
1170
|
+
const { issues, refreshing, overlay, toast, activeTab } = state;
|
|
1171
|
+
const { displayed, selectedIndex } = currentSelected(state);
|
|
1172
|
+
const selected = displayed[selectedIndex] ?? null;
|
|
1173
|
+
const issuesTabCount = issues.reduce((n, d) => (isOrphan(d) ? n : n + 1), 0);
|
|
1174
|
+
const worktreesTabCount = issues.length - issuesTabCount;
|
|
1099
1175
|
const onOverlayDescChange = (next) => {
|
|
1100
1176
|
if (state.phase !== "ready" || !state.overlay)
|
|
1101
1177
|
return;
|
|
@@ -1122,7 +1198,7 @@ export default function Dashboard() {
|
|
|
1122
1198
|
const listWidthPct = 0.5;
|
|
1123
1199
|
const listWidth = Math.max(32, Math.floor(columns * listWidthPct));
|
|
1124
1200
|
const detailWidth = columns - listWidth - 2; // border slack
|
|
1125
|
-
const identifierWidth = Math.max(3, ...
|
|
1201
|
+
const identifierWidth = Math.max(3, ...displayed.map((d) => d.issue.id.length));
|
|
1126
1202
|
// Reserve rows: header (2), top borders (1), footer (3).
|
|
1127
1203
|
const listVisibleRows = Math.max(3, rows - 9);
|
|
1128
1204
|
// Detail pane content height inside the bordered box. Header eats 2 rows,
|
|
@@ -1136,8 +1212,14 @@ export default function Dashboard() {
|
|
|
1136
1212
|
// Grouped list: build the project/status header rows interleaved with
|
|
1137
1213
|
// issue rows, then split into a sticky header region (the selected issue's
|
|
1138
1214
|
// project + Status, pinned to the top) and a windowed scrollable body.
|
|
1139
|
-
|
|
1215
|
+
// The Worktrees tab renders flat — the tab title already labels the group,
|
|
1216
|
+
// so the per-project headers would just be visual noise.
|
|
1217
|
+
const listRows = activeTab === "issues"
|
|
1218
|
+
? buildListRows(displayed)
|
|
1219
|
+
: displayed.map((d, index) => ({ kind: "issue", d, index }));
|
|
1140
1220
|
const listView = windowListRows(listRows, selectedIndex, listVisibleRows);
|
|
1141
1221
|
const listContentWidth = Math.max(8, listWidth - 4);
|
|
1142
|
-
return (_jsxs(Box, { flexDirection: "column", width: columns, height: rows, children: [_jsx(Box, { paddingX: 1, paddingTop: 0, flexDirection: "column", children: _jsx(HeaderRow, { repoName: repoName, claudeVersion: claudeVersion, issueCount:
|
|
1222
|
+
return (_jsxs(Box, { flexDirection: "column", width: columns, height: rows, children: [_jsx(Box, { paddingX: 1, paddingTop: 0, flexDirection: "column", children: _jsx(HeaderRow, { repoName: repoName, claudeVersion: claudeVersion, issueCount: issuesTabCount, worktreeCount: worktreesTabCount, activeTab: activeTab, updateAvailable: latestVersion !== null }) }), overlay ? (_jsx(Box, { flexGrow: 1, flexDirection: "column", borderStyle: "round", borderColor: overlay.kind === "remove" ? "yellow" : "cyan", children: overlay.kind === "create" ? (_jsx(CreateOverlayView, { overlay: overlay, onDescChange: onOverlayDescChange, onPromptChange: onOverlayPromptChange })) : (_jsx(RemoveOverlayView, { overlay: overlay })) })) : (_jsxs(Box, { flexGrow: 1, flexDirection: "row", children: [_jsx(Box, { width: listWidth, flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 1, children: displayed.length === 0 ? (_jsx(Text, { dimColor: true, children: activeTab === "issues"
|
|
1223
|
+
? "No open issues assigned to you in this repo."
|
|
1224
|
+
: "No orphaned worktrees — anything in `.mintree/worktrees/` matches an open issue." })) : (_jsxs(_Fragment, { children: [listView.sticky.map((row, i) => (_jsx(ListRowView, { row: row, selectedIndex: selectedIndex, identifierWidth: identifierWidth, width: listContentWidth }, `sticky-${i}`))), listView.issuesAbove > 0 && (_jsxs(Text, { dimColor: true, children: ["\u2191 ", listView.issuesAbove, " more above"] })), listView.body.map((row, i) => (_jsx(ListRowView, { row: row, selectedIndex: selectedIndex, identifierWidth: identifierWidth, width: listContentWidth }, `body-${i}`))), listView.issuesBelow > 0 && (_jsxs(Text, { dimColor: true, children: ["\u2193 ", listView.issuesBelow, " more below"] }))] })) }), _jsx(Box, { width: detailWidth, flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 1, children: _jsx(DetailPane, { d: selected, contentWidth: detailWidth - 4, contentHeight: detailContentHeight, scrollOffset: state.detailScrollOffset }) })] })), _jsxs(Box, { paddingX: 1, flexDirection: "column", children: [toast && (_jsx(Box, { children: _jsxs(Text, { color: toast.kind === "success" ? "green" : toast.kind === "error" ? "red" : "cyan", children: [toast.kind === "success" ? "✓ " : toast.kind === "error" ? "✗ " : "· ", toast.text] }) })), refreshing && (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), _jsx(Text, { dimColor: true, children: " refreshing" })] })), _jsx(FooterRow, { phase: "ready", overlayKind: overlay?.kind, latestVersion: latestVersion, listWidth: listWidth })] })] }));
|
|
1143
1225
|
}
|
|
@@ -55,19 +55,20 @@ export default function Create({ args, options }) {
|
|
|
55
55
|
const [result, setResult] = useState(null);
|
|
56
56
|
const [transition, setTransition] = useState("idle");
|
|
57
57
|
useEffect(() => {
|
|
58
|
-
|
|
58
|
+
(async () => {
|
|
59
59
|
try {
|
|
60
|
-
|
|
60
|
+
const r = await runCreate(branch, {
|
|
61
61
|
base: options.base,
|
|
62
62
|
work: options.work,
|
|
63
63
|
prompt: options.prompt,
|
|
64
64
|
permissionMode: options.permissionMode,
|
|
65
|
-
})
|
|
65
|
+
});
|
|
66
|
+
setResult(r);
|
|
66
67
|
}
|
|
67
68
|
catch (err) {
|
|
68
69
|
setResult({ ok: false, message: err instanceof Error ? err.message : String(err) });
|
|
69
70
|
}
|
|
70
|
-
}
|
|
71
|
+
})();
|
|
71
72
|
}, [branch, options.base, options.work, options.prompt, options.permissionMode]);
|
|
72
73
|
// Kick the Project v2 transition once the worktree is in place. Only when
|
|
73
74
|
// --work was on — non-work creates leave status untouched. Errors from the
|
|
@@ -5,11 +5,24 @@ export type CreateStep = {
|
|
|
5
5
|
label: string;
|
|
6
6
|
detail?: string;
|
|
7
7
|
};
|
|
8
|
+
/**
|
|
9
|
+
* Optional progress callbacks used by the dashboard overlay to render a
|
|
10
|
+
* live setup log (santree-style). `onPending(label)` highlights the
|
|
11
|
+
* currently running blocking operation (rendered with a spinner); call
|
|
12
|
+
* `onPending(null)` when it ends. `onStep(step)` appends a completed step
|
|
13
|
+
* to the log. Between every emission the implementation yields the event
|
|
14
|
+
* loop for one frame so Ink can paint before the next blocking section.
|
|
15
|
+
*/
|
|
16
|
+
export type ProgressCallbacks = {
|
|
17
|
+
onStep?: (step: CreateStep) => void;
|
|
18
|
+
onPending?: (label: string | null) => void;
|
|
19
|
+
};
|
|
8
20
|
export type CreateOpts = {
|
|
9
21
|
base?: string;
|
|
10
22
|
work: boolean;
|
|
11
23
|
prompt?: string;
|
|
12
24
|
permissionMode?: PermissionMode;
|
|
25
|
+
progress?: ProgressCallbacks;
|
|
13
26
|
};
|
|
14
27
|
export type CreateResult = {
|
|
15
28
|
ok: true;
|
|
@@ -32,14 +45,18 @@ export type CreateResult = {
|
|
|
32
45
|
* resolves a base branch, runs `git worktree add`, persists metadata, runs
|
|
33
46
|
* the optional `.mintree/init.sh`, and stages the --prompt to a temp file
|
|
34
47
|
* for the work hand-off when relevant.
|
|
48
|
+
*
|
|
49
|
+
* Async only because progress callbacks need event-loop yields between
|
|
50
|
+
* blocking sections; without them the dashboard overlay would freeze.
|
|
35
51
|
*/
|
|
36
|
-
export declare function runCreate(branchArg: string, opts: CreateOpts): CreateResult
|
|
52
|
+
export declare function runCreate(branchArg: string, opts: CreateOpts): Promise<CreateResult>;
|
|
37
53
|
export type CreateDetachedOpts = {
|
|
38
54
|
issueId: string;
|
|
39
55
|
descKebab: string;
|
|
40
56
|
work: boolean;
|
|
41
57
|
prompt?: string;
|
|
42
58
|
permissionMode?: PermissionMode;
|
|
59
|
+
progress?: ProgressCallbacks;
|
|
43
60
|
};
|
|
44
61
|
/**
|
|
45
62
|
* Variant of `runCreate` that doesn't create a new branch — the worktree is
|
|
@@ -53,4 +70,4 @@ export type CreateDetachedOpts = {
|
|
|
53
70
|
* branch-based flow so `worktree work` can still recover the issueId from
|
|
54
71
|
* the dir name (where it can't read it from the branch).
|
|
55
72
|
*/
|
|
56
|
-
export declare function runCreateDetached(opts: CreateDetachedOpts): CreateResult
|
|
73
|
+
export declare function runCreateDetached(opts: CreateDetachedOpts): Promise<CreateResult>;
|
|
@@ -37,14 +37,26 @@ function writePromptFile(prompt) {
|
|
|
37
37
|
fs.writeFileSync(filePath, prompt);
|
|
38
38
|
return filePath;
|
|
39
39
|
}
|
|
40
|
+
// Wait one frame (~16ms) so Ink has time to commit + paint the latest
|
|
41
|
+
// state before the next blocking execSync. No-op when no progress
|
|
42
|
+
// callbacks are set — CLI invocations skip the cost entirely.
|
|
43
|
+
function nextFrame(progress) {
|
|
44
|
+
if (!progress || (!progress.onStep && !progress.onPending))
|
|
45
|
+
return Promise.resolve();
|
|
46
|
+
return new Promise((resolve) => setTimeout(resolve, 16));
|
|
47
|
+
}
|
|
40
48
|
/**
|
|
41
49
|
* The whole `worktree create` flow as a pure function — same code path used
|
|
42
50
|
* by the CLI command and by the dashboard's `w` overlay. Validates input,
|
|
43
51
|
* resolves a base branch, runs `git worktree add`, persists metadata, runs
|
|
44
52
|
* the optional `.mintree/init.sh`, and stages the --prompt to a temp file
|
|
45
53
|
* for the work hand-off when relevant.
|
|
54
|
+
*
|
|
55
|
+
* Async only because progress callbacks need event-loop yields between
|
|
56
|
+
* blocking sections; without them the dashboard overlay would freeze.
|
|
46
57
|
*/
|
|
47
|
-
export function runCreate(branchArg, opts) {
|
|
58
|
+
export async function runCreate(branchArg, opts) {
|
|
59
|
+
const progress = opts.progress;
|
|
48
60
|
const root = findMainRepoRoot();
|
|
49
61
|
if (!root) {
|
|
50
62
|
return {
|
|
@@ -81,18 +93,27 @@ export function runCreate(branchArg, opts) {
|
|
|
81
93
|
};
|
|
82
94
|
}
|
|
83
95
|
const steps = [];
|
|
84
|
-
|
|
96
|
+
const pushStep = (step) => {
|
|
97
|
+
steps.push(step);
|
|
98
|
+
progress?.onStep?.(step);
|
|
99
|
+
};
|
|
100
|
+
pushStep({
|
|
85
101
|
kind: "ok",
|
|
86
102
|
label: "parsed branch",
|
|
87
103
|
detail: `type=${parsed.type}, issue=${parsed.issueId}, desc=${parsed.desc}`,
|
|
88
104
|
});
|
|
105
|
+
await nextFrame(progress);
|
|
89
106
|
// Fetch before resolving refs so the worktree forks from fresh code, not a
|
|
90
107
|
// stale local checkout. Best-effort: offline / no-remote just warns and we
|
|
91
108
|
// fall back to whatever is already local.
|
|
109
|
+
progress?.onPending?.("Fetching origin...");
|
|
110
|
+
await nextFrame(progress);
|
|
92
111
|
const fetch = fetchRemote(root);
|
|
93
|
-
|
|
112
|
+
progress?.onPending?.(null);
|
|
113
|
+
pushStep(fetch.ok
|
|
94
114
|
? { kind: "ok", label: "fetched origin", detail: "refs up to date" }
|
|
95
115
|
: { kind: "warn", label: "skipped git fetch", detail: fetch.reason });
|
|
116
|
+
await nextFrame(progress);
|
|
96
117
|
const existence = branchExists(root, parsed.branch);
|
|
97
118
|
let base;
|
|
98
119
|
if (existence === null) {
|
|
@@ -119,10 +140,13 @@ export function runCreate(branchArg, opts) {
|
|
|
119
140
|
if (existence === null && base && fetch.ok && remoteBranchExists(root, base)) {
|
|
120
141
|
baseRef = `origin/${base}`;
|
|
121
142
|
}
|
|
143
|
+
progress?.onPending?.("Creating worktree...");
|
|
144
|
+
await nextFrame(progress);
|
|
122
145
|
try {
|
|
123
146
|
addWorktree({ repoRoot: root, branch: parsed.branch, worktreePath, base: baseRef });
|
|
124
147
|
}
|
|
125
148
|
catch (err) {
|
|
149
|
+
progress?.onPending?.(null);
|
|
126
150
|
const stderr = err && typeof err === "object" && "stderr" in err
|
|
127
151
|
? String(err.stderr).trim()
|
|
128
152
|
: err instanceof Error
|
|
@@ -130,48 +154,58 @@ export function runCreate(branchArg, opts) {
|
|
|
130
154
|
: String(err);
|
|
131
155
|
return { ok: false, message: `git worktree add failed: ${stderr}` };
|
|
132
156
|
}
|
|
157
|
+
progress?.onPending?.(null);
|
|
133
158
|
if (existence === "remote") {
|
|
134
|
-
|
|
159
|
+
pushStep({
|
|
135
160
|
kind: "ok",
|
|
136
161
|
label: "checked out tracking branch",
|
|
137
162
|
detail: `from origin/${parsed.branch}`,
|
|
138
163
|
});
|
|
139
164
|
}
|
|
140
165
|
else if (existence === "local") {
|
|
141
|
-
|
|
166
|
+
pushStep({
|
|
142
167
|
kind: "ok",
|
|
143
168
|
label: "checked out existing local branch",
|
|
144
169
|
detail: parsed.branch,
|
|
145
170
|
});
|
|
146
171
|
}
|
|
147
172
|
else {
|
|
148
|
-
|
|
173
|
+
pushStep({
|
|
149
174
|
kind: "ok",
|
|
150
175
|
label: "created new branch",
|
|
151
176
|
detail: `${parsed.branch} (from ${baseRef})`,
|
|
152
177
|
});
|
|
153
178
|
}
|
|
154
|
-
|
|
179
|
+
await nextFrame(progress);
|
|
180
|
+
pushStep({ kind: "ok", label: "worktree created", detail: worktreePath });
|
|
181
|
+
await nextFrame(progress);
|
|
155
182
|
upsertIssue(root, parsed.issueId, base ? { base_branch: base } : {});
|
|
156
|
-
|
|
183
|
+
pushStep({ kind: "ok", label: "metadata updated", detail: `issue ${parsed.issueId}` });
|
|
184
|
+
await nextFrame(progress);
|
|
157
185
|
const initShPath = getInitScriptPath(root);
|
|
186
|
+
if (pathExists(initShPath)) {
|
|
187
|
+
progress?.onPending?.("Running .mintree/init.sh...");
|
|
188
|
+
await nextFrame(progress);
|
|
189
|
+
}
|
|
158
190
|
const initResult = tryRunInitScript(initShPath, worktreePath, root);
|
|
191
|
+
progress?.onPending?.(null);
|
|
159
192
|
if (initResult.ran) {
|
|
160
|
-
|
|
193
|
+
pushStep({ kind: "ok", label: "ran .mintree/init.sh", detail: worktreePath });
|
|
161
194
|
}
|
|
162
195
|
else if (initResult.error) {
|
|
163
|
-
|
|
196
|
+
pushStep({ kind: "warn", label: "init.sh failed", detail: initResult.error });
|
|
164
197
|
}
|
|
165
198
|
else if (!pathExists(initShPath)) {
|
|
166
|
-
|
|
199
|
+
pushStep({ kind: "skip", label: "no init.sh (skipping post-create hook)" });
|
|
167
200
|
}
|
|
201
|
+
await nextFrame(progress);
|
|
168
202
|
let promptFile;
|
|
169
203
|
if (opts.work && opts.prompt && opts.prompt.length > 0) {
|
|
170
204
|
try {
|
|
171
205
|
promptFile = writePromptFile(opts.prompt);
|
|
172
206
|
}
|
|
173
207
|
catch (err) {
|
|
174
|
-
|
|
208
|
+
pushStep({
|
|
175
209
|
kind: "warn",
|
|
176
210
|
label: "failed to stage --prompt for hand-off",
|
|
177
211
|
detail: err instanceof Error ? err.message : String(err),
|
|
@@ -179,7 +213,7 @@ export function runCreate(branchArg, opts) {
|
|
|
179
213
|
}
|
|
180
214
|
}
|
|
181
215
|
if (!opts.work && (opts.prompt || opts.permissionMode)) {
|
|
182
|
-
|
|
216
|
+
pushStep({
|
|
183
217
|
kind: "warn",
|
|
184
218
|
label: "ignoring --prompt / --permission-mode (only meaningful with --work)",
|
|
185
219
|
});
|
|
@@ -208,7 +242,8 @@ export function runCreate(branchArg, opts) {
|
|
|
208
242
|
* branch-based flow so `worktree work` can still recover the issueId from
|
|
209
243
|
* the dir name (where it can't read it from the branch).
|
|
210
244
|
*/
|
|
211
|
-
export function runCreateDetached(opts) {
|
|
245
|
+
export async function runCreateDetached(opts) {
|
|
246
|
+
const progress = opts.progress;
|
|
212
247
|
const root = findMainRepoRoot();
|
|
213
248
|
if (!root) {
|
|
214
249
|
return {
|
|
@@ -255,22 +290,34 @@ export function runCreateDetached(opts) {
|
|
|
255
290
|
};
|
|
256
291
|
}
|
|
257
292
|
const steps = [];
|
|
258
|
-
|
|
293
|
+
const pushStep = (step) => {
|
|
294
|
+
steps.push(step);
|
|
295
|
+
progress?.onStep?.(step);
|
|
296
|
+
};
|
|
297
|
+
pushStep({
|
|
259
298
|
kind: "ok",
|
|
260
299
|
label: "detached worktree",
|
|
261
300
|
detail: `issue=${opts.issueId}, base=${currentBranch}`,
|
|
262
301
|
});
|
|
302
|
+
await nextFrame(progress);
|
|
263
303
|
// Fetch so the detached worktree forks from the fresh remote tip of the
|
|
264
304
|
// current branch instead of a stale local checkout. Best-effort.
|
|
305
|
+
progress?.onPending?.("Fetching origin...");
|
|
306
|
+
await nextFrame(progress);
|
|
265
307
|
const fetch = fetchRemote(root);
|
|
266
|
-
|
|
308
|
+
progress?.onPending?.(null);
|
|
309
|
+
pushStep(fetch.ok
|
|
267
310
|
? { kind: "ok", label: "fetched origin", detail: "refs up to date" }
|
|
268
311
|
: { kind: "warn", label: "skipped git fetch", detail: fetch.reason });
|
|
312
|
+
await nextFrame(progress);
|
|
269
313
|
const baseRef = fetch.ok && remoteBranchExists(root, currentBranch) ? `origin/${currentBranch}` : currentBranch;
|
|
314
|
+
progress?.onPending?.("Creating worktree...");
|
|
315
|
+
await nextFrame(progress);
|
|
270
316
|
try {
|
|
271
317
|
execSync(`git worktree add --detach '${worktreePath.replace(/'/g, `'\\''`)}' '${baseRef.replace(/'/g, `'\\''`)}'`, { cwd: root, stdio: ["ignore", "pipe", "pipe"] });
|
|
272
318
|
}
|
|
273
319
|
catch (err) {
|
|
320
|
+
progress?.onPending?.(null);
|
|
274
321
|
const stderr = err && typeof err === "object" && "stderr" in err
|
|
275
322
|
? String(err.stderr).trim()
|
|
276
323
|
: err instanceof Error
|
|
@@ -278,32 +325,42 @@ export function runCreateDetached(opts) {
|
|
|
278
325
|
: String(err);
|
|
279
326
|
return { ok: false, message: `git worktree add --detach failed: ${stderr}` };
|
|
280
327
|
}
|
|
281
|
-
|
|
328
|
+
progress?.onPending?.(null);
|
|
329
|
+
pushStep({
|
|
282
330
|
kind: "ok",
|
|
283
331
|
label: "checked out detached HEAD",
|
|
284
332
|
detail: `at tip of ${baseRef}`,
|
|
285
333
|
});
|
|
286
|
-
|
|
334
|
+
await nextFrame(progress);
|
|
335
|
+
pushStep({ kind: "ok", label: "worktree created", detail: worktreePath });
|
|
336
|
+
await nextFrame(progress);
|
|
287
337
|
upsertIssue(root, opts.issueId, { base_branch: currentBranch });
|
|
288
|
-
|
|
338
|
+
pushStep({ kind: "ok", label: "metadata updated", detail: `issue ${opts.issueId}` });
|
|
339
|
+
await nextFrame(progress);
|
|
289
340
|
const initShPath = getInitScriptPath(root);
|
|
341
|
+
if (pathExists(initShPath)) {
|
|
342
|
+
progress?.onPending?.("Running .mintree/init.sh...");
|
|
343
|
+
await nextFrame(progress);
|
|
344
|
+
}
|
|
290
345
|
const initResult = tryRunInitScript(initShPath, worktreePath, root);
|
|
346
|
+
progress?.onPending?.(null);
|
|
291
347
|
if (initResult.ran) {
|
|
292
|
-
|
|
348
|
+
pushStep({ kind: "ok", label: "ran .mintree/init.sh", detail: worktreePath });
|
|
293
349
|
}
|
|
294
350
|
else if (initResult.error) {
|
|
295
|
-
|
|
351
|
+
pushStep({ kind: "warn", label: "init.sh failed", detail: initResult.error });
|
|
296
352
|
}
|
|
297
353
|
else if (!pathExists(initShPath)) {
|
|
298
|
-
|
|
354
|
+
pushStep({ kind: "skip", label: "no init.sh (skipping post-create hook)" });
|
|
299
355
|
}
|
|
356
|
+
await nextFrame(progress);
|
|
300
357
|
let promptFile;
|
|
301
358
|
if (opts.work && opts.prompt && opts.prompt.length > 0) {
|
|
302
359
|
try {
|
|
303
360
|
promptFile = writePromptFile(opts.prompt);
|
|
304
361
|
}
|
|
305
362
|
catch (err) {
|
|
306
|
-
|
|
363
|
+
pushStep({
|
|
307
364
|
kind: "warn",
|
|
308
365
|
label: "failed to stage --prompt for hand-off",
|
|
309
366
|
detail: err instanceof Error ? err.message : String(err),
|
package/package.json
CHANGED