mintree 0.2.1 → 0.2.3
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.
|
@@ -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);
|
|
@@ -112,11 +98,15 @@ function kebabize(title) {
|
|
|
112
98
|
* Default prompt seeded into the overlay's Prompt field when the user opens
|
|
113
99
|
* `w` for an issue. Single-line on purpose — `ink-text-input` is one-line,
|
|
114
100
|
* so multi-line templates render weirdly when the user tabs in to edit.
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
101
|
+
* Provider-aware: GitHub issues get the `#<n>` + `gh issue view` form;
|
|
102
|
+
* Plane work items (id like `DSGN-1`) get the bare id + the issue URL,
|
|
103
|
+
* since `gh` can't read Plane and `#` isn't Plane's notation.
|
|
118
104
|
*/
|
|
119
|
-
function defaultPromptForIssue(id, title) {
|
|
105
|
+
function defaultPromptForIssue(id, title, url) {
|
|
106
|
+
const isPlane = /^[A-Z][A-Z0-9_]*-\d+$/.test(id);
|
|
107
|
+
if (isPlane) {
|
|
108
|
+
return `Empezá a trabajar el ticket ${id} (${title}). Abrí ${url} para leer el contexto completo y seguí las convenciones del repo.`;
|
|
109
|
+
}
|
|
120
110
|
return `Empezá a trabajar el issue #${id} (${title}). Usá \`gh issue view ${id}\` para leer el contexto completo y seguí las convenciones del repo.`;
|
|
121
111
|
}
|
|
122
112
|
/**
|
|
@@ -165,7 +155,7 @@ function useTerminalSize() {
|
|
|
165
155
|
function HeaderRow({ repoName, claudeVersion, issueCount, updateAvailable, }) {
|
|
166
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}) ` }) })] }));
|
|
167
157
|
}
|
|
168
|
-
function FooterRow({ phase, overlayKind, latestVersion, }) {
|
|
158
|
+
function FooterRow({ phase, overlayKind, latestVersion, listWidth, }) {
|
|
169
159
|
if (phase === "error") {
|
|
170
160
|
return (_jsx(Box, { children: _jsx(Text, { dimColor: true, children: "q quit" }) }));
|
|
171
161
|
}
|
|
@@ -175,7 +165,13 @@ function FooterRow({ phase, overlayKind, latestVersion, }) {
|
|
|
175
165
|
if (overlayKind === "remove") {
|
|
176
166
|
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
167
|
}
|
|
178
|
-
|
|
168
|
+
// Two-column footer like santree: common navigation/dashboard commands
|
|
169
|
+
// align under the left (list) pane; ticket-specific actions align under
|
|
170
|
+
// the right (detail) pane. Falls back to a single inline row when no
|
|
171
|
+
// width hint is available (e.g. the error path).
|
|
172
|
+
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" })] }));
|
|
173
|
+
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" })] }));
|
|
174
|
+
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
175
|
}
|
|
180
176
|
function RemoveOverlayView({ overlay }) {
|
|
181
177
|
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 +188,33 @@ function CreateOverlayView({ overlay, onDescChange, onPromptChange, }) {
|
|
|
192
188
|
: `${overlay.issue.issue.id}-${detachedDesc}`;
|
|
193
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] })] }))] }));
|
|
194
190
|
}
|
|
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, }) {
|
|
191
|
+
function IssueListRow({ d, selected, identifierWidth, rowWidth, }) {
|
|
207
192
|
// Display the issue id raw (e.g. "AUTH-6", "100"). The `#` prefix is a
|
|
208
193
|
// GitHub convention that reads as noise for Plane's already-prefixed
|
|
209
194
|
// ids, and dropping it across the board keeps the dashboard provider-
|
|
210
195
|
// agnostic.
|
|
211
196
|
const idText = d.issue.id.padEnd(identifierWidth, " ");
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
//
|
|
217
|
-
//
|
|
218
|
-
|
|
219
|
-
|
|
197
|
+
// Status-coloured leading dot — same convention as santree. Falls back to
|
|
198
|
+
// gray when the issue has no project board membership.
|
|
199
|
+
const dotColor = d.project?.statusColor ?? "gray";
|
|
200
|
+
const title = d.issue.title;
|
|
201
|
+
// The leading-dot Text and the rest are nested under a single Text so the
|
|
202
|
+
// selection background paints the whole row in one contiguous block.
|
|
203
|
+
// `wrap="truncate"` clamps the row to a single line and Ink renders an
|
|
204
|
+
// ellipsis at the cut. The outer Box has a fixed width so the wrap
|
|
205
|
+
// behaviour knows where to truncate.
|
|
206
|
+
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
207
|
}
|
|
221
208
|
// A project board header — the top level of the grouped issue list. Mirrors
|
|
222
209
|
// the bold project name + dim count seen in the santree dashboard.
|
|
223
210
|
function ProjectHeaderRow({ title, count, width, }) {
|
|
224
211
|
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
212
|
}
|
|
226
|
-
// A Status sub-header within a project group.
|
|
227
|
-
//
|
|
213
|
+
// A Status sub-header within a project group. Matches santree's look: just
|
|
214
|
+
// the status name in its board colour, no leading bullet — the bullets live
|
|
215
|
+
// on the individual issue rows below it.
|
|
228
216
|
function StatusHeaderRow({ name, color, count, width, }) {
|
|
229
|
-
return (_jsxs(Box, { children: [_jsx(Text, { color: color, children:
|
|
217
|
+
return (_jsxs(Box, { children: [_jsx(Text, { color: color, children: ` ${truncate(name, Math.max(4, width - 6))}` }), _jsx(Text, { dimColor: true, children: ` ${count}` })] }));
|
|
230
218
|
}
|
|
231
219
|
/**
|
|
232
220
|
* Walks the already-grouped flat issue array (loadDashboard sorts it by
|
|
@@ -366,7 +354,7 @@ function windowListRows(listRows, selectedIndex, viewportRows) {
|
|
|
366
354
|
}
|
|
367
355
|
// Renders a single grouped-list row — used for both the sticky header region
|
|
368
356
|
// and the scrollable body so the two stay visually identical.
|
|
369
|
-
function ListRowView({ row, selectedIndex, identifierWidth,
|
|
357
|
+
function ListRowView({ row, selectedIndex, identifierWidth, width, }) {
|
|
370
358
|
if (row.kind === "spacer")
|
|
371
359
|
return _jsx(Text, { children: " " });
|
|
372
360
|
if (row.kind === "project") {
|
|
@@ -375,7 +363,7 @@ function ListRowView({ row, selectedIndex, identifierWidth, maxTitleWidth, width
|
|
|
375
363
|
if (row.kind === "status") {
|
|
376
364
|
return _jsx(StatusHeaderRow, { name: row.name, color: row.color, count: row.count, width: width });
|
|
377
365
|
}
|
|
378
|
-
return (_jsx(IssueListRow, { d: row.d, selected: row.index === selectedIndex, identifierWidth: identifierWidth,
|
|
366
|
+
return (_jsx(IssueListRow, { d: row.d, selected: row.index === selectedIndex, identifierWidth: identifierWidth, rowWidth: width }));
|
|
379
367
|
}
|
|
380
368
|
// Word-wraps a single line at `width` columns, breaking on the last space
|
|
381
369
|
// before the limit when that yields a reasonable cut. Falls back to a hard
|
|
@@ -830,7 +818,9 @@ export default function Dashboard() {
|
|
|
830
818
|
}
|
|
831
819
|
if (input === "o") {
|
|
832
820
|
const issue = state.issues[state.selectedIndex];
|
|
833
|
-
|
|
821
|
+
// Orphan rows carry an empty URL — nothing to open. Skip silently
|
|
822
|
+
// rather than asking the OS to open an empty string.
|
|
823
|
+
if (issue && issue.issue.url)
|
|
834
824
|
openInBrowser(issue.issue.url);
|
|
835
825
|
return;
|
|
836
826
|
}
|
|
@@ -893,7 +883,7 @@ export default function Dashboard() {
|
|
|
893
883
|
currentBranch: root ? getCurrentBranch(root) : null,
|
|
894
884
|
type: "feat",
|
|
895
885
|
desc: kebabize(issue.issue.title) || `issue-${issue.issue.id}`,
|
|
896
|
-
prompt: defaultPromptForIssue(issue.issue.id, issue.issue.title),
|
|
886
|
+
prompt: defaultPromptForIssue(issue.issue.id, issue.issue.title, issue.issue.url),
|
|
897
887
|
field: "branchMode",
|
|
898
888
|
error: null,
|
|
899
889
|
conventionDoc: root ? findBranchConventionDoc(root) : null,
|
|
@@ -1104,15 +1094,13 @@ export default function Dashboard() {
|
|
|
1104
1094
|
overlay: { ...state.overlay, prompt: next, error: null },
|
|
1105
1095
|
});
|
|
1106
1096
|
};
|
|
1107
|
-
// Left pane is the issue list — it
|
|
1108
|
-
//
|
|
1109
|
-
//
|
|
1110
|
-
const listWidthPct = 0.
|
|
1097
|
+
// Left pane is the issue list — santree gives it ~half the width and the
|
|
1098
|
+
// detail pane still has room for URLs, descriptions and branch paths
|
|
1099
|
+
// because long lines wrap within the pane.
|
|
1100
|
+
const listWidthPct = 0.5;
|
|
1111
1101
|
const listWidth = Math.max(32, Math.floor(columns * listWidthPct));
|
|
1112
1102
|
const detailWidth = columns - listWidth - 2; // border slack
|
|
1113
1103
|
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
1104
|
// Reserve rows: header (2), top borders (1), footer (3).
|
|
1117
1105
|
const listVisibleRows = Math.max(3, rows - 9);
|
|
1118
1106
|
// Detail pane content height inside the bordered box. Header eats 2 rows,
|
|
@@ -1129,5 +1117,5 @@ export default function Dashboard() {
|
|
|
1129
1117
|
const listRows = buildListRows(issues);
|
|
1130
1118
|
const listView = windowListRows(listRows, selectedIndex, listVisibleRows);
|
|
1131
1119
|
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,
|
|
1120
|
+
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
1121
|
}
|
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
|
}
|
|
@@ -362,7 +362,7 @@ function mapWorkItemToProviderIssue(project, workspaceSlug, wi) {
|
|
|
362
362
|
}
|
|
363
363
|
}
|
|
364
364
|
const state = normaliseState(wi.state);
|
|
365
|
-
const url = `https://app.plane.so/${workspaceSlug}/
|
|
365
|
+
const url = `https://app.plane.so/${workspaceSlug}/browse/${project.identifier}-${wi.sequence_id}/`;
|
|
366
366
|
return {
|
|
367
367
|
id: toIssueId(project, wi.sequence_id),
|
|
368
368
|
title: wi.name,
|
package/package.json
CHANGED