santree 0.2.3 → 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(worktree, pr) {
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
- items.push({ key: "o", label: "Linear", color: "gray" });
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(worktree, pr);
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;
@@ -15,6 +15,8 @@ function stateColor(type, name) {
15
15
  return "blue";
16
16
  case "backlog":
17
17
  return "gray";
18
+ case "orphaned":
19
+ return "gray";
18
20
  default:
19
21
  return "yellow";
20
22
  }
@@ -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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "santree",
3
- "version": "0.2.3",
3
+ "version": "0.2.4",
4
4
  "description": "Git worktree manager",
5
5
  "license": "MIT",
6
6
  "author": "Santiago Toscanini",