mintree 0.2.1 → 0.2.2
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 +33 -49
- package/dist/lib/dashboard.d.ts +1 -0
- package/dist/lib/dashboard.js +63 -7
- package/package.json +1 -1
|
@@ -35,20 +35,6 @@ const MOUSE_ON = "\x1b[?1002h\x1b[?1006h";
|
|
|
35
35
|
const MOUSE_OFF = "\x1b[?1006l\x1b[?1002l";
|
|
36
36
|
const MOUSE_SGR_RE = /\x1b\[<(\d+);(\d+);(\d+)([Mm])/g;
|
|
37
37
|
const SCROLL_STEP = 3;
|
|
38
|
-
function StateIcon({ state }) {
|
|
39
|
-
if (!state)
|
|
40
|
-
return _jsx(Text, { dimColor: true, children: "\u00B7" });
|
|
41
|
-
switch (state) {
|
|
42
|
-
case "active":
|
|
43
|
-
return _jsx(Text, { color: "green", children: "\u25CF" });
|
|
44
|
-
case "waiting":
|
|
45
|
-
return _jsx(Text, { color: "yellow", children: "!" });
|
|
46
|
-
case "idle":
|
|
47
|
-
return _jsx(Text, { dimColor: true, children: "\u25CB" });
|
|
48
|
-
case "exited":
|
|
49
|
-
return _jsx(Text, { dimColor: true, children: "\u2014" });
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
38
|
function truncate(s, max) {
|
|
53
39
|
if (max <= 1)
|
|
54
40
|
return s.slice(0, max);
|
|
@@ -165,7 +151,7 @@ function useTerminalSize() {
|
|
|
165
151
|
function HeaderRow({ repoName, claudeVersion, issueCount, updateAvailable, }) {
|
|
166
152
|
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}) ` }) })] }));
|
|
167
153
|
}
|
|
168
|
-
function FooterRow({ phase, overlayKind, latestVersion, }) {
|
|
154
|
+
function FooterRow({ phase, overlayKind, latestVersion, listWidth, }) {
|
|
169
155
|
if (phase === "error") {
|
|
170
156
|
return (_jsx(Box, { children: _jsx(Text, { dimColor: true, children: "q quit" }) }));
|
|
171
157
|
}
|
|
@@ -175,7 +161,13 @@ function FooterRow({ phase, overlayKind, latestVersion, }) {
|
|
|
175
161
|
if (overlayKind === "remove") {
|
|
176
162
|
return (_jsxs(Box, { children: [_jsx(Text, { bold: true, children: "y/Y" }), _jsx(Text, { dimColor: true, children: " confirm " }), _jsx(Text, { bold: true, children: "n/Esc" }), _jsx(Text, { dimColor: true, children: " cancel" })] }));
|
|
177
163
|
}
|
|
178
|
-
|
|
164
|
+
// Two-column footer like santree: common navigation/dashboard commands
|
|
165
|
+
// align under the left (list) pane; ticket-specific actions align under
|
|
166
|
+
// the right (detail) pane. Falls back to a single inline row when no
|
|
167
|
+
// width hint is available (e.g. the error path).
|
|
168
|
+
const common = (_jsxs(Text, { children: [_jsx(Text, { bold: true, children: "j/k" }), _jsx(Text, { dimColor: true, children: " nav " }), _jsx(Text, { dimColor: true, children: "\u00B7" }), _jsx(Text, { bold: true, children: " PgUp/PgDn" }), _jsx(Text, { dimColor: true, children: " scroll " }), _jsx(Text, { dimColor: true, children: "\u00B7" }), _jsx(Text, { bold: true, children: " r" }), _jsx(Text, { dimColor: true, children: " refresh " }), _jsx(Text, { dimColor: true, children: "\u00B7" }), _jsx(Text, { bold: true, children: " q" }), _jsx(Text, { dimColor: true, children: " quit" })] }));
|
|
169
|
+
const ticket = (_jsxs(Text, { children: [_jsx(Text, { bold: true, children: "\u21B5" }), _jsx(Text, { dimColor: true, children: " Switch " }), _jsx(Text, { dimColor: true, children: "\u00B7" }), _jsx(Text, { bold: true, children: " w" }), _jsx(Text, { dimColor: true, children: " Work " }), _jsx(Text, { dimColor: true, children: "\u00B7" }), _jsx(Text, { bold: true, children: " o" }), _jsx(Text, { dimColor: true, children: " Open " }), _jsx(Text, { dimColor: true, children: "\u00B7" }), _jsx(Text, { bold: true, children: " d" }), _jsx(Text, { dimColor: true, children: " Remove" })] }));
|
|
170
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { width: listWidth, children: common }), _jsx(Box, { flexGrow: 1, children: ticket })] }), latestVersion && (_jsxs(Box, { children: [_jsx(Text, { color: "yellow", children: "(*)" }), _jsx(Text, { dimColor: true, children: ` new version available — v${latestVersion} · npm i -g mintree` })] }))] }));
|
|
179
171
|
}
|
|
180
172
|
function RemoveOverlayView({ overlay }) {
|
|
181
173
|
return (_jsxs(Box, { flexGrow: 1, flexDirection: "column", paddingX: 1, children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, color: "cyan", children: "Remove 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(Text, { children: [_jsx(Text, { dimColor: true, children: "Branch: " }), _jsx(Text, { color: "cyan", children: overlay.branch ?? `(detached) ${overlay.worktreePath}` })] }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "State: " }), overlay.dirty ? (_jsx(Text, { color: "yellow", children: "dirty (uncommitted changes will be lost)" })) : (_jsx(Text, { color: "green", children: "clean" }))] })] }), _jsx(Box, { marginTop: 1, flexDirection: "column", children: _jsx(Text, { dimColor: true, children: "Removing the worktree leaves the branch and the issue's session_id in place. You can re-attach later with `mintree worktree create`." }) }), _jsx(Box, { marginTop: 1, children: overlay.dirty ? (_jsxs(Text, { children: ["This worktree is dirty. Press", " ", _jsx(Text, { bold: true, color: "red", children: "Y" }), " ", "to force-remove, ", _jsx(Text, { bold: true, children: "N" }), "/", _jsx(Text, { bold: true, children: "Esc" }), " to cancel."] })) : (_jsxs(Text, { children: ["Press", " ", _jsx(Text, { bold: true, color: "green", children: "y" }), " ", "to remove, ", _jsx(Text, { bold: true, children: "N" }), "/", _jsx(Text, { bold: true, children: "Esc" }), " to cancel."] })) }), overlay.error && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: "red", bold: true, children: ["\u2717 ", overlay.error] }) }))] }));
|
|
@@ -192,41 +184,33 @@ function CreateOverlayView({ overlay, onDescChange, onPromptChange, }) {
|
|
|
192
184
|
: `${overlay.issue.issue.id}-${detachedDesc}`;
|
|
193
185
|
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] })] }))] }));
|
|
194
186
|
}
|
|
195
|
-
function
|
|
196
|
-
if (!state)
|
|
197
|
-
return "·";
|
|
198
|
-
if (state === "active")
|
|
199
|
-
return "●";
|
|
200
|
-
if (state === "waiting")
|
|
201
|
-
return "!";
|
|
202
|
-
if (state === "idle")
|
|
203
|
-
return "○";
|
|
204
|
-
return "—";
|
|
205
|
-
}
|
|
206
|
-
function IssueListRow({ d, selected, identifierWidth, maxTitleWidth, }) {
|
|
187
|
+
function IssueListRow({ d, selected, identifierWidth, rowWidth, }) {
|
|
207
188
|
// Display the issue id raw (e.g. "AUTH-6", "100"). The `#` prefix is a
|
|
208
189
|
// GitHub convention that reads as noise for Plane's already-prefixed
|
|
209
190
|
// ids, and dropping it across the board keeps the dashboard provider-
|
|
210
191
|
// agnostic.
|
|
211
192
|
const idText = d.issue.id.padEnd(identifierWidth, " ");
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
//
|
|
217
|
-
//
|
|
218
|
-
|
|
219
|
-
|
|
193
|
+
// Status-coloured leading dot — same convention as santree. Falls back to
|
|
194
|
+
// gray when the issue has no project board membership.
|
|
195
|
+
const dotColor = d.project?.statusColor ?? "gray";
|
|
196
|
+
const title = d.issue.title;
|
|
197
|
+
// The leading-dot Text and the rest are nested under a single Text so the
|
|
198
|
+
// selection background paints the whole row in one contiguous block.
|
|
199
|
+
// `wrap="truncate"` clamps the row to a single line and Ink renders an
|
|
200
|
+
// ellipsis at the cut. The outer Box has a fixed width so the wrap
|
|
201
|
+
// behaviour knows where to truncate.
|
|
202
|
+
return (_jsx(Box, { width: rowWidth, children: _jsxs(Text, { wrap: "truncate", backgroundColor: selected ? "blue" : undefined, color: selected ? "white" : undefined, children: [" ", _jsx(Text, { color: selected ? "white" : dotColor, children: "\u25CF" }), ` ${idText} ${title}`] }) }));
|
|
220
203
|
}
|
|
221
204
|
// A project board header — the top level of the grouped issue list. Mirrors
|
|
222
205
|
// the bold project name + dim count seen in the santree dashboard.
|
|
223
206
|
function ProjectHeaderRow({ title, count, width, }) {
|
|
224
207
|
return (_jsxs(Box, { children: [_jsx(Text, { bold: true, color: "cyan", children: truncate(title, Math.max(4, width - 6)) }), _jsx(Text, { dimColor: true, children: ` ${count}` })] }));
|
|
225
208
|
}
|
|
226
|
-
// A Status sub-header within a project group.
|
|
227
|
-
//
|
|
209
|
+
// A Status sub-header within a project group. Matches santree's look: just
|
|
210
|
+
// the status name in its board colour, no leading bullet — the bullets live
|
|
211
|
+
// on the individual issue rows below it.
|
|
228
212
|
function StatusHeaderRow({ name, color, count, width, }) {
|
|
229
|
-
return (_jsxs(Box, { children: [_jsx(Text, { color: color, children:
|
|
213
|
+
return (_jsxs(Box, { children: [_jsx(Text, { color: color, children: ` ${truncate(name, Math.max(4, width - 6))}` }), _jsx(Text, { dimColor: true, children: ` ${count}` })] }));
|
|
230
214
|
}
|
|
231
215
|
/**
|
|
232
216
|
* Walks the already-grouped flat issue array (loadDashboard sorts it by
|
|
@@ -366,7 +350,7 @@ function windowListRows(listRows, selectedIndex, viewportRows) {
|
|
|
366
350
|
}
|
|
367
351
|
// Renders a single grouped-list row — used for both the sticky header region
|
|
368
352
|
// and the scrollable body so the two stay visually identical.
|
|
369
|
-
function ListRowView({ row, selectedIndex, identifierWidth,
|
|
353
|
+
function ListRowView({ row, selectedIndex, identifierWidth, width, }) {
|
|
370
354
|
if (row.kind === "spacer")
|
|
371
355
|
return _jsx(Text, { children: " " });
|
|
372
356
|
if (row.kind === "project") {
|
|
@@ -375,7 +359,7 @@ function ListRowView({ row, selectedIndex, identifierWidth, maxTitleWidth, width
|
|
|
375
359
|
if (row.kind === "status") {
|
|
376
360
|
return _jsx(StatusHeaderRow, { name: row.name, color: row.color, count: row.count, width: width });
|
|
377
361
|
}
|
|
378
|
-
return (_jsx(IssueListRow, { d: row.d, selected: row.index === selectedIndex, identifierWidth: identifierWidth,
|
|
362
|
+
return (_jsx(IssueListRow, { d: row.d, selected: row.index === selectedIndex, identifierWidth: identifierWidth, rowWidth: width }));
|
|
379
363
|
}
|
|
380
364
|
// Word-wraps a single line at `width` columns, breaking on the last space
|
|
381
365
|
// before the limit when that yields a reasonable cut. Falls back to a hard
|
|
@@ -830,7 +814,9 @@ export default function Dashboard() {
|
|
|
830
814
|
}
|
|
831
815
|
if (input === "o") {
|
|
832
816
|
const issue = state.issues[state.selectedIndex];
|
|
833
|
-
|
|
817
|
+
// Orphan rows carry an empty URL — nothing to open. Skip silently
|
|
818
|
+
// rather than asking the OS to open an empty string.
|
|
819
|
+
if (issue && issue.issue.url)
|
|
834
820
|
openInBrowser(issue.issue.url);
|
|
835
821
|
return;
|
|
836
822
|
}
|
|
@@ -1104,15 +1090,13 @@ export default function Dashboard() {
|
|
|
1104
1090
|
overlay: { ...state.overlay, prompt: next, error: null },
|
|
1105
1091
|
});
|
|
1106
1092
|
};
|
|
1107
|
-
// Left pane is the issue list — it
|
|
1108
|
-
//
|
|
1109
|
-
//
|
|
1110
|
-
const listWidthPct = 0.
|
|
1093
|
+
// Left pane is the issue list — santree gives it ~half the width and the
|
|
1094
|
+
// detail pane still has room for URLs, descriptions and branch paths
|
|
1095
|
+
// because long lines wrap within the pane.
|
|
1096
|
+
const listWidthPct = 0.5;
|
|
1111
1097
|
const listWidth = Math.max(32, Math.floor(columns * listWidthPct));
|
|
1112
1098
|
const detailWidth = columns - listWidth - 2; // border slack
|
|
1113
1099
|
const identifierWidth = Math.max(3, ...issues.map((d) => d.issue.id.length));
|
|
1114
|
-
// Lista ocupa todo menos: " #N ICON " (2-space nest indent + id + icon).
|
|
1115
|
-
const maxTitleWidth = Math.max(8, listWidth - identifierWidth - 9);
|
|
1116
1100
|
// Reserve rows: header (2), top borders (1), footer (3).
|
|
1117
1101
|
const listVisibleRows = Math.max(3, rows - 9);
|
|
1118
1102
|
// Detail pane content height inside the bordered box. Header eats 2 rows,
|
|
@@ -1129,5 +1113,5 @@ export default function Dashboard() {
|
|
|
1129
1113
|
const listRows = buildListRows(issues);
|
|
1130
1114
|
const listView = windowListRows(listRows, selectedIndex, listVisibleRows);
|
|
1131
1115
|
const listContentWidth = Math.max(8, listWidth - 4);
|
|
1132
|
-
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,
|
|
1116
|
+
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 })] })] }));
|
|
1133
1117
|
}
|
package/dist/lib/dashboard.d.ts
CHANGED
package/dist/lib/dashboard.js
CHANGED
|
@@ -69,19 +69,22 @@ function isSessionState(v) {
|
|
|
69
69
|
* its group headers by walking this already-ordered array.
|
|
70
70
|
*
|
|
71
71
|
* Project group order: a config-pinned project first, then other projects
|
|
72
|
-
* alphabetically, then issues with no project ("Sin proyecto")
|
|
72
|
+
* alphabetically, then issues with no project ("Sin proyecto"), then orphan
|
|
73
|
+
* worktrees last.
|
|
73
74
|
*/
|
|
74
75
|
function sortGroupedIssues(issues, configuredUrl) {
|
|
75
|
-
const projectTier = (
|
|
76
|
-
if (
|
|
76
|
+
const projectTier = (d) => {
|
|
77
|
+
if (d.orphan)
|
|
78
|
+
return 3;
|
|
79
|
+
if (!d.project)
|
|
77
80
|
return 2;
|
|
78
|
-
if (configuredUrl &&
|
|
81
|
+
if (configuredUrl && d.project.projectUrl === configuredUrl)
|
|
79
82
|
return 0;
|
|
80
83
|
return 1;
|
|
81
84
|
};
|
|
82
85
|
return [...issues].sort((a, b) => {
|
|
83
|
-
const ta = projectTier(a
|
|
84
|
-
const tb = projectTier(b
|
|
86
|
+
const ta = projectTier(a);
|
|
87
|
+
const tb = projectTier(b);
|
|
85
88
|
if (ta !== tb)
|
|
86
89
|
return ta - tb;
|
|
87
90
|
if (a.project && b.project) {
|
|
@@ -102,6 +105,57 @@ function sortGroupedIssues(issues, configuredUrl) {
|
|
|
102
105
|
return b.issue.id.localeCompare(a.issue.id);
|
|
103
106
|
});
|
|
104
107
|
}
|
|
108
|
+
/**
|
|
109
|
+
* Synthetic DashboardIssue rows for worktrees on disk whose issueId isn't in
|
|
110
|
+
* the provider's assigned-issues list. These end up grouped under "Orphaned
|
|
111
|
+
* Worktrees" at the bottom of the dashboard so the user can find and `d`elete
|
|
112
|
+
* them.
|
|
113
|
+
*
|
|
114
|
+
* The `issue` stub uses the worktree directory name as the title (the part
|
|
115
|
+
* after the issue id, e.g. "claude-md-inicial") so the row is identifiable
|
|
116
|
+
* even when there's no live issue to fetch a title from.
|
|
117
|
+
*/
|
|
118
|
+
function buildOrphanRows(worktreesByIssue, assignedIds, sessionLookup, prByBranch, metadataSessionId) {
|
|
119
|
+
const orphans = [];
|
|
120
|
+
for (const [issueId, w] of worktreesByIssue) {
|
|
121
|
+
if (assignedIds.has(issueId))
|
|
122
|
+
continue;
|
|
123
|
+
const dirName = path.basename(w.path);
|
|
124
|
+
// Strip the leading "<issueId>-" — that leaves the kebab description
|
|
125
|
+
// that originally seeded the branch name.
|
|
126
|
+
const desc = dirName.startsWith(`${issueId}-`)
|
|
127
|
+
? dirName.slice(issueId.length + 1)
|
|
128
|
+
: dirName;
|
|
129
|
+
const sessionId = metadataSessionId(issueId);
|
|
130
|
+
const worktree = { ...w, sessionId };
|
|
131
|
+
const pr = w.branch ? (prByBranch.get(w.branch) ?? null) : null;
|
|
132
|
+
orphans.push({
|
|
133
|
+
issue: {
|
|
134
|
+
id: issueId,
|
|
135
|
+
title: desc || dirName,
|
|
136
|
+
state: "UNKNOWN",
|
|
137
|
+
url: "",
|
|
138
|
+
labels: [],
|
|
139
|
+
body: "",
|
|
140
|
+
createdAt: "",
|
|
141
|
+
updatedAt: "",
|
|
142
|
+
},
|
|
143
|
+
worktree,
|
|
144
|
+
session: sessionLookup(issueId),
|
|
145
|
+
pr,
|
|
146
|
+
project: {
|
|
147
|
+
projectTitle: "Orphaned Worktrees",
|
|
148
|
+
projectUrl: "",
|
|
149
|
+
projectNumber: 0,
|
|
150
|
+
status: "Orphaned",
|
|
151
|
+
statusColor: "gray",
|
|
152
|
+
statusOrder: 9999,
|
|
153
|
+
},
|
|
154
|
+
orphan: true,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
return orphans;
|
|
158
|
+
}
|
|
105
159
|
/**
|
|
106
160
|
* Top-level loader: enriches each assigned issue with its worktree and
|
|
107
161
|
* session snapshot. Designed to be called on dashboard mount and on every
|
|
@@ -152,5 +206,7 @@ export async function loadDashboard(repoRoot) {
|
|
|
152
206
|
project: projectByIssue.get(issue.id) ?? null,
|
|
153
207
|
};
|
|
154
208
|
});
|
|
155
|
-
|
|
209
|
+
const assignedIds = new Set(issues.map((i) => i.id));
|
|
210
|
+
const orphans = buildOrphanRows(worktreesByIssue, assignedIds, (id) => readSessionState(repoRoot, id), prByBranch, (id) => metadata.issues[id]?.session_id);
|
|
211
|
+
return sortGroupedIssues([...enriched, ...orphans], configuredUrl);
|
|
156
212
|
}
|
package/package.json
CHANGED