santree 0.2.2 → 0.2.4
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.
|
@@ -924,6 +924,10 @@ export default function Dashboard() {
|
|
|
924
924
|
}
|
|
925
925
|
// Open in Linear
|
|
926
926
|
if (input === "o") {
|
|
927
|
+
if (!di.issue.url) {
|
|
928
|
+
dispatch({ type: "SET_ACTION_MESSAGE", message: "No Linear ticket URL" });
|
|
929
|
+
return;
|
|
930
|
+
}
|
|
927
931
|
const openCmd = process.platform === "darwin" ? "open" : "xdg-open";
|
|
928
932
|
execSync(`${openCmd} "${di.issue.url}"`, { stdio: "ignore" });
|
|
929
933
|
dispatch({ type: "SET_ACTION_MESSAGE", message: "Opened in browser" });
|
|
@@ -8,6 +8,8 @@ function stateColor(type) {
|
|
|
8
8
|
return "blue";
|
|
9
9
|
case "backlog":
|
|
10
10
|
return "gray";
|
|
11
|
+
case "orphaned":
|
|
12
|
+
return "gray";
|
|
11
13
|
default:
|
|
12
14
|
return "yellow";
|
|
13
15
|
}
|
|
@@ -47,7 +49,8 @@ function fileColor(xy) {
|
|
|
47
49
|
return "gray";
|
|
48
50
|
return "yellow";
|
|
49
51
|
}
|
|
50
|
-
function buildActions(
|
|
52
|
+
function buildActions(di) {
|
|
53
|
+
const { worktree, pr, issue } = di;
|
|
51
54
|
const items = [];
|
|
52
55
|
// Work/Resume
|
|
53
56
|
if (worktree?.sessionId) {
|
|
@@ -77,7 +80,9 @@ function buildActions(worktree, pr) {
|
|
|
77
80
|
items.push({ key: "r", label: "Review", color: "cyan" });
|
|
78
81
|
}
|
|
79
82
|
// Links
|
|
80
|
-
|
|
83
|
+
if (issue.url) {
|
|
84
|
+
items.push({ key: "o", label: "Linear", color: "gray" });
|
|
85
|
+
}
|
|
81
86
|
if (pr)
|
|
82
87
|
items.push({ key: "p", label: "Open PR", color: "gray" });
|
|
83
88
|
// Destructive
|
|
@@ -210,7 +215,7 @@ export default function DetailPanel({ issue, scrollOffset, height, width, creati
|
|
|
210
215
|
}
|
|
211
216
|
}
|
|
212
217
|
// ── Build actions footer ──────────────────────────────────────────
|
|
213
|
-
const actionRows = buildActions(
|
|
218
|
+
const actionRows = buildActions(issue);
|
|
214
219
|
// +1 for the separator line
|
|
215
220
|
const actionsHeight = actionRows.length + 1;
|
|
216
221
|
const scrollableHeight = height - actionsHeight;
|
|
@@ -20,9 +20,13 @@ export async function loadDashboardData(repoRoot) {
|
|
|
20
20
|
}
|
|
21
21
|
// Read metadata once for session IDs
|
|
22
22
|
const metadata = readAllMetadata(repoRoot);
|
|
23
|
+
// Track which ticket IDs are consumed by fetched issues
|
|
24
|
+
const consumedTicketIds = new Set();
|
|
23
25
|
// Enrich issues in parallel
|
|
24
26
|
const enriched = await Promise.all(issues.map(async (issue) => {
|
|
25
27
|
const wt = wtMap.get(issue.identifier);
|
|
28
|
+
if (wt)
|
|
29
|
+
consumedTicketIds.add(issue.identifier);
|
|
26
30
|
let worktreeInfo = null;
|
|
27
31
|
let prInfo = null;
|
|
28
32
|
let checksInfo = null;
|
|
@@ -58,6 +62,56 @@ export async function loadDashboardData(repoRoot) {
|
|
|
58
62
|
reviews: reviewsInfo,
|
|
59
63
|
};
|
|
60
64
|
}));
|
|
65
|
+
// Build orphan DashboardIssue objects for worktrees not matched to any fetched issue
|
|
66
|
+
const orphans = await Promise.all([...wtMap.entries()]
|
|
67
|
+
.filter(([tid]) => !consumedTicketIds.has(tid))
|
|
68
|
+
.map(async ([tid, wt]) => {
|
|
69
|
+
const base = getBaseBranch(wt.branch);
|
|
70
|
+
const [gitStatusOutput, ahead, pr] = await Promise.all([
|
|
71
|
+
getGitStatusAsync(wt.path),
|
|
72
|
+
getCommitsAheadAsync(wt.path, base),
|
|
73
|
+
getPRInfoAsync(wt.branch),
|
|
74
|
+
]);
|
|
75
|
+
let checksInfo = null;
|
|
76
|
+
let reviewsInfo = null;
|
|
77
|
+
if (pr) {
|
|
78
|
+
[checksInfo, reviewsInfo] = await Promise.all([
|
|
79
|
+
getPRChecksAsync(pr.number),
|
|
80
|
+
getPRReviewsAsync(pr.number),
|
|
81
|
+
]);
|
|
82
|
+
}
|
|
83
|
+
// Derive a readable title from branch name: strip prefix and ticket ID
|
|
84
|
+
const titleFromBranch = wt.branch
|
|
85
|
+
.replace(/^[^/]+\//, "") // strip prefix (e.g. "feature/")
|
|
86
|
+
.replace(/^[A-Z]+-\d+-?/, "") // strip ticket ID
|
|
87
|
+
.replace(/-/g, " ")
|
|
88
|
+
.trim() || tid;
|
|
89
|
+
return {
|
|
90
|
+
issue: {
|
|
91
|
+
identifier: tid,
|
|
92
|
+
title: titleFromBranch,
|
|
93
|
+
description: null,
|
|
94
|
+
url: "",
|
|
95
|
+
priority: 0,
|
|
96
|
+
priorityLabel: "None",
|
|
97
|
+
state: { name: "Orphaned", type: "orphaned" },
|
|
98
|
+
labels: [],
|
|
99
|
+
projectId: null,
|
|
100
|
+
projectName: null,
|
|
101
|
+
},
|
|
102
|
+
worktree: {
|
|
103
|
+
path: wt.path,
|
|
104
|
+
branch: wt.branch,
|
|
105
|
+
dirty: Boolean(gitStatusOutput),
|
|
106
|
+
commitsAhead: ahead,
|
|
107
|
+
sessionId: metadata[tid]?.session_id ?? null,
|
|
108
|
+
gitStatus: gitStatusOutput,
|
|
109
|
+
},
|
|
110
|
+
pr,
|
|
111
|
+
checks: checksInfo,
|
|
112
|
+
reviews: reviewsInfo,
|
|
113
|
+
};
|
|
114
|
+
}));
|
|
61
115
|
// Group by project
|
|
62
116
|
const groupMap = new Map();
|
|
63
117
|
for (const di of enriched) {
|
|
@@ -98,6 +152,20 @@ export async function loadDashboardData(repoRoot) {
|
|
|
98
152
|
statusGroups,
|
|
99
153
|
};
|
|
100
154
|
});
|
|
155
|
+
// Append orphaned worktrees as a separate group at the bottom
|
|
156
|
+
if (orphans.length > 0) {
|
|
157
|
+
groups.push({
|
|
158
|
+
name: "Orphaned Worktrees",
|
|
159
|
+
id: null,
|
|
160
|
+
statusGroups: [
|
|
161
|
+
{
|
|
162
|
+
name: "Orphaned",
|
|
163
|
+
type: "orphaned",
|
|
164
|
+
issues: orphans,
|
|
165
|
+
},
|
|
166
|
+
],
|
|
167
|
+
});
|
|
168
|
+
}
|
|
101
169
|
const flatIssues = groups.flatMap((g) => g.statusGroups.flatMap((sg) => sg.issues));
|
|
102
170
|
return { groups, flatIssues };
|
|
103
171
|
}
|
package/dist/lib/git.js
CHANGED
|
@@ -263,8 +263,18 @@ export function extractTicketId(branch) {
|
|
|
263
263
|
*/
|
|
264
264
|
export function getWorktreePath(branchName) {
|
|
265
265
|
const worktrees = listWorktrees();
|
|
266
|
+
// Try exact match first
|
|
266
267
|
const wt = worktrees.find((w) => w.branch === branchName);
|
|
267
|
-
|
|
268
|
+
if (wt)
|
|
269
|
+
return wt.path;
|
|
270
|
+
// Fall back to matching by ticket ID
|
|
271
|
+
const inputTicketId = extractTicketId(branchName);
|
|
272
|
+
if (inputTicketId) {
|
|
273
|
+
const byTicket = worktrees.find((w) => w.branch && extractTicketId(w.branch) === inputTicketId);
|
|
274
|
+
if (byTicket)
|
|
275
|
+
return byTicket.path;
|
|
276
|
+
}
|
|
277
|
+
return null;
|
|
268
278
|
}
|
|
269
279
|
/**
|
|
270
280
|
* Get path to centralized metadata file: .santree/metadata.json in the repo root.
|