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.
@@ -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
- 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}` })] }), _jsx(Box, { children: _jsx(Text, { bold: true, backgroundColor: "cyan", color: "black", children: ` Issues (${issueCount}) ` }) })] }));
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 previousIndex = prev.phase === "ready" ? prev.selectedIndex : 0;
650
- const previousOverlay = prev.phase === "ready" ? prev.overlay : null;
651
- const previousToast = prev.phase === "ready" ? prev.toast : null;
652
- const previousScroll = prev.phase === "ready" ? prev.detailScrollOffset : 0;
653
- const clamped = Math.min(previousIndex, Math.max(0, issues.length - 1));
654
- // Preserve scroll only when the selected issue stayed put — clamping
655
- // to a different row means the user is now reading something else.
656
- const detailScrollOffset = clamped === previousIndex ? previousScroll : 0;
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
- selectedIndex: clamped,
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 next = Math.max(0, Math.min(prev.issues.length - 1, prev.selectedIndex + delta));
718
- if (next === prev.selectedIndex)
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, selectedIndex: next, detailScrollOffset: 0 };
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
- setState({
793
- ...state,
794
- selectedIndex: Math.max(0, state.selectedIndex - 1),
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
- setState({
801
- ...state,
802
- selectedIndex: Math.min(state.issues.length - 1, state.selectedIndex + 1),
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 issue = state.issues[state.selectedIndex];
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 issue = state.issues[state.selectedIndex];
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 issue = state.issues[state.selectedIndex];
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 issue = state.issues[state.selectedIndex];
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, selectedIndex, refreshing, overlay, toast } = state;
1133
- const selected = issues[selectedIndex] ?? null;
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, ...issues.map((d) => d.issue.id.length));
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
- const listRows = buildListRows(issues);
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: issues.length, 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: issues.length === 0 ? (_jsx(Text, { dimColor: true, children: "No open issues assigned to you in this repo." })) : (_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 })] })] }));
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mintree",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "Issue-driven git worktrees + Claude Code sessions for repos with an opinionated SDD+TDD flow.",
5
5
  "license": "MIT",
6
6
  "author": "Martin Mineo <mmineo@canarytechnologies.com>",