dsh-git-badge 0.5.5 → 0.7.0
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/CHANGELOG.md +141 -0
- package/README.md +9 -3
- package/lib/client.js +325 -121
- package/lib/index.js +528 -127
- package/package.json +5 -1
package/lib/client.js
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* dsh-git-badge — client half.
|
|
3
3
|
*
|
|
4
|
+
* Both surfaces show the SAME status mark — a circle, or a tree for a linked
|
|
5
|
+
* worktree, filled with the status colour — so status and worktree-ness read
|
|
6
|
+
* identically wherever they appear.
|
|
7
|
+
*
|
|
4
8
|
* Surfaces:
|
|
5
|
-
* - sidebar.workspaces.row (seam):
|
|
6
|
-
*
|
|
9
|
+
* - sidebar.workspaces.row (seam): row badge — workspace name, the status mark
|
|
10
|
+
* floated right, and a worktree's name beside it. Deliberately shows NO
|
|
11
|
+
* branch: the chip is the surface that names it.
|
|
12
|
+
* - conversation.input.left (upstream): chip — status mark + branch + a
|
|
13
|
+
* worktree's name + in-progress operation token + sync/dirty suffix
|
|
7
14
|
* The hover card is intentionally untouched.
|
|
8
15
|
*/
|
|
9
16
|
window.__ModuleLoader__.load({
|
|
@@ -22,6 +29,19 @@ window.__ModuleLoader__.load({
|
|
|
22
29
|
/** Slow safety-net poll: refreshes even if the SSE stream is silently dead. */
|
|
23
30
|
const FALLBACK_POLL_MS = 60000;
|
|
24
31
|
|
|
32
|
+
/**
|
|
33
|
+
* A request target names WHO is asking, never a filesystem path — the node
|
|
34
|
+
* half resolves the directory itself, so a surface never needs to know
|
|
35
|
+
* one. The chip targets its conversation (`session`); a sidebar row
|
|
36
|
+
* targets the workspace id the seam hands it. The query string doubles as
|
|
37
|
+
* the cache key.
|
|
38
|
+
*/
|
|
39
|
+
function targetQuery(target) {
|
|
40
|
+
if (target === void 0 || target.id === void 0) return void 0;
|
|
41
|
+
const param = target.kind === "workspace" ? "workspace" : "session";
|
|
42
|
+
return param + "=" + encodeURIComponent(target.id);
|
|
43
|
+
}
|
|
44
|
+
|
|
25
45
|
//#region SSE change feed (one EventSource per page, ref-counted)
|
|
26
46
|
const eventListeners = new Set();
|
|
27
47
|
let eventSource = null;
|
|
@@ -32,25 +52,27 @@ window.__ModuleLoader__.load({
|
|
|
32
52
|
eventRefs += 1;
|
|
33
53
|
if (eventSource === null) {
|
|
34
54
|
eventSource = new EventSource("/api/git-badge/events");
|
|
35
|
-
|
|
36
|
-
|
|
55
|
+
// NAMED frames: the node half writes `event: change`, and onmessage
|
|
56
|
+
// never fires for a named event
|
|
57
|
+
eventSource.addEventListener("change", (message) => {
|
|
58
|
+
let payload;
|
|
37
59
|
try {
|
|
38
|
-
|
|
60
|
+
payload = JSON.parse(message.data);
|
|
39
61
|
} catch {
|
|
40
62
|
return;
|
|
41
63
|
}
|
|
42
|
-
//
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
64
|
+
// The server names the workspace it watched, so a subscriber that
|
|
65
|
+
// asked by id matches without ever knowing a path. Each
|
|
66
|
+
// subscriber refetches itself, and the per-key in-flight map
|
|
67
|
+
// collapses the resulting burst into one request.
|
|
46
68
|
for (const fn of [...eventListeners]) {
|
|
47
69
|
try {
|
|
48
|
-
fn(
|
|
70
|
+
fn(payload);
|
|
49
71
|
} catch {
|
|
50
72
|
/* one bad subscriber must not starve the rest */
|
|
51
73
|
}
|
|
52
74
|
}
|
|
53
|
-
};
|
|
75
|
+
});
|
|
54
76
|
}
|
|
55
77
|
return () => {
|
|
56
78
|
eventListeners.delete(onChange);
|
|
@@ -65,76 +87,122 @@ window.__ModuleLoader__.load({
|
|
|
65
87
|
|
|
66
88
|
/**
|
|
67
89
|
* Shared git-status hook. Fetches once on mount and then only when the
|
|
68
|
-
* node half's watcher reports a change for this
|
|
69
|
-
*
|
|
70
|
-
*
|
|
90
|
+
* node half's watcher reports a change for this target (SSE), plus a slow
|
|
91
|
+
* safety-net poll in case the stream dies silently. Returns undefined
|
|
92
|
+
* while loading and for non-git workspaces.
|
|
71
93
|
*/
|
|
72
|
-
function useGitStatus(
|
|
73
|
-
const cacheKey =
|
|
94
|
+
function useGitStatus(target) {
|
|
95
|
+
const cacheKey = targetQuery(target);
|
|
74
96
|
const hit = cacheKey === void 0 ? void 0 : GIT_CACHE.get(cacheKey);
|
|
75
|
-
|
|
97
|
+
// state carries the key it belongs to: switching conversations changes
|
|
98
|
+
// the key, and showing the previous session's badge until the new fetch
|
|
99
|
+
// lands would be wrong
|
|
100
|
+
const [state, setState] = react.useState({ key: cacheKey, data: hit === void 0 ? void 0 : hit.data });
|
|
76
101
|
react.useEffect(() => {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
//
|
|
81
|
-
//
|
|
82
|
-
//
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
pending = fetch("/api/git-badge?path=" + encodeURIComponent(cwd))
|
|
92
|
-
.then((r) => r.json())
|
|
93
|
-
.finally(() => GIT_INFLIGHT.delete(cacheKey));
|
|
94
|
-
GIT_INFLIGHT.set(cacheKey, pending);
|
|
102
|
+
if (cacheKey === void 0) return;
|
|
103
|
+
let alive = true;
|
|
104
|
+
// The workspace this target resolved to, taken from the response (the
|
|
105
|
+
// server echoes it). THIS is what events are matched against: an event
|
|
106
|
+
// carries a workspace id, and a session-targeted chip has only a
|
|
107
|
+
// session id, so comparing the two directly never matches.
|
|
108
|
+
let resolvedWorkspace;
|
|
109
|
+
// Re-seed from cache when the key changes (switching conversations):
|
|
110
|
+
// render the last-known badge immediately instead of blanking until the
|
|
111
|
+
// fetch lands, and take the workspace id from it so events still match.
|
|
112
|
+
const cached = GIT_CACHE.get(cacheKey);
|
|
113
|
+
if (cached !== void 0) {
|
|
114
|
+
if (typeof cached.data?.workspace === "string") resolvedWorkspace = cached.data.workspace;
|
|
115
|
+
setState({ key: cacheKey, data: cached.data });
|
|
95
116
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
117
|
+
const apply = (data) => {
|
|
118
|
+
// A degraded response (git timeout/failure) must never clobber a
|
|
119
|
+
// good cached badge — keep the last-known state until a real
|
|
120
|
+
// event or the fallback poll succeeds.
|
|
121
|
+
if (data !== null && data.error !== void 0 && GIT_CACHE.has(cacheKey)) return;
|
|
122
|
+
if (data !== null && typeof data.workspace === "string") resolvedWorkspace = data.workspace;
|
|
123
|
+
GIT_CACHE.set(cacheKey, { at: Date.now(), data });
|
|
124
|
+
if (alive) setState({ key: cacheKey, data });
|
|
125
|
+
};
|
|
126
|
+
const load = () => {
|
|
127
|
+
// dedupe: an in-flight fetch for this key serves all callers
|
|
128
|
+
let pending = GIT_INFLIGHT.get(cacheKey);
|
|
129
|
+
if (pending === void 0) {
|
|
130
|
+
pending = fetch("/api/git-badge?" + cacheKey)
|
|
131
|
+
.then((r) => r.json())
|
|
132
|
+
.finally(() => GIT_INFLIGHT.delete(cacheKey));
|
|
133
|
+
GIT_INFLIGHT.set(cacheKey, pending);
|
|
134
|
+
}
|
|
135
|
+
pending.then(apply).catch(() => {});
|
|
136
|
+
};
|
|
137
|
+
load();
|
|
138
|
+
const unsubscribe = subscribeGitEvents((payload) => {
|
|
139
|
+
// An event with no workspace id (no watcher record for that path)
|
|
140
|
+
// cannot be attributed, and neither can one arriving before we have
|
|
141
|
+
// resolved our own target — refetch rather than guess, since a
|
|
142
|
+
// missed refresh is the bug this guards and an extra request is cheap
|
|
143
|
+
// (the in-flight map collapses bursts).
|
|
144
|
+
if (payload.workspace === void 0 || resolvedWorkspace === void 0 || payload.workspace === resolvedWorkspace) load();
|
|
145
|
+
});
|
|
146
|
+
const fallback = setInterval(load, FALLBACK_POLL_MS);
|
|
147
|
+
return () => {
|
|
148
|
+
alive = false;
|
|
149
|
+
unsubscribe();
|
|
150
|
+
clearInterval(fallback);
|
|
151
|
+
};
|
|
152
|
+
}, [cacheKey]);
|
|
153
|
+
return state.key === cacheKey ? state.data : void 0;
|
|
111
154
|
}
|
|
112
155
|
|
|
113
156
|
/**
|
|
114
|
-
* Three-state
|
|
115
|
-
*
|
|
116
|
-
* - RED: unmerged files (mid-conflict, git is blocked) — or a dirty
|
|
157
|
+
* Three-state status, checked top-down, first match wins:
|
|
158
|
+
* - "error": unmerged files (mid-conflict, git is blocked) — or a dirty
|
|
117
159
|
* tree that is ALSO behind upstream (unsaved edits on an outdated
|
|
118
160
|
* base: commit-then-pull friction ahead).
|
|
119
|
-
* -
|
|
161
|
+
* - "warn": dirty files, or any ahead/behind (routine work or sync
|
|
120
162
|
* pending — including the previously "quiet" green-but-↓1 case).
|
|
121
|
-
* -
|
|
163
|
+
* - "ok": clean and in sync.
|
|
122
164
|
* Red never means merely "behind": a clean tree one commit behind is
|
|
123
165
|
* normal between pulls, not an alarm.
|
|
166
|
+
*
|
|
167
|
+
* The single source of truth for that summary. BOTH surfaces render it the
|
|
168
|
+
* same way (see StatusMark): a circle, or a tree in a linked worktree,
|
|
169
|
+
* filled with the colour below.
|
|
124
170
|
*/
|
|
125
|
-
function
|
|
171
|
+
function badgeStatus(info) {
|
|
126
172
|
const dirty = info.dirty === true;
|
|
127
173
|
const behind = (info.behind || 0) > 0;
|
|
128
174
|
const ahead = (info.ahead || 0) > 0;
|
|
129
175
|
const conflict = (info.unmergedFiles || 0) > 0;
|
|
130
|
-
if (conflict || (dirty && behind)) return "
|
|
131
|
-
if (dirty || ahead || behind) return "
|
|
132
|
-
return "
|
|
176
|
+
if (conflict || (dirty && behind)) return "error";
|
|
177
|
+
if (dirty || ahead || behind) return "warn";
|
|
178
|
+
return "ok";
|
|
133
179
|
}
|
|
134
180
|
|
|
135
181
|
/**
|
|
136
|
-
*
|
|
137
|
-
*
|
|
182
|
+
* Mark fill per status, from the app's own semantic tokens so BOTH surfaces
|
|
183
|
+
* follow light/dark and custom themes — which a hardcoded emoji cannot do.
|
|
184
|
+
* The literals are only fallbacks for use outside DSH.
|
|
185
|
+
*/
|
|
186
|
+
const MARK_FILL = {
|
|
187
|
+
error: "var(--dsw-alias-state-error-primary, #e5484d)",
|
|
188
|
+
warn: "var(--dsw-alias-state-warn-primary, #d29922)",
|
|
189
|
+
ok: "var(--dsw-alias-state-success-primary, #30a46c)"
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Accessible name per status, WITHOUT the "git" prefix — the mark adds
|
|
194
|
+
* "git: " or "git worktree: ", so shape and colour are never the only
|
|
195
|
+
* channel for either fact.
|
|
196
|
+
*/
|
|
197
|
+
const STATUS_LABEL = {
|
|
198
|
+
error: "conflict, or uncommitted changes on an outdated base",
|
|
199
|
+
warn: "uncommitted changes, or out of sync with upstream",
|
|
200
|
+
ok: "clean and in sync"
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Sync/dirty suffix — INPUT CHIP ONLY. The sidebar row shows status alone;
|
|
205
|
+
* the chip is the surface that carries the numbers. Rule set:
|
|
138
206
|
* - dirty files render as ✎n (pencil = worktree files, distinct from
|
|
139
207
|
* the ↑/↓ commit-sync axis)
|
|
140
208
|
* - while dirty, BOTH sync counts render including zeros (↑0 ↓2 ✎3),
|
|
@@ -154,6 +222,68 @@ window.__ModuleLoader__.load({
|
|
|
154
222
|
return parts.length > 0 ? " " + parts.join(" ") : "";
|
|
155
223
|
}
|
|
156
224
|
|
|
225
|
+
/**
|
|
226
|
+
* In-progress operation token — INPUT CHIP ONLY (the row shows the status
|
|
227
|
+
* tree alone). A paused rebase or cherry-pick whose conflicts are all
|
|
228
|
+
* already staged has no unmerged files, so the three-state summary cannot
|
|
229
|
+
* distinguish it from ordinary dirty work; this says which
|
|
230
|
+
* history-rewriting operation git is waiting on.
|
|
231
|
+
*/
|
|
232
|
+
const OPERATION_LABELS = {
|
|
233
|
+
merge: "\u2694merge",
|
|
234
|
+
squash: "\u2694squash",
|
|
235
|
+
"cherry-pick": "\u2694cherry-pick",
|
|
236
|
+
revert: "\u2694revert",
|
|
237
|
+
bisect: "\u2694bisect",
|
|
238
|
+
rebase: "\u2694rebase",
|
|
239
|
+
sequencer: "\u2694sequencer"
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
/** ` ⚔rebase` when an operation is paused, else "". */
|
|
243
|
+
function formatOperationToken(info) {
|
|
244
|
+
const label = OPERATION_LABELS[info.operation];
|
|
245
|
+
return label === void 0 ? "" : " " + label;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Linked-worktree NAME token — INPUT CHIP ONLY, and only the name: worktree-
|
|
250
|
+
* ness itself is carried by the status mark's shape (a tree instead of a
|
|
251
|
+
* circle) on both surfaces. The chip has no other workspace identity, so
|
|
252
|
+
* several worktrees of one repository would otherwise render identical chips
|
|
253
|
+
* with no way to tell which checkout a conversation is in. The node half
|
|
254
|
+
* reports whether this is a linked worktree plus its directory NAME (never a
|
|
255
|
+
* path).
|
|
256
|
+
*
|
|
257
|
+
* The name is appended only when it says something the branch does not: the
|
|
258
|
+
* usual `repo-feat-x` directory sitting on branch `feat-x` would otherwise
|
|
259
|
+
* read as stutter. (The ROW needs no such rule — it shows the name instead
|
|
260
|
+
* of the branch, so there is nothing to stutter against.)
|
|
261
|
+
*/
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* True when the worktree name is already implied by the branch name, so
|
|
265
|
+
* showing both is pure repetition. Compared on a normalized form —
|
|
266
|
+
* case-folded, with `/` and `_` folded to `-` — because directory and
|
|
267
|
+
* branch conventions differ without changing the meaning (`feat/x` in a
|
|
268
|
+
* `repo-feat-x` directory).
|
|
269
|
+
*/
|
|
270
|
+
function worktreeNameIsRedundant(name, branch) {
|
|
271
|
+
if (typeof name !== "string" || name === "" || typeof branch !== "string") return true;
|
|
272
|
+
const normalize = (value) => value.toLowerCase().replace(/[/_]/g, "-");
|
|
273
|
+
const normalizedName = normalize(name);
|
|
274
|
+
const normalizedBranch = normalize(branch);
|
|
275
|
+
return normalizedName === normalizedBranch
|
|
276
|
+
|| normalizedName.endsWith("-" + normalizedBranch)
|
|
277
|
+
|| normalizedBranch.endsWith("-" + normalizedName);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** ` name` for a worktree whose name the branch does not already say, else "". */
|
|
281
|
+
function formatWorktreeToken(info) {
|
|
282
|
+
if (info.isWorktree !== true) return "";
|
|
283
|
+
const name = typeof info.worktreeName === "string" ? info.worktreeName : "";
|
|
284
|
+
return worktreeNameIsRedundant(name, info.branch) ? "" : " " + name;
|
|
285
|
+
}
|
|
286
|
+
|
|
157
287
|
const META_STYLE = {
|
|
158
288
|
color: "var(--dsw-alias-label-tertiary, #9ea7ad)",
|
|
159
289
|
fontSize: "12px",
|
|
@@ -163,66 +293,122 @@ window.__ModuleLoader__.load({
|
|
|
163
293
|
};
|
|
164
294
|
|
|
165
295
|
/**
|
|
166
|
-
*
|
|
167
|
-
* the input chip
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
*
|
|
171
|
-
*
|
|
172
|
-
*
|
|
296
|
+
* Status mark, SHARED BY BOTH SURFACES: the same 12px shape in the same fill
|
|
297
|
+
* wherever it appears, so the sidebar row and the input chip read
|
|
298
|
+
* identically. The FILL is the status colour; the SHAPE carries
|
|
299
|
+
* worktree-ness — a circle for a main checkout, a tree for a linked
|
|
300
|
+
* worktree — so the two facts never compete for the same channel.
|
|
301
|
+
*
|
|
302
|
+
* Drawn rather than typed because 🌳 is a COLOUR EMOJI: CSS cannot tint
|
|
303
|
+
* its leaves, and a mark that carries the status has to be paintable.
|
|
304
|
+
* Three overlapping crown lobes plus a trunk stay legible at this size.
|
|
305
|
+
*
|
|
306
|
+
* Neither colour nor shape is the only channel: the accessible name
|
|
307
|
+
* states both.
|
|
173
308
|
*/
|
|
174
|
-
function
|
|
175
|
-
const
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
309
|
+
function StatusMark({ info }) {
|
|
310
|
+
const status = badgeStatus(info);
|
|
311
|
+
const fill = MARK_FILL[status];
|
|
312
|
+
const isWorktree = info.isWorktree === true;
|
|
313
|
+
const svg = {
|
|
314
|
+
// 14px, NOT 12px: a colour emoji draws well above its nominal size — the
|
|
315
|
+
// 🔴/🟡/🟢 dot this replaces put down ~13-14px of ink at the surfaces'
|
|
316
|
+
// 12px font-size, so a 12px box read as a shrunken dot.
|
|
317
|
+
width: 14,
|
|
318
|
+
height: 14,
|
|
319
|
+
viewBox: "0 0 12 12",
|
|
320
|
+
role: "img",
|
|
321
|
+
"aria-label": (isWorktree ? "git worktree: " : "git: ") + STATUS_LABEL[status],
|
|
322
|
+
focusable: "false",
|
|
323
|
+
style: { flex: "none", display: "block", color: META_STYLE.color }
|
|
324
|
+
};
|
|
325
|
+
if (!isWorktree) {
|
|
326
|
+
// a plain filled disc: this is a main checkout. It fills its box so the
|
|
327
|
+
// ink lands at the size the emoji dot used to.
|
|
328
|
+
return react_jsx_runtime.jsx("svg", { ...svg, children: react_jsx_runtime.jsx("circle", { cx: 6, cy: 6, r: 5.5, fill }) });
|
|
183
329
|
}
|
|
184
|
-
return react_jsx_runtime.
|
|
330
|
+
return react_jsx_runtime.jsxs("svg", {
|
|
331
|
+
...svg,
|
|
332
|
+
children: [
|
|
333
|
+
// trunk first, so the crown's lobes overlap and join it. These are the
|
|
334
|
+
// 12px-era coordinates scaled 1.15 about the tree's centre, so its ink
|
|
335
|
+
// stays comparable to the disc's instead of reading small beside it.
|
|
336
|
+
react_jsx_runtime.jsx("rect", { key: "trunk", x: 5.25, y: 7.3, width: 1.5, height: 4.14, rx: 0.63, fill: "currentColor" }),
|
|
337
|
+
react_jsx_runtime.jsx("circle", { key: "crown-1", cx: 6, cy: 3.62, r: 3.57, fill }),
|
|
338
|
+
react_jsx_runtime.jsx("circle", { key: "crown-2", cx: 3.24, cy: 5.46, r: 2.53, fill }),
|
|
339
|
+
react_jsx_runtime.jsx("circle", { key: "crown-3", cx: 8.76, cy: 5.46, r: 2.53, fill })
|
|
340
|
+
]
|
|
341
|
+
});
|
|
185
342
|
}
|
|
186
343
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
344
|
+
/**
|
|
345
|
+
* A linked worktree's directory name, or null when there is nothing to
|
|
346
|
+
* show. A main checkout shows NO text: the row surveys status and identity,
|
|
347
|
+
* and the branch lives on the input chip — which is why the branch is never
|
|
348
|
+
* rendered here. The name is the one thing the row's own label (the
|
|
349
|
+
* worktree directory by default) cannot be trusted to say once a workspace
|
|
350
|
+
* has been renamed.
|
|
351
|
+
*/
|
|
352
|
+
function rowName(info) {
|
|
353
|
+
if (info.isWorktree !== true) return null;
|
|
354
|
+
const name = typeof info.worktreeName === "string" ? info.worktreeName : "";
|
|
355
|
+
return name === "" ? null : name;
|
|
356
|
+
}
|
|
190
357
|
|
|
191
358
|
/**
|
|
192
|
-
*
|
|
193
|
-
*
|
|
194
|
-
*
|
|
359
|
+
* Row badge — workspace name on the left, the status mark floated right:
|
|
360
|
+
* the_paragliding_app ●
|
|
361
|
+
* worktree_thing hotfix-tree 🌳
|
|
362
|
+
* Fill colour via badgeStatus(): green clean+synced, amber dirty or
|
|
363
|
+
* out-of-sync, red conflict or dirty-and-behind. Shape via the node half's
|
|
364
|
+
* isWorktree: circle = main checkout, tree = linked worktree.
|
|
365
|
+
* Always renders the workspace name (so the row keeps its identity) and
|
|
366
|
+
* appends the right-hand mark only for git workspaces. No branch, and no
|
|
367
|
+
* `|` separator.
|
|
368
|
+
*
|
|
369
|
+
* Targets the row's `workspaceId`. The seam also passes `cwd`, which is
|
|
370
|
+
* deliberately ignored: the node half resolves the directory itself, so
|
|
371
|
+
* the client never has to name one. A row with no workspaceId (the
|
|
372
|
+
* ungrouped bucket) has no workspace to report on, so it renders
|
|
373
|
+
* name-only.
|
|
195
374
|
*/
|
|
196
|
-
function
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
});
|
|
210
|
-
}
|
|
211
|
-
return
|
|
375
|
+
function WorkspaceGitBadge({ label, workspaceId }) {
|
|
376
|
+
// no detail=1: the row needs status and identity only, and the extra
|
|
377
|
+
// log / stash calls have no consumer yet
|
|
378
|
+
const info = useGitStatus(workspaceId === void 0 ? void 0 : { kind: "workspace", id: workspaceId });
|
|
379
|
+
const children = [react_jsx_runtime.jsx("span", { style: { minWidth: 0, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }, children: label })];
|
|
380
|
+
if (workspaceId !== void 0 && info !== void 0 && info.git === true) {
|
|
381
|
+
// marginLeft:auto floats the mark to the right edge, so the status
|
|
382
|
+
// shape holds a fixed right-hand column and a worktree's name grows
|
|
383
|
+
// leftwards from it instead of shoving the shape around.
|
|
384
|
+
const meta = [];
|
|
385
|
+
const name = rowName(info);
|
|
386
|
+
if (name !== null) meta.push(react_jsx_runtime.jsx("span", { key: "name", style: META_STYLE, children: name }));
|
|
387
|
+
meta.push(react_jsx_runtime.jsx(StatusMark, { key: "mark", info }));
|
|
388
|
+
children.push(react_jsx_runtime.jsx("span", { style: { display: "inline-flex", alignItems: "center", gap: "6px", flex: "none", marginLeft: "auto", paddingLeft: "8px" }, children: meta }));
|
|
389
|
+
}
|
|
390
|
+
return react_jsx_runtime.jsx("span", { style: { display: "flex", alignItems: "center", minWidth: 0, width: "100%" }, children });
|
|
212
391
|
}
|
|
213
392
|
|
|
393
|
+
//#region composer chip (upstream additive surface: conversation.input.left)
|
|
214
394
|
/**
|
|
215
|
-
* Chip line
|
|
216
|
-
*
|
|
217
|
-
*
|
|
218
|
-
*
|
|
219
|
-
*
|
|
395
|
+
* Chip line in the input row: git state of the workspace the CURRENT
|
|
396
|
+
* conversation is attached to. It targets the session id and lets the node
|
|
397
|
+
* half resolve the workspace, so this surface needs no `workspaces`
|
|
398
|
+
* service, no cwd and no path plumbing at all. Works on unpatched installs
|
|
399
|
+
* (conversation.input.left is an upstream additive slot), and stays useful
|
|
400
|
+
* next to the sidebar rows because it is context-anchored ("where am I")
|
|
401
|
+
* rather than surveying.
|
|
220
402
|
*/
|
|
221
403
|
function ComposerGitChip({ sessionId }) {
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
404
|
+
// no detail=1: nothing renders lastCommits / stashCount yet, and asking
|
|
405
|
+
// for them would add a log -3 plus a stash list to every refresh
|
|
406
|
+
const info = useGitStatus(sessionId === void 0 ? void 0 : { kind: "session", id: sessionId });
|
|
407
|
+
if (info === void 0 || info.git !== true) return null;
|
|
408
|
+
// the mark is an element now rather than a leading glyph in the string, so
|
|
409
|
+
// the SAME StatusMark the sidebar row draws carries the status here too;
|
|
410
|
+
// the container's 4px gap supplies the space the emoji's own did
|
|
411
|
+
const text = info.branch + formatWorktreeToken(info) + formatOperationToken(info) + formatGitSuffix(info);
|
|
226
412
|
return react_jsx_runtime.jsx("span", {
|
|
227
413
|
style: {
|
|
228
414
|
display: "inline-flex",
|
|
@@ -235,28 +421,46 @@ window.__ModuleLoader__.load({
|
|
|
235
421
|
whiteSpace: "nowrap",
|
|
236
422
|
cursor: "default"
|
|
237
423
|
},
|
|
238
|
-
children:
|
|
424
|
+
children: [
|
|
425
|
+
react_jsx_runtime.jsx(StatusMark, { key: "mark", info }),
|
|
426
|
+
react_jsx_runtime.jsx("span", { key: "text", children: text })
|
|
427
|
+
]
|
|
239
428
|
});
|
|
240
429
|
}
|
|
241
430
|
//#endregion
|
|
242
431
|
|
|
243
|
-
|
|
432
|
+
// `workspaces` is no longer required: both surfaces target an id and the
|
|
433
|
+
// node half resolves the workspace, so the client never needs a service
|
|
434
|
+
// lookup. Fewer declared services also means fewer ways to fail to load.
|
|
435
|
+
const inject = ["slots"];
|
|
244
436
|
|
|
245
437
|
/**
|
|
246
438
|
* Register the badge into both seams. The seam owner hands each entry the
|
|
247
|
-
* row owner share as props; the badge destructures { workspaceId,
|
|
248
|
-
*
|
|
439
|
+
* row owner share as props; the badge destructures { workspaceId, label }
|
|
440
|
+
* and deliberately ignores the cwd it is also given.
|
|
249
441
|
*/
|
|
250
442
|
function apply(ctx) {
|
|
251
|
-
clientCtx = ctx;
|
|
252
443
|
// inject() re-evaluates when a seam's declaration appears, so boot
|
|
253
444
|
// order relative to the workspace browser does not matter. On an
|
|
254
|
-
// unpatched install the row
|
|
255
|
-
//
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
445
|
+
// unpatched install the row seam is never declared, so this callback
|
|
446
|
+
// never fires and the row badge simply never renders.
|
|
447
|
+
//
|
|
448
|
+
// The registration is reported from INSIDE the callback on purpose: a
|
|
449
|
+
// `spec()` check here at apply() time runs before the workspace browser
|
|
450
|
+
// declares the seam, so it reports the boot race rather than the
|
|
451
|
+
// outcome. That mistake made the old one-shot line claim "sidebar rows
|
|
452
|
+
// = off" while five row badges were rendering.
|
|
453
|
+
let rowsReported = false;
|
|
454
|
+
ctx.slots.inject("sidebar.workspaces.row", () => {
|
|
455
|
+
if (!rowsReported) {
|
|
456
|
+
rowsReported = true;
|
|
457
|
+
console.info("[dsh-git-badge] surfaces: sidebar rows = on (seam present).");
|
|
458
|
+
}
|
|
459
|
+
return ctx.slots.register({
|
|
460
|
+
name: "sidebar.workspaces.row",
|
|
461
|
+
id: "git-badge"
|
|
462
|
+
}, WorkspaceGitBadge);
|
|
463
|
+
});
|
|
260
464
|
// Input-row chip: upstream additive slot rendered in the input bar's
|
|
261
465
|
// leading cluster, right after the access picker — the git state sits
|
|
262
466
|
// with the controls that govern the conversation. Present on every
|
|
@@ -269,7 +473,7 @@ window.__ModuleLoader__.load({
|
|
|
269
473
|
inject: (sessionId) => ({ sessionId })
|
|
270
474
|
}, ComposerGitChip));
|
|
271
475
|
const seamDeclared = ctx.slots.spec("sidebar.workspaces.row") !== void 0;
|
|
272
|
-
console.info("[dsh-git-badge] surfaces: input chip = on; sidebar rows = " + (seamDeclared ? "on (seam present)" : "
|
|
476
|
+
console.info("[dsh-git-badge] surfaces: input chip = on; sidebar rows = " + (seamDeclared ? "on (seam present)." : "awaiting the sidebar.workspaces.row seam — the line above reports it if it appears."));
|
|
273
477
|
}
|
|
274
478
|
|
|
275
479
|
exports.apply = apply;
|