dsh-git-badge 0.6.0 → 0.8.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 +198 -0
- package/README.md +12 -4
- package/lib/client.js +569 -136
- package/lib/index.js +554 -90
- package/package.json +2 -1
package/lib/client.js
CHANGED
|
@@ -1,11 +1,24 @@
|
|
|
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): row badge —
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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 + the
|
|
14
|
+
* PR/CI token, with a HOVER CARD carrying the breakdown the chip has no room
|
|
15
|
+
* for (file detail, recent commits, stash, PR state).
|
|
16
|
+
*
|
|
17
|
+
* The hover card is the INPUT CHIP's, not the sidebar row's, and it needs
|
|
18
|
+
* nothing but the upstream slot: the Tooltip primitive is seeded by the shell
|
|
19
|
+
* itself, so the card works on a PRISTINE install with no seam patch. Its extra
|
|
20
|
+
* fields are fetched only once a pointer rests on the chip, so the everyday
|
|
21
|
+
* badge pays for none of them.
|
|
9
22
|
*/
|
|
10
23
|
window.__ModuleLoader__.load({
|
|
11
24
|
id: "dsh-git-badge",
|
|
@@ -16,6 +29,24 @@ window.__ModuleLoader__.load({
|
|
|
16
29
|
let react_jsx_runtime = require("react/jsx-runtime");
|
|
17
30
|
let react = require("react");
|
|
18
31
|
|
|
32
|
+
/**
|
|
33
|
+
* Shell-seeded UI primitives. `Tooltip` is handed to EVERY client bundle
|
|
34
|
+
* by the web shell itself — it needs no `dsh.client.inject` entry and no
|
|
35
|
+
* npm dependency (dsh-client-ui-open-in-app requires it the same way).
|
|
36
|
+
*
|
|
37
|
+
* Guarded, because the seed table belongs to the shell and not to this
|
|
38
|
+
* plugin: on a shell that does not provide it the chip must still render,
|
|
39
|
+
* just without the hover card. A missing card is a smaller failure than a
|
|
40
|
+
* missing badge.
|
|
41
|
+
*/
|
|
42
|
+
let primitives = null;
|
|
43
|
+
try {
|
|
44
|
+
primitives = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
45
|
+
} catch {
|
|
46
|
+
primitives = null;
|
|
47
|
+
}
|
|
48
|
+
const Tooltip = primitives === null ? void 0 : primitives.Tooltip;
|
|
49
|
+
|
|
19
50
|
/** Module-level git-status cache. Entries are invalidated by SSE events, never by age. */
|
|
20
51
|
const GIT_CACHE = new Map();
|
|
21
52
|
/** In-flight fetches per cacheKey: bursts collapse into one request. */
|
|
@@ -24,16 +55,26 @@ window.__ModuleLoader__.load({
|
|
|
24
55
|
const FALLBACK_POLL_MS = 60000;
|
|
25
56
|
|
|
26
57
|
/**
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
58
|
+
* A request target names WHO is asking, never a filesystem path — the node
|
|
59
|
+
* half resolves the directory itself, so a surface never needs to know
|
|
60
|
+
* one. The chip targets its conversation (`session`); a sidebar row
|
|
61
|
+
* targets the workspace id the seam hands it. The query string doubles as
|
|
62
|
+
* the cache key.
|
|
63
|
+
*
|
|
64
|
+
* The two optional additions are the INPUT CHIP's alone, and each is an
|
|
65
|
+
* explicit request rather than something every badge pays for:
|
|
66
|
+
* - `detail=1` — recent commits and stash for the hover card;
|
|
67
|
+
* - `pr=1` — the branch's GitHub PR/CI state.
|
|
68
|
+
* A sidebar row asks for neither, so surveying N workspaces never costs N
|
|
69
|
+
* forge calls (see the node half's PR region).
|
|
31
70
|
*/
|
|
32
|
-
function
|
|
33
|
-
if (
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
71
|
+
function targetQuery(target, options) {
|
|
72
|
+
if (target === void 0 || target.id === void 0) return void 0;
|
|
73
|
+
const param = target.kind === "workspace" ? "workspace" : "session";
|
|
74
|
+
let query = param + "=" + encodeURIComponent(target.id);
|
|
75
|
+
if (options?.pr === true) query += "&pr=1";
|
|
76
|
+
if (options?.detail === true) query += "&detail=1";
|
|
77
|
+
return query;
|
|
37
78
|
}
|
|
38
79
|
|
|
39
80
|
//#region SSE change feed (one EventSource per page, ref-counted)
|
|
@@ -49,19 +90,19 @@ window.__ModuleLoader__.load({
|
|
|
49
90
|
// NAMED frames: the node half writes `event: change`, and onmessage
|
|
50
91
|
// never fires for a named event
|
|
51
92
|
eventSource.addEventListener("change", (message) => {
|
|
52
|
-
let
|
|
93
|
+
let payload;
|
|
53
94
|
try {
|
|
54
|
-
|
|
95
|
+
payload = JSON.parse(message.data);
|
|
55
96
|
} catch {
|
|
56
97
|
return;
|
|
57
98
|
}
|
|
58
|
-
//
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
99
|
+
// The server names the workspace it watched, so a subscriber that
|
|
100
|
+
// asked by id matches without ever knowing a path. Each
|
|
101
|
+
// subscriber refetches itself, and the per-key in-flight map
|
|
102
|
+
// collapses the resulting burst into one request.
|
|
62
103
|
for (const fn of [...eventListeners]) {
|
|
63
104
|
try {
|
|
64
|
-
fn(
|
|
105
|
+
fn(payload);
|
|
65
106
|
} catch {
|
|
66
107
|
/* one bad subscriber must not starve the rest */
|
|
67
108
|
}
|
|
@@ -81,76 +122,130 @@ window.__ModuleLoader__.load({
|
|
|
81
122
|
|
|
82
123
|
/**
|
|
83
124
|
* Shared git-status hook. Fetches once on mount and then only when the
|
|
84
|
-
* node half's watcher reports a change for this
|
|
85
|
-
*
|
|
86
|
-
*
|
|
125
|
+
* node half's watcher reports a change for this target (SSE), plus a slow
|
|
126
|
+
* safety-net poll in case the stream dies silently. Returns undefined
|
|
127
|
+
* while loading and for non-git workspaces.
|
|
128
|
+
*
|
|
129
|
+
* `options.detail` / `options.pr` ask the node half for the chip's extras
|
|
130
|
+
* (see targetQuery). `options.enabled: false` holds the fetch back
|
|
131
|
+
* entirely — that is what makes the hover card's extra fields LAZY: the
|
|
132
|
+
* chip renders without them and only starts paying for `log -3` plus a
|
|
133
|
+
* stash list once a pointer actually rests on it. The hook is still called
|
|
134
|
+
* unconditionally (hooks may not be conditional); the GATE is inside.
|
|
87
135
|
*/
|
|
88
|
-
function useGitStatus(
|
|
89
|
-
const
|
|
136
|
+
function useGitStatus(target, options) {
|
|
137
|
+
const enabled = options?.enabled !== false;
|
|
138
|
+
const cacheKey = targetQuery(target, options);
|
|
90
139
|
const hit = cacheKey === void 0 ? void 0 : GIT_CACHE.get(cacheKey);
|
|
91
|
-
|
|
140
|
+
// state carries the key it belongs to: switching conversations changes
|
|
141
|
+
// the key, and showing the previous session's badge until the new fetch
|
|
142
|
+
// lands would be wrong
|
|
143
|
+
const [state, setState] = react.useState({ key: cacheKey, data: hit === void 0 ? void 0 : hit.data });
|
|
92
144
|
react.useEffect(() => {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
//
|
|
97
|
-
//
|
|
98
|
-
//
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
pending = fetch("/api/git-badge?path=" + encodeURIComponent(cwd))
|
|
108
|
-
.then((r) => r.json())
|
|
109
|
-
.finally(() => GIT_INFLIGHT.delete(cacheKey));
|
|
110
|
-
GIT_INFLIGHT.set(cacheKey, pending);
|
|
145
|
+
if (cacheKey === void 0 || !enabled) return;
|
|
146
|
+
let alive = true;
|
|
147
|
+
// The workspace this target resolved to, taken from the response (the
|
|
148
|
+
// server echoes it). THIS is what events are matched against: an event
|
|
149
|
+
// carries a workspace id, and a session-targeted chip has only a
|
|
150
|
+
// session id, so comparing the two directly never matches.
|
|
151
|
+
let resolvedWorkspace;
|
|
152
|
+
// Re-seed from cache when the key changes (switching conversations):
|
|
153
|
+
// render the last-known badge immediately instead of blanking until the
|
|
154
|
+
// fetch lands, and take the workspace id from it so events still match.
|
|
155
|
+
const cached = GIT_CACHE.get(cacheKey);
|
|
156
|
+
if (cached !== void 0) {
|
|
157
|
+
if (typeof cached.data?.workspace === "string") resolvedWorkspace = cached.data.workspace;
|
|
158
|
+
setState({ key: cacheKey, data: cached.data });
|
|
111
159
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
160
|
+
const apply = (data) => {
|
|
161
|
+
// A degraded response (git timeout/failure) must never clobber a
|
|
162
|
+
// good cached badge — keep the last-known state until a real
|
|
163
|
+
// event or the fallback poll succeeds.
|
|
164
|
+
if (data !== null && data.error !== void 0 && GIT_CACHE.has(cacheKey)) return;
|
|
165
|
+
if (data !== null && typeof data.workspace === "string") resolvedWorkspace = data.workspace;
|
|
166
|
+
GIT_CACHE.set(cacheKey, { at: Date.now(), data });
|
|
167
|
+
if (alive) setState({ key: cacheKey, data });
|
|
168
|
+
};
|
|
169
|
+
const load = () => {
|
|
170
|
+
// dedupe: an in-flight fetch for this key serves all callers
|
|
171
|
+
let pending = GIT_INFLIGHT.get(cacheKey);
|
|
172
|
+
if (pending === void 0) {
|
|
173
|
+
pending = fetch("/api/git-badge?" + cacheKey)
|
|
174
|
+
.then((r) => r.json())
|
|
175
|
+
.finally(() => GIT_INFLIGHT.delete(cacheKey));
|
|
176
|
+
GIT_INFLIGHT.set(cacheKey, pending);
|
|
177
|
+
}
|
|
178
|
+
pending.then(apply).catch(() => {});
|
|
179
|
+
};
|
|
180
|
+
load();
|
|
181
|
+
const unsubscribe = subscribeGitEvents((payload) => {
|
|
182
|
+
// An event with no workspace id (no watcher record for that path)
|
|
183
|
+
// cannot be attributed, and neither can one arriving before we have
|
|
184
|
+
// resolved our own target — refetch rather than guess, since a
|
|
185
|
+
// missed refresh is the bug this guards and an extra request is cheap
|
|
186
|
+
// (the in-flight map collapses bursts).
|
|
187
|
+
if (payload.workspace === void 0 || resolvedWorkspace === void 0 || payload.workspace === resolvedWorkspace) load();
|
|
188
|
+
});
|
|
189
|
+
const fallback = setInterval(load, FALLBACK_POLL_MS);
|
|
190
|
+
return () => {
|
|
191
|
+
alive = false;
|
|
192
|
+
unsubscribe();
|
|
193
|
+
clearInterval(fallback);
|
|
194
|
+
};
|
|
195
|
+
}, [cacheKey, enabled]);
|
|
196
|
+
return state.key === cacheKey ? state.data : void 0;
|
|
127
197
|
}
|
|
128
198
|
|
|
129
199
|
/**
|
|
130
|
-
* Three-state
|
|
131
|
-
*
|
|
132
|
-
* - RED: unmerged files (mid-conflict, git is blocked) — or a dirty
|
|
200
|
+
* Three-state status, checked top-down, first match wins:
|
|
201
|
+
* - "error": unmerged files (mid-conflict, git is blocked) — or a dirty
|
|
133
202
|
* tree that is ALSO behind upstream (unsaved edits on an outdated
|
|
134
203
|
* base: commit-then-pull friction ahead).
|
|
135
|
-
* -
|
|
204
|
+
* - "warn": dirty files, or any ahead/behind (routine work or sync
|
|
136
205
|
* pending — including the previously "quiet" green-but-↓1 case).
|
|
137
|
-
* -
|
|
206
|
+
* - "ok": clean and in sync.
|
|
138
207
|
* Red never means merely "behind": a clean tree one commit behind is
|
|
139
208
|
* normal between pulls, not an alarm.
|
|
209
|
+
*
|
|
210
|
+
* The single source of truth for that summary. BOTH surfaces render it the
|
|
211
|
+
* same way (see StatusMark): a circle, or a tree in a linked worktree,
|
|
212
|
+
* filled with the colour below.
|
|
140
213
|
*/
|
|
141
|
-
function
|
|
214
|
+
function badgeStatus(info) {
|
|
142
215
|
const dirty = info.dirty === true;
|
|
143
216
|
const behind = (info.behind || 0) > 0;
|
|
144
217
|
const ahead = (info.ahead || 0) > 0;
|
|
145
218
|
const conflict = (info.unmergedFiles || 0) > 0;
|
|
146
|
-
if (conflict || (dirty && behind)) return "
|
|
147
|
-
if (dirty || ahead || behind) return "
|
|
148
|
-
return "
|
|
219
|
+
if (conflict || (dirty && behind)) return "error";
|
|
220
|
+
if (dirty || ahead || behind) return "warn";
|
|
221
|
+
return "ok";
|
|
149
222
|
}
|
|
150
223
|
|
|
151
224
|
/**
|
|
152
|
-
*
|
|
153
|
-
*
|
|
225
|
+
* Mark fill per status, from the app's own semantic tokens so BOTH surfaces
|
|
226
|
+
* follow light/dark and custom themes — which a hardcoded emoji cannot do.
|
|
227
|
+
* The literals are only fallbacks for use outside DSH.
|
|
228
|
+
*/
|
|
229
|
+
const MARK_FILL = {
|
|
230
|
+
error: "var(--dsw-alias-state-error-primary, #e5484d)",
|
|
231
|
+
warn: "var(--dsw-alias-state-warn-primary, #d29922)",
|
|
232
|
+
ok: "var(--dsw-alias-state-success-primary, #30a46c)"
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Accessible name per status, WITHOUT the "git" prefix — the mark adds
|
|
237
|
+
* "git: " or "git worktree: ", so shape and colour are never the only
|
|
238
|
+
* channel for either fact.
|
|
239
|
+
*/
|
|
240
|
+
const STATUS_LABEL = {
|
|
241
|
+
error: "conflict, or uncommitted changes on an outdated base",
|
|
242
|
+
warn: "uncommitted changes, or out of sync with upstream",
|
|
243
|
+
ok: "clean and in sync"
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Sync/dirty suffix — INPUT CHIP ONLY. The sidebar row shows status alone;
|
|
248
|
+
* the chip is the surface that carries the numbers. Rule set:
|
|
154
249
|
* - dirty files render as ✎n (pencil = worktree files, distinct from
|
|
155
250
|
* the ↑/↓ commit-sync axis)
|
|
156
251
|
* - while dirty, BOTH sync counts render including zeros (↑0 ↓2 ✎3),
|
|
@@ -171,9 +266,60 @@ window.__ModuleLoader__.load({
|
|
|
171
266
|
}
|
|
172
267
|
|
|
173
268
|
/**
|
|
174
|
-
*
|
|
175
|
-
*
|
|
176
|
-
*
|
|
269
|
+
* PR / CI token — INPUT CHIP ONLY, and absent entirely when the node half
|
|
270
|
+
* has nothing to say (no `gh`, no GitHub remote, no PR for the branch), so
|
|
271
|
+
* a machine without `gh` sees precisely the chip it saw before.
|
|
272
|
+
*
|
|
273
|
+
* The state glyph is not the only channel: the token carries an accessible
|
|
274
|
+
* name spelling the state out, the same rule the status mark follows.
|
|
275
|
+
*/
|
|
276
|
+
const PR_STATE_GLYPH = { passing: "\u2713", pending: "\u2026", failing: "\u2717", none: "" };
|
|
277
|
+
const PR_STATE_LABEL = { passing: "checks passing", pending: "checks running", failing: "checks failing", none: "no checks" };
|
|
278
|
+
|
|
279
|
+
/** ` PR#142 ✗` (plus ` draft`), or "" when there is no PR to report. */
|
|
280
|
+
function formatPrToken(info) {
|
|
281
|
+
const pr = info.pr;
|
|
282
|
+
if (pr === void 0 || pr === null || pr.number === void 0) return "";
|
|
283
|
+
const state = PR_STATE_GLYPH[pr.state] === void 0 ? "none" : pr.state;
|
|
284
|
+
const glyph = PR_STATE_GLYPH[state];
|
|
285
|
+
return " PR#" + pr.number + (glyph === "" ? "" : " " + glyph) + (pr.draft === true ? " draft" : "");
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/** The token's accessible name: "pull request 142, checks failing, draft". */
|
|
289
|
+
function prTokenLabel(info) {
|
|
290
|
+
const pr = info.pr;
|
|
291
|
+
if (pr === void 0 || pr === null || pr.number === void 0) return void 0;
|
|
292
|
+
const state = PR_STATE_LABEL[pr.state] === void 0 ? PR_STATE_LABEL.none : PR_STATE_LABEL[pr.state];
|
|
293
|
+
return "pull request " + pr.number + ", " + state + (pr.draft === true ? ", draft" : "");
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* The PR's web URL, or undefined — the token is a LINK only when the node
|
|
298
|
+
* half vouched for a URL, and is plain text otherwise (no `gh`, no GitHub
|
|
299
|
+
* remote, an unauthenticated `gh`: the token still appears, it just does not
|
|
300
|
+
* pretend to be clickable, so there is no dead link to discover).
|
|
301
|
+
*
|
|
302
|
+
* The protocol is re-checked HERE even though the node half already dropped
|
|
303
|
+
* a non-http(s) value: an `href` is the one place a payload string becomes
|
|
304
|
+
* executable, so the element that creates it verifies its own input rather
|
|
305
|
+
* than trusting an upstream guard to stay in place. The shell's own markdown
|
|
306
|
+
* renderer guards its links the same way.
|
|
307
|
+
*/
|
|
308
|
+
function prLinkUrl(info) {
|
|
309
|
+
const pr = info.pr;
|
|
310
|
+
if (pr === void 0 || pr === null || typeof pr.url !== "string") return void 0;
|
|
311
|
+
try {
|
|
312
|
+
const { protocol } = new URL(pr.url);
|
|
313
|
+
return protocol === "http:" || protocol === "https:" ? pr.url : void 0;
|
|
314
|
+
} catch {
|
|
315
|
+
return void 0;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* In-progress operation token — INPUT CHIP ONLY (the row shows the status
|
|
321
|
+
* tree alone). A paused rebase or cherry-pick whose conflicts are all
|
|
322
|
+
* already staged has no unmerged files, so the three-state summary cannot
|
|
177
323
|
* distinguish it from ordinary dirty work; this says which
|
|
178
324
|
* history-rewriting operation git is waiting on.
|
|
179
325
|
*/
|
|
@@ -193,6 +339,45 @@ window.__ModuleLoader__.load({
|
|
|
193
339
|
return label === void 0 ? "" : " " + label;
|
|
194
340
|
}
|
|
195
341
|
|
|
342
|
+
/**
|
|
343
|
+
* Linked-worktree NAME token — INPUT CHIP ONLY, and only the name: worktree-
|
|
344
|
+
* ness itself is carried by the status mark's shape (a tree instead of a
|
|
345
|
+
* circle) on both surfaces. The chip has no other workspace identity, so
|
|
346
|
+
* several worktrees of one repository would otherwise render identical chips
|
|
347
|
+
* with no way to tell which checkout a conversation is in. The node half
|
|
348
|
+
* reports whether this is a linked worktree plus its directory NAME (never a
|
|
349
|
+
* path).
|
|
350
|
+
*
|
|
351
|
+
* The name is appended only when it says something the branch does not: the
|
|
352
|
+
* usual `repo-feat-x` directory sitting on branch `feat-x` would otherwise
|
|
353
|
+
* read as stutter. (The ROW needs no such rule — it shows the name instead
|
|
354
|
+
* of the branch, so there is nothing to stutter against.)
|
|
355
|
+
*/
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* True when the worktree name is already implied by the branch name, so
|
|
359
|
+
* showing both is pure repetition. Compared on a normalized form —
|
|
360
|
+
* case-folded, with `/` and `_` folded to `-` — because directory and
|
|
361
|
+
* branch conventions differ without changing the meaning (`feat/x` in a
|
|
362
|
+
* `repo-feat-x` directory).
|
|
363
|
+
*/
|
|
364
|
+
function worktreeNameIsRedundant(name, branch) {
|
|
365
|
+
if (typeof name !== "string" || name === "" || typeof branch !== "string") return true;
|
|
366
|
+
const normalize = (value) => value.toLowerCase().replace(/[/_]/g, "-");
|
|
367
|
+
const normalizedName = normalize(name);
|
|
368
|
+
const normalizedBranch = normalize(branch);
|
|
369
|
+
return normalizedName === normalizedBranch
|
|
370
|
+
|| normalizedName.endsWith("-" + normalizedBranch)
|
|
371
|
+
|| normalizedBranch.endsWith("-" + normalizedName);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/** ` name` for a worktree whose name the branch does not already say, else "". */
|
|
375
|
+
function formatWorktreeToken(info) {
|
|
376
|
+
if (info.isWorktree !== true) return "";
|
|
377
|
+
const name = typeof info.worktreeName === "string" ? info.worktreeName : "";
|
|
378
|
+
return worktreeNameIsRedundant(name, info.branch) ? "" : " " + name;
|
|
379
|
+
}
|
|
380
|
+
|
|
196
381
|
const META_STYLE = {
|
|
197
382
|
color: "var(--dsw-alias-label-tertiary, #9ea7ad)",
|
|
198
383
|
fontSize: "12px",
|
|
@@ -202,71 +387,234 @@ window.__ModuleLoader__.load({
|
|
|
202
387
|
};
|
|
203
388
|
|
|
204
389
|
/**
|
|
205
|
-
*
|
|
206
|
-
* the input chip
|
|
207
|
-
*
|
|
208
|
-
*
|
|
209
|
-
*
|
|
210
|
-
*
|
|
211
|
-
*
|
|
390
|
+
* Status mark, SHARED BY BOTH SURFACES: the same shape in the same fill
|
|
391
|
+
* wherever it appears, so the sidebar row and the input chip read
|
|
392
|
+
* identically. The FILL is the status colour; the SHAPE carries
|
|
393
|
+
* worktree-ness — a circle for a main checkout, a tree for a linked
|
|
394
|
+
* worktree — so the two facts never compete for the same channel.
|
|
395
|
+
*
|
|
396
|
+
* Drawn rather than typed because 🌳 is a COLOUR EMOJI: CSS cannot tint
|
|
397
|
+
* its leaves, and a mark that carries the status has to be paintable.
|
|
398
|
+
* Three overlapping crown lobes plus a trunk stay legible at this size.
|
|
399
|
+
*
|
|
400
|
+
* Neither colour nor shape is the only channel: the accessible name
|
|
401
|
+
* states both.
|
|
402
|
+
*/
|
|
403
|
+
function StatusMark({ info }) {
|
|
404
|
+
const status = badgeStatus(info);
|
|
405
|
+
const fill = MARK_FILL[status];
|
|
406
|
+
const isWorktree = info.isWorktree === true;
|
|
407
|
+
const svg = {
|
|
408
|
+
// 14px, NOT 12px: a colour emoji draws well above its nominal size — the
|
|
409
|
+
// 🔴/🟡/🟢 dot this replaces put down ~13-14px of ink at the surfaces'
|
|
410
|
+
// 12px font-size, so a 12px box read as a shrunken dot.
|
|
411
|
+
width: 14,
|
|
412
|
+
height: 14,
|
|
413
|
+
viewBox: "0 0 12 12",
|
|
414
|
+
role: "img",
|
|
415
|
+
"aria-label": (isWorktree ? "git worktree: " : "git: ") + STATUS_LABEL[status],
|
|
416
|
+
focusable: "false",
|
|
417
|
+
style: { flex: "none", display: "block", color: META_STYLE.color }
|
|
418
|
+
};
|
|
419
|
+
if (!isWorktree) {
|
|
420
|
+
// a plain filled disc: this is a main checkout. It fills its box so the
|
|
421
|
+
// ink lands at the size the emoji dot used to.
|
|
422
|
+
return react_jsx_runtime.jsx("svg", { ...svg, children: react_jsx_runtime.jsx("circle", { cx: 6, cy: 6, r: 5.5, fill }) });
|
|
423
|
+
}
|
|
424
|
+
return react_jsx_runtime.jsxs("svg", {
|
|
425
|
+
...svg,
|
|
426
|
+
children: [
|
|
427
|
+
// trunk first, so the crown's lobes overlap and join it. These are the
|
|
428
|
+
// 12px-era coordinates scaled 1.15 about the tree's centre, so its ink
|
|
429
|
+
// stays comparable to the disc's instead of reading small beside it.
|
|
430
|
+
react_jsx_runtime.jsx("rect", { key: "trunk", x: 5.25, y: 7.3, width: 1.5, height: 4.14, rx: 0.63, fill: "currentColor" }),
|
|
431
|
+
react_jsx_runtime.jsx("circle", { key: "crown-1", cx: 6, cy: 3.62, r: 3.57, fill }),
|
|
432
|
+
react_jsx_runtime.jsx("circle", { key: "crown-2", cx: 3.24, cy: 5.46, r: 2.53, fill }),
|
|
433
|
+
react_jsx_runtime.jsx("circle", { key: "crown-3", cx: 8.76, cy: 5.46, r: 2.53, fill })
|
|
434
|
+
]
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* A linked worktree's directory name, or null when there is nothing to
|
|
440
|
+
* show. A main checkout shows NO text: the row surveys status and identity,
|
|
441
|
+
* and the branch lives on the input chip — which is why the branch is never
|
|
442
|
+
* rendered here. The name is the one thing the row's own label (the
|
|
443
|
+
* worktree directory by default) cannot be trusted to say once a workspace
|
|
444
|
+
* has been renamed.
|
|
212
445
|
*/
|
|
213
|
-
function
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
446
|
+
function rowName(info) {
|
|
447
|
+
if (info.isWorktree !== true) return null;
|
|
448
|
+
const name = typeof info.worktreeName === "string" ? info.worktreeName : "";
|
|
449
|
+
return name === "" ? null : name;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Row badge — workspace name on the left, the status mark floated right:
|
|
454
|
+
* the_paragliding_app ●
|
|
455
|
+
* worktree_thing hotfix-tree 🌳
|
|
456
|
+
* Fill colour via badgeStatus(): green clean+synced, amber dirty or
|
|
457
|
+
* out-of-sync, red conflict or dirty-and-behind. Shape via the node half's
|
|
458
|
+
* isWorktree: circle = main checkout, tree = linked worktree.
|
|
459
|
+
* Always renders the workspace name (so the row keeps its identity) and
|
|
460
|
+
* appends the right-hand mark only for git workspaces. No branch, and no
|
|
461
|
+
* `|` separator.
|
|
462
|
+
*
|
|
463
|
+
* Targets the row's `workspaceId`. The seam also passes `cwd`, which is
|
|
464
|
+
* deliberately ignored: the node half resolves the directory itself, so
|
|
465
|
+
* the client never has to name one. A row with no workspaceId (the
|
|
466
|
+
* ungrouped bucket) has no workspace to report on, so it renders
|
|
467
|
+
* name-only.
|
|
468
|
+
*/
|
|
469
|
+
function WorkspaceGitBadge({ label, workspaceId }) {
|
|
470
|
+
// no detail=1: the row needs status and identity only, and the extra
|
|
471
|
+
// log / stash calls have no consumer yet
|
|
472
|
+
const info = useGitStatus(workspaceId === void 0 ? void 0 : { kind: "workspace", id: workspaceId });
|
|
217
473
|
const children = [react_jsx_runtime.jsx("span", { style: { minWidth: 0, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }, children: label })];
|
|
218
|
-
if (
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
);
|
|
474
|
+
if (workspaceId !== void 0 && info !== void 0 && info.git === true) {
|
|
475
|
+
// marginLeft:auto floats the mark to the right edge, so the status
|
|
476
|
+
// shape holds a fixed right-hand column and a worktree's name grows
|
|
477
|
+
// leftwards from it instead of shoving the shape around.
|
|
478
|
+
const meta = [];
|
|
479
|
+
const name = rowName(info);
|
|
480
|
+
if (name !== null) meta.push(react_jsx_runtime.jsx("span", { key: "name", style: META_STYLE, children: name }));
|
|
481
|
+
meta.push(react_jsx_runtime.jsx(StatusMark, { key: "mark", info }));
|
|
482
|
+
children.push(react_jsx_runtime.jsx("span", { style: { display: "inline-flex", alignItems: "center", gap: "6px", flex: "none", marginLeft: "auto", paddingLeft: "8px" }, children: meta }));
|
|
224
483
|
}
|
|
225
|
-
return react_jsx_runtime.jsx("span", { style: { display: "flex", alignItems: "center", minWidth: 0 }, children });
|
|
484
|
+
return react_jsx_runtime.jsx("span", { style: { display: "flex", alignItems: "center", minWidth: 0, width: "100%" }, children });
|
|
226
485
|
}
|
|
227
486
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
487
|
+
/**
|
|
488
|
+
* The chip's `✎n` is one number for four different things; the card splits
|
|
489
|
+
* it, so "ready to commit" is distinguishable from "not staged yet". The
|
|
490
|
+
* collapsed caveat is the node half's own honesty flag: when the untracked
|
|
491
|
+
* walk blew its budget the count is a DIRECTORY count, and saying so beats
|
|
492
|
+
* presenting an under-count as exact.
|
|
493
|
+
*/
|
|
494
|
+
function formatFileBreakdown(info) {
|
|
495
|
+
const parts = [];
|
|
496
|
+
if ((info.stagedFiles || 0) > 0) parts.push(info.stagedFiles + " staged");
|
|
497
|
+
if ((info.unstagedFiles || 0) > 0) parts.push(info.unstagedFiles + " unstaged");
|
|
498
|
+
if ((info.unmergedFiles || 0) > 0) parts.push(info.unmergedFiles + " unmerged");
|
|
499
|
+
const untracked = info.untrackedFiles || 0;
|
|
500
|
+
if (untracked > 0) parts.push(untracked + " untracked" + (info.untrackedMode === "collapsed" ? " (collapsed)" : ""));
|
|
501
|
+
return parts.length === 0 ? "clean" : parts.join(" \u00B7 ");
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/** The PR's full state as text, for the card: "#142 · checks failing · draft · review approved". */
|
|
505
|
+
function formatPrDetail(pr) {
|
|
506
|
+
if (pr === void 0 || pr === null || pr.number === void 0) return void 0;
|
|
507
|
+
const state = PR_STATE_LABEL[pr.state] === void 0 ? PR_STATE_LABEL.none : PR_STATE_LABEL[pr.state];
|
|
508
|
+
const bits = ["#" + pr.number, state];
|
|
509
|
+
if (pr.draft === true) bits.push("draft");
|
|
510
|
+
if (pr.review !== void 0) bits.push(String(pr.review).toLowerCase().replace(/_/g, " "));
|
|
511
|
+
if (pr.open === false) bits.push("not open");
|
|
512
|
+
return bits.join(" \u00B7 ");
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
const CARD_CONTAINER = {
|
|
516
|
+
display: "flex",
|
|
517
|
+
flexDirection: "column",
|
|
518
|
+
gap: "4px",
|
|
519
|
+
fontSize: "12px",
|
|
520
|
+
lineHeight: "18px",
|
|
521
|
+
maxWidth: "340px"
|
|
522
|
+
};
|
|
523
|
+
const CARD_ROW = { display: "flex", gap: "8px", alignItems: "baseline" };
|
|
524
|
+
const CARD_LABEL = { color: "var(--dsw-alias-label-tertiary, #9ea7ad)", flex: "none", minWidth: "62px" };
|
|
525
|
+
const CARD_VALUE = { minWidth: 0, overflowWrap: "anywhere" };
|
|
231
526
|
|
|
232
527
|
/**
|
|
233
|
-
*
|
|
234
|
-
*
|
|
235
|
-
*
|
|
528
|
+
* Hover card body for the input chip — INPUT CHIP ONLY. Pure presentation
|
|
529
|
+
* over fields the status response already carries, plus the `detail=1`
|
|
530
|
+
* additions (recent commits, stash) when they have arrived. Everything is
|
|
531
|
+
* spelled out in text, so no fact here depends on a colour or a glyph.
|
|
236
532
|
*/
|
|
237
|
-
function
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
533
|
+
function HoverCard({ info, detail }) {
|
|
534
|
+
// the detail payload is a superset of the plain one, so it wins when it
|
|
535
|
+
// has landed; until then the card still shows the base facts
|
|
536
|
+
const data = detail === void 0 ? info : detail;
|
|
537
|
+
const rows = [];
|
|
538
|
+
const add = (label, value) => {
|
|
539
|
+
if (value === void 0 || value === null || value === "") return;
|
|
540
|
+
rows.push(
|
|
541
|
+
react_jsx_runtime.jsxs("div", {
|
|
542
|
+
style: CARD_ROW,
|
|
543
|
+
children: [
|
|
544
|
+
react_jsx_runtime.jsx("span", { style: CARD_LABEL, children: label }),
|
|
545
|
+
react_jsx_runtime.jsx("span", { style: CARD_VALUE, children: value })
|
|
546
|
+
]
|
|
547
|
+
}, label)
|
|
548
|
+
);
|
|
243
549
|
};
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
550
|
+
add("branch", info.branch);
|
|
551
|
+
add("upstream", info.upstream === void 0 ? "none configured" : info.upstream);
|
|
552
|
+
if (info.ahead !== void 0 || info.behind !== void 0) {
|
|
553
|
+
add("sync", "\u2191" + (info.ahead || 0) + " \u2193" + (info.behind || 0));
|
|
554
|
+
}
|
|
555
|
+
add("files", formatFileBreakdown(info));
|
|
556
|
+
add("operation", info.operation === void 0 || info.operation === null ? void 0 : String(info.operation));
|
|
557
|
+
add("pull request", formatPrDetail(data.pr));
|
|
558
|
+
if (Array.isArray(data.lastCommits) && data.lastCommits.length > 0) {
|
|
559
|
+
rows.push(
|
|
560
|
+
react_jsx_runtime.jsxs("div", {
|
|
561
|
+
style: CARD_ROW,
|
|
562
|
+
children: [
|
|
563
|
+
react_jsx_runtime.jsx("span", { style: CARD_LABEL, children: "commits" }),
|
|
564
|
+
react_jsx_runtime.jsx("span", {
|
|
565
|
+
style: { ...CARD_VALUE, display: "flex", flexDirection: "column", gap: "2px" },
|
|
566
|
+
children: data.lastCommits.map((commit) =>
|
|
567
|
+
react_jsx_runtime.jsxs("span", {
|
|
568
|
+
children: [
|
|
569
|
+
react_jsx_runtime.jsx("span", { style: CARD_LABEL, children: commit.hash }),
|
|
570
|
+
" " + commit.subject + (commit.when === "" ? "" : " \u00B7 " + commit.when)
|
|
571
|
+
]
|
|
572
|
+
}, commit.hash)
|
|
573
|
+
)
|
|
574
|
+
})
|
|
575
|
+
]
|
|
576
|
+
}, "commits")
|
|
577
|
+
);
|
|
578
|
+
}
|
|
579
|
+
add("stash", data.stashCount === void 0 ? void 0 : data.stashCount + " stashed");
|
|
580
|
+
return react_jsx_runtime.jsx("div", { style: CARD_CONTAINER, children: rows });
|
|
253
581
|
}
|
|
254
582
|
|
|
583
|
+
//#region composer chip (upstream additive surface: conversation.input.left)
|
|
255
584
|
/**
|
|
256
|
-
* Chip line
|
|
257
|
-
*
|
|
258
|
-
*
|
|
259
|
-
*
|
|
260
|
-
*
|
|
585
|
+
* Chip line in the input row: git state of the workspace the CURRENT
|
|
586
|
+
* conversation is attached to. It targets the session id and lets the node
|
|
587
|
+
* half resolve the workspace, so this surface needs no `workspaces`
|
|
588
|
+
* service, no cwd and no path plumbing at all. Works on unpatched installs
|
|
589
|
+
* (conversation.input.left is an upstream additive slot), and stays useful
|
|
590
|
+
* next to the sidebar rows because it is context-anchored ("where am I")
|
|
591
|
+
* rather than surveying.
|
|
592
|
+
*
|
|
593
|
+
* This is the ONLY surface that asks the node half for the extras (`pr=1`
|
|
594
|
+
* always, `detail=1` on hover), because it is the only one that renders
|
|
595
|
+
* them — see targetQuery.
|
|
261
596
|
*/
|
|
262
597
|
function ComposerGitChip({ sessionId }) {
|
|
263
|
-
const
|
|
264
|
-
//
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
598
|
+
const target = sessionId === void 0 ? void 0 : { kind: "session", id: sessionId };
|
|
599
|
+
// the PR/CI token is always on the chip, so its fetch is not gated
|
|
600
|
+
const info = useGitStatus(target, { pr: true });
|
|
601
|
+
// The card's extras cost a `log -3` plus a stash list, so they are
|
|
602
|
+
// fetched only once a pointer actually RESTS on the chip, then kept
|
|
603
|
+
// fresh by the same SSE path. Eagerly asking would add both invocations
|
|
604
|
+
// to every refresh — and a refresh fires on every file edit.
|
|
605
|
+
const [hovered, setHovered] = react.useState(false);
|
|
606
|
+
// the PR token's own hover/focus state: it underlines on hover (and on
|
|
607
|
+
// focus) rather than at rest, and that underline is applied inline below
|
|
608
|
+
const [linkHover, setLinkHover] = react.useState(false);
|
|
609
|
+
const detail = useGitStatus(target, { pr: true, detail: true, enabled: hovered });
|
|
610
|
+
if (info === void 0 || info.git !== true) return null;
|
|
611
|
+
// the mark is an element now rather than a leading glyph in the string, so
|
|
612
|
+
// the SAME StatusMark the sidebar row draws carries the status here too;
|
|
613
|
+
// the container's 4px gap supplies the space the emoji's own did
|
|
614
|
+
const text = info.branch + formatWorktreeToken(info) + formatOperationToken(info) + formatGitSuffix(info);
|
|
615
|
+
const prToken = formatPrToken(info);
|
|
616
|
+
const prUrl = prLinkUrl(info);
|
|
617
|
+
const chip = react_jsx_runtime.jsxs("span", {
|
|
270
618
|
style: {
|
|
271
619
|
display: "inline-flex",
|
|
272
620
|
alignItems: "center",
|
|
@@ -278,28 +626,98 @@ window.__ModuleLoader__.load({
|
|
|
278
626
|
whiteSpace: "nowrap",
|
|
279
627
|
cursor: "default"
|
|
280
628
|
},
|
|
281
|
-
|
|
629
|
+
// the card is fetched for a pointer that RESTS here, not one that
|
|
630
|
+
// merely crosses the chip
|
|
631
|
+
onPointerEnter: () => setHovered(true),
|
|
632
|
+
children: [
|
|
633
|
+
react_jsx_runtime.jsx(StatusMark, { key: "mark", info }),
|
|
634
|
+
react_jsx_runtime.jsx("span", { key: "text", children: text }),
|
|
635
|
+
prToken === ""
|
|
636
|
+
? null
|
|
637
|
+
: prUrl === void 0
|
|
638
|
+
? react_jsx_runtime.jsx("span", {
|
|
639
|
+
key: "pr",
|
|
640
|
+
// the glyph is not the only channel: the token says what the
|
|
641
|
+
// CI state IS, for anyone who cannot see it
|
|
642
|
+
"aria-label": prTokenLabel(info),
|
|
643
|
+
children: prToken
|
|
644
|
+
})
|
|
645
|
+
: react_jsx_runtime.jsx("a", {
|
|
646
|
+
key: "pr",
|
|
647
|
+
href: prUrl,
|
|
648
|
+
// a new tab, because navigating THIS one away from the app
|
|
649
|
+
// would lose the conversation; noopener/noreferrer keep the
|
|
650
|
+
// opened tab from getting a handle on it
|
|
651
|
+
target: "_blank",
|
|
652
|
+
rel: "noopener noreferrer",
|
|
653
|
+
"aria-label": prTokenLabel(info) + ", opens on GitHub in a new tab",
|
|
654
|
+
// the chip's own colour rather than the browser's link blue /
|
|
655
|
+
// visited purple, so the token still reads as one row. The
|
|
656
|
+
// affordance is the pointer cursor plus an underline on hover
|
|
657
|
+
// or focus, set HERE rather than left to the host stylesheet:
|
|
658
|
+
// a shell that resets anchors would otherwise drop it silently.
|
|
659
|
+
style: {
|
|
660
|
+
color: "inherit",
|
|
661
|
+
textDecoration: linkHover ? "underline" : "none",
|
|
662
|
+
cursor: "pointer"
|
|
663
|
+
},
|
|
664
|
+
// focus joins hover — the token is keyboard-reachable now, and a
|
|
665
|
+
// keyboard user needs the same "this is a link" signal
|
|
666
|
+
onPointerEnter: () => setLinkHover(true),
|
|
667
|
+
onPointerLeave: () => setLinkHover(false),
|
|
668
|
+
onFocus: () => setLinkHover(true),
|
|
669
|
+
onBlur: () => setLinkHover(false),
|
|
670
|
+
children: prToken
|
|
671
|
+
})
|
|
672
|
+
]
|
|
673
|
+
});
|
|
674
|
+
// No Tooltip primitive (a shell that does not seed it): render the chip
|
|
675
|
+
// itself rather than losing the badge. The card is an enhancement, the
|
|
676
|
+
// badge is the feature.
|
|
677
|
+
if (Tooltip === void 0) return chip;
|
|
678
|
+
return react_jsx_runtime.jsx(Tooltip, {
|
|
679
|
+
side: "top",
|
|
680
|
+
maxWidth: 360,
|
|
681
|
+
// a function label keeps the card's element tree out of every render
|
|
682
|
+
// until the tooltip actually opens
|
|
683
|
+
label: () => react_jsx_runtime.jsx(HoverCard, { info, detail }),
|
|
684
|
+
children: chip
|
|
282
685
|
});
|
|
283
686
|
}
|
|
284
687
|
//#endregion
|
|
285
688
|
|
|
286
|
-
|
|
689
|
+
// `workspaces` is no longer required: both surfaces target an id and the
|
|
690
|
+
// node half resolves the workspace, so the client never needs a service
|
|
691
|
+
// lookup. Fewer declared services also means fewer ways to fail to load.
|
|
692
|
+
const inject = ["slots"];
|
|
287
693
|
|
|
288
694
|
/**
|
|
289
695
|
* Register the badge into both seams. The seam owner hands each entry the
|
|
290
|
-
* row owner share as props; the badge destructures { workspaceId,
|
|
291
|
-
*
|
|
696
|
+
* row owner share as props; the badge destructures { workspaceId, label }
|
|
697
|
+
* and deliberately ignores the cwd it is also given.
|
|
292
698
|
*/
|
|
293
699
|
function apply(ctx) {
|
|
294
|
-
clientCtx = ctx;
|
|
295
700
|
// inject() re-evaluates when a seam's declaration appears, so boot
|
|
296
701
|
// order relative to the workspace browser does not matter. On an
|
|
297
|
-
// unpatched install the row
|
|
298
|
-
//
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
702
|
+
// unpatched install the row seam is never declared, so this callback
|
|
703
|
+
// never fires and the row badge simply never renders.
|
|
704
|
+
//
|
|
705
|
+
// The registration is reported from INSIDE the callback on purpose: a
|
|
706
|
+
// `spec()` check here at apply() time runs before the workspace browser
|
|
707
|
+
// declares the seam, so it reports the boot race rather than the
|
|
708
|
+
// outcome. That mistake made the old one-shot line claim "sidebar rows
|
|
709
|
+
// = off" while five row badges were rendering.
|
|
710
|
+
let rowsReported = false;
|
|
711
|
+
ctx.slots.inject("sidebar.workspaces.row", () => {
|
|
712
|
+
if (!rowsReported) {
|
|
713
|
+
rowsReported = true;
|
|
714
|
+
console.info("[dsh-git-badge] surfaces: sidebar rows = on (seam present).");
|
|
715
|
+
}
|
|
716
|
+
return ctx.slots.register({
|
|
717
|
+
name: "sidebar.workspaces.row",
|
|
718
|
+
id: "git-badge"
|
|
719
|
+
}, WorkspaceGitBadge);
|
|
720
|
+
});
|
|
303
721
|
// Input-row chip: upstream additive slot rendered in the input bar's
|
|
304
722
|
// leading cluster, right after the access picker — the git state sits
|
|
305
723
|
// with the controls that govern the conversation. Present on every
|
|
@@ -312,11 +730,26 @@ window.__ModuleLoader__.load({
|
|
|
312
730
|
inject: (sessionId) => ({ sessionId })
|
|
313
731
|
}, ComposerGitChip));
|
|
314
732
|
const seamDeclared = ctx.slots.spec("sidebar.workspaces.row") !== void 0;
|
|
315
|
-
console.info("[dsh-git-badge] surfaces: input chip = on; sidebar rows = " + (seamDeclared ? "on (seam present)" : "
|
|
733
|
+
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."));
|
|
734
|
+
// The hover card depends on a primitive the SHELL seeds, not on anything
|
|
735
|
+
// this plugin declares. Report the outcome rather than letting a missing
|
|
736
|
+
// seed look like a missing feature: the try/catch above deliberately keeps
|
|
737
|
+
// the badge working, which would otherwise make this failure silent.
|
|
738
|
+
console.info(
|
|
739
|
+
"[dsh-git-badge] hover card = " +
|
|
740
|
+
(Tooltip === void 0
|
|
741
|
+
? "off (this shell seeds no Tooltip primitive) — the chip renders uncarded."
|
|
742
|
+
: "on (shell Tooltip primitive).")
|
|
743
|
+
);
|
|
316
744
|
}
|
|
317
745
|
|
|
318
746
|
exports.apply = apply;
|
|
319
747
|
exports.inject = inject;
|
|
748
|
+
// ---------- test-only exports ----------
|
|
749
|
+
// Additive; the host reads apply/inject and ignores the rest. The suite
|
|
750
|
+
// drives these to assert the REQUEST contract — which surface asks for the
|
|
751
|
+
// expensive extras — without a browser, a fetch or a network.
|
|
752
|
+
exports.__internals = { targetQuery, formatPrToken, formatFileBreakdown, formatPrDetail };
|
|
320
753
|
return module.exports;
|
|
321
754
|
}
|
|
322
755
|
});
|