mintree 0.4.0 → 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.
- package/dist/commands/dashboard.js +80 -33
- package/package.json +1 -1
|
@@ -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") {
|
|
@@ -646,18 +662,32 @@ export default function Dashboard() {
|
|
|
646
662
|
return;
|
|
647
663
|
}
|
|
648
664
|
setState((prev) => {
|
|
649
|
-
const
|
|
650
|
-
const
|
|
651
|
-
const
|
|
652
|
-
const
|
|
653
|
-
const
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
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;
|
|
657
685
|
return {
|
|
658
686
|
phase: "ready",
|
|
659
687
|
issues,
|
|
660
|
-
|
|
688
|
+
activeTab,
|
|
689
|
+
issuesIndex,
|
|
690
|
+
worktreesIndex,
|
|
661
691
|
detailScrollOffset,
|
|
662
692
|
refreshing: false,
|
|
663
693
|
overlay: previousOverlay,
|
|
@@ -714,10 +744,11 @@ export default function Dashboard() {
|
|
|
714
744
|
if (prev.overlay)
|
|
715
745
|
return prev; // overlay pauses scroll routing
|
|
716
746
|
if (inLeftPane) {
|
|
717
|
-
const
|
|
718
|
-
|
|
747
|
+
const { displayed, selectedIndex } = currentSelected(prev);
|
|
748
|
+
const next = Math.max(0, Math.min(displayed.length - 1, selectedIndex + delta));
|
|
749
|
+
if (next === selectedIndex)
|
|
719
750
|
return prev;
|
|
720
|
-
return { ...prev,
|
|
751
|
+
return { ...withSelectedIndex(prev, next), detailScrollOffset: 0 };
|
|
721
752
|
}
|
|
722
753
|
const next = Math.max(0, prev.detailScrollOffset + delta);
|
|
723
754
|
if (next === prev.detailScrollOffset)
|
|
@@ -788,20 +819,23 @@ export default function Dashboard() {
|
|
|
788
819
|
}
|
|
789
820
|
if (state.phase !== "ready")
|
|
790
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
|
+
}
|
|
791
829
|
if (key.upArrow || input === "k") {
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
detailScrollOffset: 0,
|
|
796
|
-
});
|
|
830
|
+
const { selectedIndex } = currentSelected(state);
|
|
831
|
+
const nextIndex = Math.max(0, selectedIndex - 1);
|
|
832
|
+
setState({ ...withSelectedIndex(state, nextIndex), detailScrollOffset: 0 });
|
|
797
833
|
return;
|
|
798
834
|
}
|
|
799
835
|
if (key.downArrow || input === "j") {
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
detailScrollOffset: 0,
|
|
804
|
-
});
|
|
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 });
|
|
805
839
|
return;
|
|
806
840
|
}
|
|
807
841
|
if (key.pageUp) {
|
|
@@ -824,7 +858,8 @@ export default function Dashboard() {
|
|
|
824
858
|
return;
|
|
825
859
|
}
|
|
826
860
|
if (input === "o") {
|
|
827
|
-
const
|
|
861
|
+
const { displayed, selectedIndex } = currentSelected(state);
|
|
862
|
+
const issue = displayed[selectedIndex];
|
|
828
863
|
// Orphan rows carry an empty URL — nothing to open. Skip silently
|
|
829
864
|
// rather than asking the OS to open an empty string.
|
|
830
865
|
if (issue && issue.issue.url)
|
|
@@ -832,7 +867,8 @@ export default function Dashboard() {
|
|
|
832
867
|
return;
|
|
833
868
|
}
|
|
834
869
|
if (input === "w") {
|
|
835
|
-
const
|
|
870
|
+
const { displayed, selectedIndex } = currentSelected(state);
|
|
871
|
+
const issue = displayed[selectedIndex];
|
|
836
872
|
if (!issue)
|
|
837
873
|
return;
|
|
838
874
|
if (issue.worktree) {
|
|
@@ -843,7 +879,8 @@ export default function Dashboard() {
|
|
|
843
879
|
return;
|
|
844
880
|
}
|
|
845
881
|
if (key.return) {
|
|
846
|
-
const
|
|
882
|
+
const { displayed, selectedIndex } = currentSelected(state);
|
|
883
|
+
const issue = displayed[selectedIndex];
|
|
847
884
|
if (!issue)
|
|
848
885
|
return;
|
|
849
886
|
if (issue.worktree) {
|
|
@@ -859,7 +896,8 @@ export default function Dashboard() {
|
|
|
859
896
|
return;
|
|
860
897
|
}
|
|
861
898
|
if (input === "d") {
|
|
862
|
-
const
|
|
899
|
+
const { displayed, selectedIndex } = currentSelected(state);
|
|
900
|
+
const issue = displayed[selectedIndex];
|
|
863
901
|
if (!issue || !issue.worktree)
|
|
864
902
|
return;
|
|
865
903
|
setState({
|
|
@@ -1129,8 +1167,11 @@ export default function Dashboard() {
|
|
|
1129
1167
|
if (state.phase === "error") {
|
|
1130
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" }) })] }));
|
|
1131
1169
|
}
|
|
1132
|
-
const { issues,
|
|
1133
|
-
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;
|
|
1134
1175
|
const onOverlayDescChange = (next) => {
|
|
1135
1176
|
if (state.phase !== "ready" || !state.overlay)
|
|
1136
1177
|
return;
|
|
@@ -1157,7 +1198,7 @@ export default function Dashboard() {
|
|
|
1157
1198
|
const listWidthPct = 0.5;
|
|
1158
1199
|
const listWidth = Math.max(32, Math.floor(columns * listWidthPct));
|
|
1159
1200
|
const detailWidth = columns - listWidth - 2; // border slack
|
|
1160
|
-
const identifierWidth = Math.max(3, ...
|
|
1201
|
+
const identifierWidth = Math.max(3, ...displayed.map((d) => d.issue.id.length));
|
|
1161
1202
|
// Reserve rows: header (2), top borders (1), footer (3).
|
|
1162
1203
|
const listVisibleRows = Math.max(3, rows - 9);
|
|
1163
1204
|
// Detail pane content height inside the bordered box. Header eats 2 rows,
|
|
@@ -1171,8 +1212,14 @@ export default function Dashboard() {
|
|
|
1171
1212
|
// Grouped list: build the project/status header rows interleaved with
|
|
1172
1213
|
// issue rows, then split into a sticky header region (the selected issue's
|
|
1173
1214
|
// project + Status, pinned to the top) and a windowed scrollable body.
|
|
1174
|
-
|
|
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 }));
|
|
1175
1220
|
const listView = windowListRows(listRows, selectedIndex, listVisibleRows);
|
|
1176
1221
|
const listContentWidth = Math.max(8, listWidth - 4);
|
|
1177
|
-
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 })] })] }));
|
|
1178
1225
|
}
|
package/package.json
CHANGED