dsh-git-badge 0.19.0 → 0.20.1
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 +149 -0
- package/README.md +15 -7
- package/lib/client.js +826 -722
- package/lib/index.js +272 -176
- package/package.json +5 -11
- package/seam/checkout.js +1 -1
- package/seam/cli.js +31 -0
- package/seam/skill.js +3 -3
- package/seam/store.js +15 -74
- package/skill/gh/SKILL.md +20 -7
- package/seam/anchors.js +0 -105
- package/seam/apply.js +0 -243
- package/seam/patch.js +0 -181
- package/seam/postinstall.js +0 -82
- package/seam/stub-index.js +0 -11
package/lib/client.js
CHANGED
|
@@ -1,28 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* dsh-git-badge — client half.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* worktree, filled with the status colour — so status and worktree-ness read
|
|
6
|
-
* identically wherever they appear.
|
|
4
|
+
* Every surface rides an UPSTREAM seat. No patch, no manual step.
|
|
7
5
|
*
|
|
8
6
|
* Surfaces:
|
|
9
|
-
* - sidebar.
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* names that checkout, rendered inside the card upstream ALREADY shows for a
|
|
7
|
+
* - sidebar.session.row.leading: the session row's token as a single glyph in
|
|
8
|
+
* the row's 16px leading cell, from the node half's `rowToken`. Upstream
|
|
9
|
+
* renders this seat only while the session is IDLE.
|
|
10
|
+
* - sidebar.session.row.hover: the hover card that explains the token and
|
|
11
|
+
* names the checkout, rendered inside the card upstream ALREADY shows for a
|
|
15
12
|
* session row rather than in a tooltip nested inside it.
|
|
16
|
-
* - conversation.input.left
|
|
13
|
+
* - conversation.input.left: chip — status mark + branch + a
|
|
17
14
|
* worktree's name + in-progress operation token + sync/dirty suffix + the
|
|
18
15
|
* PR/CI token, with a HOVER CARD carrying the breakdown the chip has no room
|
|
19
16
|
* for (file detail, recent commits, stash, PR state, and the conversation's own
|
|
20
17
|
* checkout when the badge followed a worktree).
|
|
21
18
|
*
|
|
22
19
|
* The chip's hover card needs nothing but the upstream slot: the Tooltip
|
|
23
|
-
* primitive is seeded by the shell itself
|
|
24
|
-
*
|
|
25
|
-
* rests on the chip, so the everyday badge pays for none of them.
|
|
20
|
+
* primitive is seeded by the shell itself. Its extra fields are fetched only once
|
|
21
|
+
* a pointer rests on the chip, so the everyday badge pays for none of them.
|
|
26
22
|
*/
|
|
27
23
|
window.__ModuleLoader__.load({
|
|
28
24
|
id: "dsh-git-badge",
|
|
@@ -50,11 +46,6 @@ window.__ModuleLoader__.load({
|
|
|
50
46
|
primitives = null;
|
|
51
47
|
}
|
|
52
48
|
const Tooltip = primitives === null ? void 0 : primitives.Tooltip;
|
|
53
|
-
// The shell's pull-down primitive (what the input-area selectors use) and
|
|
54
|
-
// its candidate ranker. Same guarded require as Tooltip: a shell without
|
|
55
|
-
// them loses the pull-down affordances, never the badges.
|
|
56
|
-
const Menu = primitives === null ? void 0 : primitives.Menu;
|
|
57
|
-
const rankByName = primitives === null ? void 0 : primitives.rankByName;
|
|
58
49
|
|
|
59
50
|
/**
|
|
60
51
|
* Module-level git-status cache. Entries are invalidated by SSE events,
|
|
@@ -75,16 +66,14 @@ window.__ModuleLoader__.load({
|
|
|
75
66
|
/**
|
|
76
67
|
* A request target names WHO is asking, never a filesystem path — the node
|
|
77
68
|
* half resolves the directory itself, so a surface never needs to know
|
|
78
|
-
* one.
|
|
79
|
-
*
|
|
80
|
-
* the cache key.
|
|
69
|
+
* one. Every surface here targets its conversation (`session`). The query
|
|
70
|
+
* string doubles as the cache key.
|
|
81
71
|
*
|
|
82
|
-
* The two optional additions
|
|
83
|
-
*
|
|
84
|
-
* - `
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
* forge calls (see the node half's PR region).
|
|
72
|
+
* The two optional additions, each an explicit request:
|
|
73
|
+
* - `detail=1` — recent commits and stash for the chip's hover card;
|
|
74
|
+
* - `pr=1` — the branch's GitHub PR/CI state, which both the chip and
|
|
75
|
+
* the row token need (the leading glyph and the hover card share this
|
|
76
|
+
* exact key, so they cost one request between them).
|
|
88
77
|
*/
|
|
89
78
|
function targetQuery(target, options) {
|
|
90
79
|
if (target === void 0 || target.id === void 0) return void 0;
|
|
@@ -98,11 +87,9 @@ window.__ModuleLoader__.load({
|
|
|
98
87
|
//#region SSE change feed (one EventSource per page, ref-counted)
|
|
99
88
|
const eventListeners = new Set();
|
|
100
89
|
let eventSource = null;
|
|
101
|
-
let eventRefs = 0;
|
|
102
90
|
|
|
103
91
|
function subscribeGitEvents(onChange) {
|
|
104
92
|
eventListeners.add(onChange);
|
|
105
|
-
eventRefs += 1;
|
|
106
93
|
if (eventSource === null) {
|
|
107
94
|
eventSource = new EventSource("/api/git-badge/events");
|
|
108
95
|
// NAMED frames: the node half writes `event: change`, and onmessage
|
|
@@ -129,8 +116,7 @@ window.__ModuleLoader__.load({
|
|
|
129
116
|
}
|
|
130
117
|
return () => {
|
|
131
118
|
eventListeners.delete(onChange);
|
|
132
|
-
|
|
133
|
-
if (eventRefs === 0 && eventSource !== null) {
|
|
119
|
+
if (eventListeners.size === 0 && eventSource !== null) {
|
|
134
120
|
eventSource.close();
|
|
135
121
|
eventSource = null;
|
|
136
122
|
}
|
|
@@ -181,7 +167,7 @@ window.__ModuleLoader__.load({
|
|
|
181
167
|
// event or the fallback poll succeeds.
|
|
182
168
|
if (data !== null && data.error !== void 0 && GIT_CACHE.has(cacheKey)) return;
|
|
183
169
|
if (data !== null && typeof data.workspace === "string") resolvedWorkspace = data.workspace;
|
|
184
|
-
GIT_CACHE.set(cacheKey, {
|
|
170
|
+
GIT_CACHE.set(cacheKey, { data });
|
|
185
171
|
if (GIT_CACHE.size > GIT_CACHE_LIMIT) {
|
|
186
172
|
GIT_CACHE.delete(GIT_CACHE.keys().next().value);
|
|
187
173
|
}
|
|
@@ -217,32 +203,6 @@ window.__ModuleLoader__.load({
|
|
|
217
203
|
return state.key === cacheKey ? state.data : void 0;
|
|
218
204
|
}
|
|
219
205
|
|
|
220
|
-
/**
|
|
221
|
-
* Three-state status, checked top-down, first match wins:
|
|
222
|
-
* - "error": unmerged files (mid-conflict, git is blocked) — or a dirty
|
|
223
|
-
* tree that is ALSO behind upstream (unsaved edits on an outdated
|
|
224
|
-
* base: commit-then-pull friction ahead).
|
|
225
|
-
* - "warn": dirty files, or any ahead/behind (routine work or sync
|
|
226
|
-
* pending — including the previously "quiet" green-but-↓1 case).
|
|
227
|
-
* - "ok": clean and in sync.
|
|
228
|
-
* Red never means merely "behind": a clean tree one commit behind is
|
|
229
|
-
* normal between pulls, not an alarm.
|
|
230
|
-
*
|
|
231
|
-
* The single source of truth for that summary. BOTH surfaces render it the
|
|
232
|
-
* same way (see StatusMark): a circle, or a tree in a linked worktree,
|
|
233
|
-
* filled with the colour below.
|
|
234
|
-
*/
|
|
235
|
-
function badgeStatus(info) {
|
|
236
|
-
const dirty = info.dirty === true;
|
|
237
|
-
const behind = (info.behind || 0) > 0;
|
|
238
|
-
const ahead = (info.ahead || 0) > 0;
|
|
239
|
-
const conflict = (info.unmergedFiles || 0) > 0;
|
|
240
|
-
if (conflict || (dirty && behind)) return "error";
|
|
241
|
-
if (dirty || ahead || behind) return "warn";
|
|
242
|
-
return "ok";
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
|
|
246
206
|
/**
|
|
247
207
|
* The actions BOTH surfaces render — the server's list, verbatim.
|
|
248
208
|
*
|
|
@@ -278,49 +238,32 @@ window.__ModuleLoader__.load({
|
|
|
278
238
|
}
|
|
279
239
|
|
|
280
240
|
/**
|
|
281
|
-
*
|
|
282
|
-
* follow light/dark and custom themes — which a hardcoded emoji cannot do.
|
|
283
|
-
* The literals are only fallbacks for use outside DSH.
|
|
284
|
-
*/
|
|
285
|
-
const MARK_FILL = {
|
|
286
|
-
error: "var(--dsw-alias-state-error-primary, #e5484d)",
|
|
287
|
-
warn: "var(--dsw-alias-state-warn-primary, #d29922)",
|
|
288
|
-
ok: "var(--dsw-alias-state-success-primary, #30a46c)"
|
|
289
|
-
};
|
|
290
|
-
|
|
291
|
-
/**
|
|
292
|
-
* Accessible name per status, WITHOUT the "git" prefix — the mark adds
|
|
293
|
-
* "git: " or "git worktree: ", so shape and colour are never the only
|
|
294
|
-
* channel for either fact.
|
|
295
|
-
*/
|
|
296
|
-
const STATUS_LABEL = {
|
|
297
|
-
error: "conflict, or uncommitted changes on an outdated base",
|
|
298
|
-
warn: "uncommitted changes, or out of sync with upstream",
|
|
299
|
-
ok: "clean and in sync"
|
|
300
|
-
};
|
|
301
|
-
|
|
302
|
-
/**
|
|
303
|
-
* Sync/dirty suffix — INPUT CHIP ONLY. The sidebar row shows status alone;
|
|
241
|
+
* Base/dirty suffix — INPUT CHIP ONLY. The sidebar row shows status alone;
|
|
304
242
|
* the chip is the surface that carries the numbers. Rule set:
|
|
305
243
|
* - dirty files render as ✎n (pencil = worktree files, distinct from
|
|
306
|
-
* the ↑/↓ commit
|
|
307
|
-
* - while dirty, BOTH
|
|
244
|
+
* the ↑/↓ commit axis)
|
|
245
|
+
* - while dirty, BOTH counts render including zeros (↑0 ↓2 ✎3),
|
|
308
246
|
* so every number is positionally attributable
|
|
309
247
|
* - clean workspaces stay quiet: arrows only when nonzero, no ✎ at 0
|
|
310
|
-
* - no
|
|
248
|
+
* - no base known: arrows omitted entirely
|
|
311
249
|
*/
|
|
250
|
+
/** The base branch's name, for the arrows and the card's base row. */
|
|
251
|
+
function baseNameFor(info) {
|
|
252
|
+
return typeof info?.mainBranch === "string" && info.mainBranch !== "" ? info.mainBranch : "main";
|
|
253
|
+
}
|
|
254
|
+
|
|
312
255
|
/**
|
|
313
|
-
* The
|
|
314
|
-
*
|
|
315
|
-
*
|
|
316
|
-
* branch
|
|
317
|
-
*
|
|
256
|
+
* The BASE ahead/behind arrows and the base's name (" ↑4 ↓0 main"), the
|
|
257
|
+
* branch's standing against the branch it will merge into. Naming the base
|
|
258
|
+
* stops the arrows from reading as the git-native upstream count. The
|
|
259
|
+
* branch's own remote sync is not a number here: the action glyph (`push`,
|
|
260
|
+
* `pull`) carries the actionable part.
|
|
318
261
|
*/
|
|
319
|
-
function
|
|
320
|
-
const
|
|
262
|
+
function formatBaseSync(info) {
|
|
263
|
+
const hasBase = info.mainAhead !== void 0 || info.mainBehind !== void 0;
|
|
321
264
|
const files = (info.changedFiles || 0) + (info.untrackedFiles || 0);
|
|
322
|
-
if (!
|
|
323
|
-
return " \u2191" + (info.
|
|
265
|
+
if (!hasBase || (files === 0 && !(info.mainAhead > 0) && !(info.mainBehind > 0))) return "";
|
|
266
|
+
return " \u2191" + (info.mainAhead || 0) + " \u2193" + (info.mainBehind || 0) + " " + baseNameFor(info);
|
|
324
267
|
}
|
|
325
268
|
|
|
326
269
|
/** The ✎n file count (" ✎3"), or "". */
|
|
@@ -329,11 +272,6 @@ window.__ModuleLoader__.load({
|
|
|
329
272
|
return files > 0 ? " \u270E" + files : "";
|
|
330
273
|
}
|
|
331
274
|
|
|
332
|
-
/** Both halves, in chip order — kept for callers that want the whole suffix. */
|
|
333
|
-
function formatGitSuffix(info) {
|
|
334
|
-
return formatSync(info) + formatFileCount(info);
|
|
335
|
-
}
|
|
336
|
-
|
|
337
275
|
/**
|
|
338
276
|
* PR / CI token — INPUT CHIP ONLY, and absent entirely when the node half
|
|
339
277
|
* has nothing to say (no `gh`, no GitHub remote, no PR for the branch), so
|
|
@@ -344,21 +282,41 @@ window.__ModuleLoader__.load({
|
|
|
344
282
|
*/
|
|
345
283
|
const PR_STATE_GLYPH = { passing: "\u2713", pending: "\u2026", failing: "\u2717", none: "" };
|
|
346
284
|
const PR_STATE_LABEL = { passing: "checks passing", pending: "checks running", failing: "checks failing", none: "no checks" };
|
|
285
|
+
/** The PR's state label, defaulting to "no checks". */
|
|
286
|
+
function prStateLabel(pr) {
|
|
287
|
+
return PR_STATE_LABEL[pr.state] === void 0 ? PR_STATE_LABEL.none : PR_STATE_LABEL[pr.state];
|
|
288
|
+
}
|
|
347
289
|
|
|
348
|
-
/**
|
|
349
|
-
|
|
290
|
+
/**
|
|
291
|
+
* The CI state -> colour, for the token's STATE GLYPH only. The number and
|
|
292
|
+
* the link keep the chip's own muted colour, so the token still reads as one
|
|
293
|
+
* row and only the state shouts. These are the same app state tokens the
|
|
294
|
+
* action glyph uses.
|
|
295
|
+
*/
|
|
296
|
+
const PR_STATE_COLOUR = {
|
|
297
|
+
passing: "var(--dsw-alias-state-success-primary, #30a46c)",
|
|
298
|
+
pending: "var(--dsw-alias-state-warn-primary, #d29922)",
|
|
299
|
+
failing: "var(--dsw-alias-state-error-primary, #e5484d)"
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
/** The PR token's parts, so its plain text and its coloured form cannot drift. */
|
|
303
|
+
function prTokenParts(info) {
|
|
350
304
|
const pr = info.pr;
|
|
351
|
-
if (pr === void 0 || pr === null || pr.number === void 0) return
|
|
305
|
+
if (pr === void 0 || pr === null || pr.number === void 0) return null;
|
|
352
306
|
const state = PR_STATE_GLYPH[pr.state] === void 0 ? "none" : pr.state;
|
|
353
|
-
|
|
354
|
-
|
|
307
|
+
return {
|
|
308
|
+
number: " PR#" + pr.number,
|
|
309
|
+
glyph: PR_STATE_GLYPH[state],
|
|
310
|
+
colour: PR_STATE_COLOUR[state] ?? META_STYLE.color,
|
|
311
|
+
draft: pr.draft === true ? " draft" : ""
|
|
312
|
+
};
|
|
355
313
|
}
|
|
356
314
|
|
|
357
315
|
/** The token's accessible name: "pull request 142, checks failing, draft". */
|
|
358
316
|
function prTokenLabel(info) {
|
|
359
317
|
const pr = info.pr;
|
|
360
318
|
if (pr === void 0 || pr === null || pr.number === void 0) return void 0;
|
|
361
|
-
const state =
|
|
319
|
+
const state = prStateLabel(pr);
|
|
362
320
|
return "pull request " + pr.number + ", " + state + (pr.draft === true ? ", draft" : "");
|
|
363
321
|
}
|
|
364
322
|
|
|
@@ -424,90 +382,6 @@ window.__ModuleLoader__.load({
|
|
|
424
382
|
whiteSpace: "nowrap"
|
|
425
383
|
};
|
|
426
384
|
|
|
427
|
-
/**
|
|
428
|
-
* Status mark, SHARED BY BOTH SURFACES: the same shape in the same fill
|
|
429
|
-
* wherever it appears, so the sidebar row and the input chip read
|
|
430
|
-
* identically. The FILL is the status colour; the SHAPE carries
|
|
431
|
-
* worktree-ness — a circle for a main checkout, a tree for a linked
|
|
432
|
-
* worktree — so the two facts never compete for the same channel.
|
|
433
|
-
*
|
|
434
|
-
* Drawn rather than typed because 🌳 is a COLOUR EMOJI: CSS cannot tint
|
|
435
|
-
* its leaves, and a mark that carries the status has to be paintable.
|
|
436
|
-
* Three overlapping crown lobes plus a trunk stay legible at this size.
|
|
437
|
-
*
|
|
438
|
-
* Neither colour nor shape is the only channel: the accessible name
|
|
439
|
-
* states both.
|
|
440
|
-
*/
|
|
441
|
-
function StatusMark({ info }) {
|
|
442
|
-
const status = badgeStatus(info);
|
|
443
|
-
const fill = MARK_FILL[status];
|
|
444
|
-
const isWorktree = info.isWorktree === true;
|
|
445
|
-
const svg = {
|
|
446
|
-
// 14px, NOT 12px: a colour emoji draws well above its nominal size — the
|
|
447
|
-
// 🔴/🟡/🟢 dot this replaces put down ~13-14px of ink at the surfaces'
|
|
448
|
-
// 12px font-size, so a 12px box read as a shrunken dot.
|
|
449
|
-
width: 14,
|
|
450
|
-
height: 14,
|
|
451
|
-
viewBox: "0 0 12 12",
|
|
452
|
-
role: "img",
|
|
453
|
-
"aria-label":
|
|
454
|
-
(isWorktree ? "git worktree: " : "git: ") +
|
|
455
|
-
STATUS_LABEL[status] +
|
|
456
|
-
// a followed checkout is not where the conversation lives, and the
|
|
457
|
-
// accessible name says so — the session registered it explicitly
|
|
458
|
-
(info.worktreeFollowed === true ? " (registered for this session)" : ""),
|
|
459
|
-
focusable: "false",
|
|
460
|
-
style: { flex: "none", display: "block", color: META_STYLE.color }
|
|
461
|
-
};
|
|
462
|
-
if (!isWorktree) {
|
|
463
|
-
// a plain filled disc: this is a main checkout. It fills its box so the
|
|
464
|
-
// ink lands at the size the emoji dot used to.
|
|
465
|
-
return react_jsx_runtime.jsx("svg", { ...svg, children: react_jsx_runtime.jsx("circle", { cx: 6, cy: 6, r: 5.5, fill }) });
|
|
466
|
-
}
|
|
467
|
-
return react_jsx_runtime.jsxs("svg", {
|
|
468
|
-
...svg,
|
|
469
|
-
children: [
|
|
470
|
-
// trunk first, so the crown's lobes overlap and join it. These are the
|
|
471
|
-
// 12px-era coordinates scaled 1.15 about the tree's centre, so its ink
|
|
472
|
-
// stays comparable to the disc's instead of reading small beside it.
|
|
473
|
-
react_jsx_runtime.jsx("rect", { key: "trunk", x: 5.25, y: 7.3, width: 1.5, height: 4.14, rx: 0.63, fill: "currentColor" }),
|
|
474
|
-
react_jsx_runtime.jsx("circle", { key: "crown-1", cx: 6, cy: 3.62, r: 3.57, fill }),
|
|
475
|
-
react_jsx_runtime.jsx("circle", { key: "crown-2", cx: 3.24, cy: 5.46, r: 2.53, fill }),
|
|
476
|
-
react_jsx_runtime.jsx("circle", { key: "crown-3", cx: 8.76, cy: 5.46, r: 2.53, fill })
|
|
477
|
-
]
|
|
478
|
-
});
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
/**
|
|
482
|
-
* The ONE thing this conversation needs from you, or "" when it needs
|
|
483
|
-
* nothing.
|
|
484
|
-
*
|
|
485
|
-
* A session row's job is TRIAGE — "does this need me?" — so this is not the
|
|
486
|
-
* chip's status restated: it is a short imperative, present only when an
|
|
487
|
-
* action is genuinely warranted, first match wins. Silence is the common and
|
|
488
|
-
* correct answer: uncommitted work is a state rather than a chore, and DRAFT,
|
|
489
|
-
* BLOCKED, BEHIND, UNSTABLE and UNKNOWN are things to wait for, not to do.
|
|
490
|
-
*
|
|
491
|
-
* `merge` keys on GitHub's OWN verdict (`mergeStateStatus === "CLEAN"`) rather
|
|
492
|
-
* than a checks-plus-reviews judgement assembled here: mergeability depends on
|
|
493
|
-
* branch protection and required reviews, which is the forge's business. A PR
|
|
494
|
-
* read that could not answer has no `mergeState`, so it stays silent.
|
|
495
|
-
*/
|
|
496
|
-
function actionToken(info) {
|
|
497
|
-
if (info === void 0 || info === null || info.git !== true) return "";
|
|
498
|
-
if ((info.unmergedFiles || 0) > 0) return "resolve";
|
|
499
|
-
const pr = info.pr === void 0 || info.pr === null ? void 0 : info.pr;
|
|
500
|
-
if (pr !== void 0) {
|
|
501
|
-
if (pr.state === "failing") return "fix CI";
|
|
502
|
-
if (pr.review === "CHANGES_REQUESTED") return "review";
|
|
503
|
-
if (pr.mergeState === "CLEAN") return "merge";
|
|
504
|
-
}
|
|
505
|
-
// diverged counts as behind: reconciling is the action either way
|
|
506
|
-
if ((info.behind || 0) > 0) return "pull";
|
|
507
|
-
if ((info.ahead || 0) > 0) return "push";
|
|
508
|
-
return "";
|
|
509
|
-
}
|
|
510
|
-
|
|
511
385
|
/**
|
|
512
386
|
* Session-row badge — the ACTION, and nothing else:
|
|
513
387
|
* Session title… merge
|
|
@@ -534,70 +408,132 @@ window.__ModuleLoader__.load({
|
|
|
534
408
|
* below reuses THIS request rather than issuing its own.
|
|
535
409
|
*/
|
|
536
410
|
/**
|
|
537
|
-
*
|
|
538
|
-
*
|
|
539
|
-
*
|
|
540
|
-
*
|
|
541
|
-
*
|
|
542
|
-
*
|
|
543
|
-
*
|
|
544
|
-
*
|
|
411
|
+
* Severity -> colour. The NODE half decides the severity
|
|
412
|
+
* (`error` | `warn` | `success` | `business`) and every surface paints from
|
|
413
|
+
* that one field, so the chip and the row cannot disagree about how loud a
|
|
414
|
+
* state is. Colour here means SEVERITY — how much this needs you — rather
|
|
415
|
+
* than identity: a broken build and a conflict are the loud ones, a requested
|
|
416
|
+
* review is a nudge, `merge` is the all-clear, and routine work is the quiet
|
|
417
|
+
* blue end of the same ramp rather than outside it. The WORD remains the
|
|
418
|
+
* channel (colour only reinforces it), and the tokens are the app's own, so
|
|
419
|
+
* both themes work.
|
|
545
420
|
*/
|
|
546
|
-
const
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
pull: "var(--dsw-alias-state-business-primary, #3b82f6)",
|
|
552
|
-
push: "var(--dsw-alias-state-business-primary, #3b82f6)"
|
|
421
|
+
const SEVERITY_FILL = {
|
|
422
|
+
error: "var(--dsw-alias-state-error-primary, #e5484d)",
|
|
423
|
+
warn: "var(--dsw-alias-state-warn-primary, #d29922)",
|
|
424
|
+
success: "var(--dsw-alias-state-success-primary, #30a46c)",
|
|
425
|
+
business: "var(--dsw-alias-state-business-primary, #3b82f6)"
|
|
553
426
|
};
|
|
554
427
|
|
|
555
428
|
/**
|
|
556
|
-
* Action -> glyph. The SHAPE channel
|
|
557
|
-
*
|
|
558
|
-
*
|
|
559
|
-
*
|
|
429
|
+
* Action -> TEXT glyph. The SHAPE channel for the five tokens whose icon is a
|
|
430
|
+
* character. The other six are DRAWN (see DRAWN_ICONS) and deliberately have
|
|
431
|
+
* NO entry here: a leftover text fallback would be a second, unreachable
|
|
432
|
+
* rendering of the same icon — and for `merge` it was exactly the check mark
|
|
433
|
+
* the drawn Octicon replaced.
|
|
434
|
+
*
|
|
435
|
+
* A missing entry degrades to the bare word.
|
|
560
436
|
*/
|
|
561
437
|
const ACTION_GLYPH = {
|
|
438
|
+
resume: "\u23F5",
|
|
562
439
|
resolve: "!",
|
|
563
440
|
"fix CI": "\u2717",
|
|
564
441
|
review: "\u25C6",
|
|
565
|
-
|
|
566
|
-
pull: "\u2193",
|
|
567
|
-
push: "\u2191"
|
|
442
|
+
commit: "\u271A"
|
|
568
443
|
};
|
|
569
444
|
|
|
570
445
|
/**
|
|
571
|
-
* The
|
|
572
|
-
* - `marginLeft: auto` floats it right, so a column of actions lines up down
|
|
573
|
-
* the sidebar — that alignment is what makes the list scannable;
|
|
574
|
-
* - `marginRight` keeps it clear of the relative time beside it (the first cut
|
|
575
|
-
* sat flush and read `merge11m`);
|
|
576
|
-
* - weight 600 plus a little padding make it a PILL, so the token reads as a
|
|
577
|
-
* tag rather than as loose text the eye can skip;
|
|
578
|
-
* - the radius is the app's own small one, so the pill belongs to the shell.
|
|
446
|
+
* The icons DRAWN rather than typed, each with its own grid and render size.
|
|
579
447
|
*
|
|
580
|
-
*
|
|
581
|
-
*
|
|
448
|
+
* `↑ ↓ ⇄ ↻` are hairline strokes in the shell's font, and no weight or size
|
|
449
|
+
* makes them as solid as the filled glyphs; these four are paths on a 12x12
|
|
450
|
+
* grid, painted with `currentColor`, so each arrow matches the text's weight
|
|
451
|
+
* and centres on it. `in sync` is drawn for the same reason: the idle disc
|
|
452
|
+
* must be exactly round, not a font's idea of a circle.
|
|
453
|
+
*
|
|
454
|
+
* `merge` is Remix Icon's `git-merge-line` (24x24), vendored VERBATIM rather
|
|
455
|
+
* than redrawn, and rendered at 16px to fill the row's 16px leading cell.
|
|
456
|
+
* Unicode has no merge glyph at all, so every tool draws this shape family.
|
|
457
|
+
* Its ink is inset (about 16 of its 24 units), so at 16px it puts down
|
|
458
|
+
* ~10.7px — the arrows' weight, which a 12px render would miss.
|
|
459
|
+
*
|
|
460
|
+
* The other five tokens stay text.
|
|
582
461
|
*/
|
|
583
|
-
const
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
462
|
+
const DRAWN_ICONS = {
|
|
463
|
+
push: { size: 12, viewBox: "0 0 12 12", shapes: [{ d: "M6 1 L10.6 6.6 H7.5 V11 H4.5 V6.6 H1.4 Z" }] },
|
|
464
|
+
pull: { size: 12, viewBox: "0 0 12 12", shapes: [{ d: "M6 11 L1.4 5.4 H4.5 V1 H7.5 V5.4 H10.6 Z" }] },
|
|
465
|
+
sync: {
|
|
466
|
+
size: 12,
|
|
467
|
+
viewBox: "0 0 12 12",
|
|
468
|
+
shapes: [
|
|
469
|
+
{ d: "M1.5 3.4 H8.4 V2 L11 4 L8.4 6 V4.6 H1.5 Z" },
|
|
470
|
+
{ d: "M10.5 8.6 H3.6 V10 L1 8 L3.6 6 V7.4 H10.5 Z" }
|
|
471
|
+
]
|
|
472
|
+
},
|
|
473
|
+
update: {
|
|
474
|
+
size: 12,
|
|
475
|
+
viewBox: "0 0 12 12",
|
|
476
|
+
shapes: [
|
|
477
|
+
{ d: "M6 1.9 A4.1 4.1 0 1 1 1.9 6", stroke: true },
|
|
478
|
+
{ d: "M4.3 0.4 L8.3 1.9 L4.3 3.4 Z" }
|
|
479
|
+
]
|
|
480
|
+
},
|
|
481
|
+
"in sync": { size: 12, viewBox: "0 0 12 12", shapes: [{ circle: true }] },
|
|
482
|
+
// Remix Icon `git-merge-line` (24x24), vendored verbatim from
|
|
483
|
+
// https://github.com/Remix-Design/RemixIcon/blob/master/icons/Development/git-merge-line.svg
|
|
484
|
+
// EXP-1: this is an experiment. Swapping back to GitHub's Octicon
|
|
485
|
+
// `git-merge` is this one entry — a 16x16 grid, same shape family.
|
|
486
|
+
merge: {
|
|
487
|
+
size: 16,
|
|
488
|
+
viewBox: "0 0 24 24",
|
|
489
|
+
shapes: [{ d: "M7.10508 8.78991C7.45179 10.0635 8.61653 11 10 11H14C16.4703 11 18.5222 12.7915 18.9274 15.1461C20.1303 15.5367 21 16.6668 21 18C21 19.6569 19.6569 21 18 21C16.3431 21 15 19.6569 15 18C15 16.7334 15.7849 15.6501 16.8949 15.2101C16.5482 13.9365 15.3835 13 14 13H10C8.87439 13 7.83566 12.6281 7 12.0004V15.1707C8.16519 15.5825 9 16.6938 9 18C9 19.6569 7.65685 21 6 21C4.34315 21 3 19.6569 3 18C3 16.6938 3.83481 15.5825 5 15.1707V8.82929C3.83481 8.41746 3 7.30622 3 6C3 4.34315 4.34315 3 6 3C7.65685 3 9 4.34315 9 6C9 7.26661 8.21506 8.34988 7.10508 8.78991ZM6 7C6.55228 7 7 6.55228 7 6C7 5.44772 6.55228 5 6 5C5.44772 5 5 5.44772 5 6C5 6.55228 5.44772 7 6 7ZM6 19C6.55228 19 7 18.5523 7 18C7 17.4477 6.55228 17 6 17C5.44772 17 5 17.4477 5 18C5 18.5523 5.44772 19 6 19ZM18 19C18.5523 19 19 18.5523 19 18C19 17.4477 18.5523 17 18 17C17.4477 17 17 17.4477 17 18C17 18.5523 17.4477 19 18 19Z" }]
|
|
490
|
+
}
|
|
590
491
|
};
|
|
591
492
|
|
|
493
|
+
/**
|
|
494
|
+
* The icon NODE: a drawn shape for the six drawn tokens, a text glyph for the
|
|
495
|
+
* rest. The parent span carries the accessible name, so the drawing is
|
|
496
|
+
* decoration (`aria-hidden`).
|
|
497
|
+
*/
|
|
498
|
+
function actionGlyphNode(token) {
|
|
499
|
+
const icon = DRAWN_ICONS[token];
|
|
500
|
+
if (icon === void 0) return ACTION_GLYPH[token] ?? token;
|
|
501
|
+
return react_jsx_runtime.jsx("svg", {
|
|
502
|
+
viewBox: icon.viewBox,
|
|
503
|
+
width: icon.size,
|
|
504
|
+
height: icon.size,
|
|
505
|
+
"aria-hidden": true,
|
|
506
|
+
focusable: "false",
|
|
507
|
+
style: { display: "block", flex: "none", color: "inherit" },
|
|
508
|
+
children: icon.shapes.map((shape, index) => shape.circle === true
|
|
509
|
+
? react_jsx_runtime.jsx("circle", {
|
|
510
|
+
cx: shape.cx ?? 6,
|
|
511
|
+
cy: shape.cy ?? 6,
|
|
512
|
+
r: shape.r ?? 5,
|
|
513
|
+
fill: "currentColor"
|
|
514
|
+
}, "shape-" + index)
|
|
515
|
+
: react_jsx_runtime.jsx("path", shape.stroke === true
|
|
516
|
+
? { d: shape.d, fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round" }
|
|
517
|
+
: { d: shape.d, fill: "currentColor" }, "shape-" + index))
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* The icon's own type style, for the TEXT glyphs only. A drawn arrow is a
|
|
523
|
+
* fixed 12px box, so size and weight do not apply to it; the six text glyphs
|
|
524
|
+
* sit one pixel larger than the surrounding 12px text.
|
|
525
|
+
*/
|
|
526
|
+
const GLYPH_TEXT = { fontSize: "13px" };
|
|
527
|
+
|
|
592
528
|
/**
|
|
593
529
|
* The row's own run state, from the shell's standard kit.
|
|
594
530
|
*
|
|
595
531
|
* The renderer that assembles slots hands EVERY entry the sessions store as
|
|
596
532
|
* a selector hook (`useSessions`) — the same kit that supplies
|
|
597
533
|
* `useWorkspaces` — which is how a row can tell an idle session from one
|
|
598
|
-
* mid-turn without the
|
|
599
|
-
* that kit leaves the prop undefined: then we cannot know, and the
|
|
600
|
-
* behaves exactly as it did before rather than guessing "idle".
|
|
534
|
+
* mid-turn without the row slot having to carry it. A shell that does not
|
|
535
|
+
* seed that kit leaves the prop undefined: then we cannot know, and the
|
|
536
|
+
* glyph behaves exactly as it did before rather than guessing "idle".
|
|
601
537
|
*
|
|
602
538
|
* One helper and one call site, so the hook order is identical on every path
|
|
603
539
|
* (the fallback is a plain function, never a hook).
|
|
@@ -608,8 +544,8 @@ window.__ModuleLoader__.load({
|
|
|
608
544
|
}
|
|
609
545
|
|
|
610
546
|
/**
|
|
611
|
-
* Report the idle gate ONCE, the way the boot diagnostics report the
|
|
612
|
-
*
|
|
547
|
+
* Report the idle gate ONCE, the way the boot diagnostics report the
|
|
548
|
+
* Tooltip primitive. Whether the shell actually handed a slot the
|
|
613
549
|
* sessions store is invisible from anywhere else, and a gate that silently
|
|
614
550
|
* never fires is indistinguishable from a gate that never needed to.
|
|
615
551
|
*/
|
|
@@ -626,60 +562,357 @@ window.__ModuleLoader__.load({
|
|
|
626
562
|
}
|
|
627
563
|
|
|
628
564
|
/**
|
|
629
|
-
* The
|
|
630
|
-
*
|
|
631
|
-
* git suggestion is noise: the checkout is still moving and the agent has not
|
|
632
|
-
* finished, so the action describes a state that is about to change. Nothing
|
|
633
|
-
* about the action itself changes; the gate is the whole feature.
|
|
565
|
+
* The leading-cell token glyph — the row's compact signal, on upstream's
|
|
566
|
+
* own `sidebar.session.row.leading` seat. No patch and no seam.
|
|
634
567
|
*
|
|
635
|
-
*
|
|
636
|
-
*
|
|
568
|
+
* It asks for PR state too (`pr: true`), the SAME query the row card uses,
|
|
569
|
+
* so the glyph shows the same PR tokens (`merge`, `review`, `fix CI`) as
|
|
570
|
+
* the card — not only the local ones. The matching query string means the
|
|
571
|
+
* glyph and the card share one cache entry and one in-flight read. The
|
|
572
|
+
* price is one forge read per row per TTL.
|
|
573
|
+
*
|
|
574
|
+
* Geometry: the cell is `width:16px; flex:none` and the row has `gap:0`, so
|
|
575
|
+
* a word would overflow into the title. Only one glyph is drawn, and no
|
|
576
|
+
* wrapper changes the cell's box.
|
|
577
|
+
*
|
|
578
|
+
* The glyph is NOT `aria-hidden`: with no word beside it, it is the
|
|
579
|
+
* accessible name. With nothing to say the cell renders NOTHING: no token
|
|
580
|
+
* means no action, so there is no icon and no colour to explain.
|
|
581
|
+
*
|
|
582
|
+
* It carries NO hover of its own. The row is already a HoverCard anchor,
|
|
583
|
+
* and the explanation rides the row's own card (SessionGitDetail) — so
|
|
584
|
+
* hovering the glyph shows exactly what hovering the session line shows.
|
|
637
585
|
*/
|
|
638
|
-
function
|
|
586
|
+
function LeadingTokenGlyph({ sessionId, useSessions }) {
|
|
639
587
|
const info = useGitStatus(sessionId === void 0 ? void 0 : { kind: "session", id: sessionId }, { pr: true });
|
|
640
588
|
const running = useSessionRunning(useSessions, sessionId);
|
|
641
589
|
reportIdleGate(typeof useSessions === "function");
|
|
642
590
|
if (running) return null;
|
|
643
591
|
if (sessionId === void 0 || info === void 0 || info.git !== true) return null;
|
|
644
|
-
const
|
|
645
|
-
if (
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
592
|
+
const token = info.rowToken;
|
|
593
|
+
if (typeof token === "string" && token !== "") {
|
|
594
|
+
const colour = SEVERITY_FILL[info.rowSeverity];
|
|
595
|
+
return react_jsx_runtime.jsx("span", {
|
|
596
|
+
role: "img",
|
|
597
|
+
"aria-label": "git action: " + token,
|
|
598
|
+
// The fallback overlay in the SAME cell is suppressed by this
|
|
599
|
+
// marker, so a status transition can never paint two glyphs.
|
|
600
|
+
"data-git-badge-leading": "",
|
|
601
|
+
style: { ...META_STYLE, ...(colour === void 0 ? {} : { color: colour }), ...GLYPH_TEXT, display: "inline-flex", alignItems: "center" },
|
|
602
|
+
children: actionGlyphNode(token)
|
|
603
|
+
});
|
|
655
604
|
}
|
|
656
|
-
|
|
657
|
-
// word, and the trailing space in its own text separates the two on screen
|
|
658
|
-
// without a margin. A missing glyph entry degrades to the word alone.
|
|
659
|
-
const glyph = ACTION_GLYPH[action];
|
|
660
|
-
return react_jsx_runtime.jsxs("span", {
|
|
661
|
-
style,
|
|
662
|
-
children:
|
|
663
|
-
glyph === void 0
|
|
664
|
-
? action
|
|
665
|
-
: [react_jsx_runtime.jsx("span", { "aria-hidden": true, children: `${glyph} ` }, "glyph"), action]
|
|
666
|
-
});
|
|
605
|
+
return null;
|
|
667
606
|
}
|
|
668
607
|
|
|
608
|
+
//#region leading-cell fallback (a solid status dot owns the cell)
|
|
609
|
+
/**
|
|
610
|
+
* The leading cell's SOLID status dots. Upstream replaces the whole
|
|
611
|
+
* `sidebar.session.row.leading` seat with one of these whenever the row's
|
|
612
|
+
* primary state is not idle, so the plugin's glyph is not mounted at all:
|
|
613
|
+
* `done` (green) when a background turn finished unread, `warning` (orange)
|
|
614
|
+
* when a pending interaction waits. `ongoing` is the running spinner, an
|
|
615
|
+
* `svg` with no solid dot, and stays upstream's alone.
|
|
616
|
+
*/
|
|
617
|
+
const ROW_FALLBACK_DOT = '[data-state="done"], [data-state="warning"]';
|
|
618
|
+
|
|
669
619
|
/**
|
|
670
|
-
*
|
|
671
|
-
*
|
|
672
|
-
*
|
|
673
|
-
*
|
|
620
|
+
* The fallback's stylesheet. The plugin paints the token into the occupied
|
|
621
|
+
* 16px cell from a marker attribute, so it adds NO foreign node to a
|
|
622
|
+
* React-managed cell: the dot element steps aside and a `::after` draws the
|
|
623
|
+
* node half's glyph, coloured by the severity custom property.
|
|
674
624
|
*
|
|
675
|
-
*
|
|
676
|
-
*
|
|
677
|
-
*
|
|
625
|
+
* `:has()` keeps the real seat and the fallback exclusive even during the
|
|
626
|
+
* one commit where a row flips back to idle: a cell that already holds
|
|
627
|
+
* `data-git-badge-leading` never paints the overlay. A browser without
|
|
628
|
+
* `:has()` drops the whole rule, so the fallback is simply absent — it can
|
|
629
|
+
* never double-draw.
|
|
630
|
+
*/
|
|
631
|
+
const ROW_FALLBACK_OVERLAY = "[data-git-badge-overlay]:not(:has([data-git-badge-leading]))";
|
|
632
|
+
const ROW_FALLBACK_CSS = [
|
|
633
|
+
ROW_FALLBACK_OVERLAY + " > span:first-child > " + ROW_FALLBACK_DOT + " { display: none; }",
|
|
634
|
+
ROW_FALLBACK_OVERLAY + " > span:first-child::after {",
|
|
635
|
+
' content: var(--git-badge-glyph, "");',
|
|
636
|
+
" display: inline-flex;",
|
|
637
|
+
" align-items: center;",
|
|
638
|
+
" justify-content: center;",
|
|
639
|
+
" width: 16px;",
|
|
640
|
+
" height: 20px;",
|
|
641
|
+
" color: var(--git-badge-color, currentColor);",
|
|
642
|
+
" font-size: 13px;",
|
|
643
|
+
" line-height: 1;",
|
|
644
|
+
"}",
|
|
645
|
+
ROW_FALLBACK_OVERLAY + "[data-git-badge-drawn] > span:first-child::after {",
|
|
646
|
+
' content: "";',
|
|
647
|
+
" background-color: var(--git-badge-color, currentColor);",
|
|
648
|
+
" -webkit-mask-image: var(--git-badge-mask-src);",
|
|
649
|
+
" mask-image: var(--git-badge-mask-src);",
|
|
650
|
+
" -webkit-mask-repeat: no-repeat;",
|
|
651
|
+
" mask-repeat: no-repeat;",
|
|
652
|
+
" -webkit-mask-position: center;",
|
|
653
|
+
" mask-position: center;",
|
|
654
|
+
" -webkit-mask-size: var(--git-badge-mask-size, 12px);",
|
|
655
|
+
" mask-size: var(--git-badge-mask-size, 12px);",
|
|
656
|
+
"}"
|
|
657
|
+
].join("\n");
|
|
658
|
+
|
|
659
|
+
/**
|
|
660
|
+
* One `DRAWN_ICONS` entry as a CSS mask URL, so the fallback paints the
|
|
661
|
+
* EXACT artwork the real seat draws. The mask supplies the shape and the
|
|
662
|
+
* element's `background-color` supplies the severity colour, which is why
|
|
663
|
+
* the SVG is painted black: only its alpha is read.
|
|
664
|
+
*/
|
|
665
|
+
function glyphMaskDataUri(icon) {
|
|
666
|
+
const shapes = icon.shapes.map((shape) => {
|
|
667
|
+
if (shape.circle === true) return '<circle cx="6" cy="6" r="5"/>';
|
|
668
|
+
if (shape.stroke === true) return '<path d="' + shape.d + '" fill="none" stroke="#000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>';
|
|
669
|
+
return '<path d="' + shape.d + '" fill="#000"/>';
|
|
670
|
+
}).join("");
|
|
671
|
+
const svg = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="' + icon.viewBox + '">' + shapes + "</svg>";
|
|
672
|
+
return 'url("data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svg) + '")';
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
/**
|
|
676
|
+
* The fallback's mark for a token, or undefined when the dot should stay.
|
|
678
677
|
*
|
|
679
|
-
*
|
|
680
|
-
*
|
|
681
|
-
*
|
|
682
|
-
*
|
|
678
|
+
* `in sync` is deliberately excluded: it is a disc, the row's own dot is
|
|
679
|
+
* also a dot, and replacing an unread marker with "nothing to do" would
|
|
680
|
+
* throw away the only signal that the turn ended. Only a real action earns
|
|
681
|
+
* the cell.
|
|
682
|
+
*/
|
|
683
|
+
function fallbackGlyph(token, severity) {
|
|
684
|
+
if (typeof token !== "string" || token === "" || token === "in sync") return void 0;
|
|
685
|
+
const colour = SEVERITY_FILL[severity];
|
|
686
|
+
const drawn = DRAWN_ICONS[token];
|
|
687
|
+
if (drawn !== void 0) return { mask: glyphMaskDataUri(drawn), size: drawn.size + "px", colour };
|
|
688
|
+
const glyph = ACTION_GLYPH[token];
|
|
689
|
+
if (glyph === void 0) return void 0;
|
|
690
|
+
return { glyph, colour };
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
/** Write one mark onto the row element. */
|
|
694
|
+
function applyFallbackMark(host, mark) {
|
|
695
|
+
host.setAttribute("data-git-badge-overlay", "");
|
|
696
|
+
if (mark.mask !== void 0) {
|
|
697
|
+
host.setAttribute("data-git-badge-drawn", "");
|
|
698
|
+
host.style.setProperty("--git-badge-mask-src", mark.mask);
|
|
699
|
+
host.style.setProperty("--git-badge-mask-size", mark.size);
|
|
700
|
+
host.style.removeProperty("--git-badge-glyph");
|
|
701
|
+
} else {
|
|
702
|
+
host.removeAttribute("data-git-badge-drawn");
|
|
703
|
+
host.style.removeProperty("--git-badge-mask-src");
|
|
704
|
+
host.style.removeProperty("--git-badge-mask-size");
|
|
705
|
+
// JSON.stringify keeps the quotes `content: var(...)` needs.
|
|
706
|
+
host.style.setProperty("--git-badge-glyph", JSON.stringify(mark.glyph));
|
|
707
|
+
}
|
|
708
|
+
if (mark.colour === void 0) host.style.removeProperty("--git-badge-color");
|
|
709
|
+
else host.style.setProperty("--git-badge-color", mark.colour);
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
/** Remove every trace of a mark, so the row is exactly as upstream left it. */
|
|
713
|
+
function clearFallbackMark(host) {
|
|
714
|
+
if (host === void 0 || host === null) return;
|
|
715
|
+
host.removeAttribute("data-git-badge-overlay");
|
|
716
|
+
host.removeAttribute("data-git-badge-drawn");
|
|
717
|
+
host.style.removeProperty("--git-badge-glyph");
|
|
718
|
+
host.style.removeProperty("--git-badge-mask-src");
|
|
719
|
+
host.style.removeProperty("--git-badge-mask-size");
|
|
720
|
+
host.style.removeProperty("--git-badge-color");
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
/** The row (`[data-row-key]`) an element sits in, or undefined. */
|
|
724
|
+
function rowHost(node) {
|
|
725
|
+
const closest = node?.closest;
|
|
726
|
+
if (typeof closest !== "function") return void 0;
|
|
727
|
+
return closest.call(node, "[data-row-key]") ?? void 0;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
/**
|
|
731
|
+
* The whole fallback decision in one testable call: does the cell hold a
|
|
732
|
+
* SOLID dot, and does the node half have an action to show? The cell is the
|
|
733
|
+
* authority, so no client-side copy of upstream's status precedence can
|
|
734
|
+
* drift. Returns whether the overlay is on.
|
|
735
|
+
*/
|
|
736
|
+
function syncRowFallback(host, info) {
|
|
737
|
+
if (host === void 0 || host === null) return false;
|
|
738
|
+
const cell = host.firstElementChild;
|
|
739
|
+
const occupied = cell !== void 0 && cell !== null && typeof cell.querySelector === "function" && cell.querySelector(ROW_FALLBACK_DOT) !== null;
|
|
740
|
+
if (!occupied) {
|
|
741
|
+
clearFallbackMark(host);
|
|
742
|
+
return false;
|
|
743
|
+
}
|
|
744
|
+
const token = info !== void 0 && info !== null && info.git === true ? info.rowToken : void 0;
|
|
745
|
+
const mark = fallbackGlyph(token, info?.rowSeverity);
|
|
746
|
+
if (mark === void 0) {
|
|
747
|
+
clearFallbackMark(host);
|
|
748
|
+
return false;
|
|
749
|
+
}
|
|
750
|
+
applyFallbackMark(host, mark);
|
|
751
|
+
return true;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
/** One stylesheet per page; the loader tags it for its own bookkeeping. */
|
|
755
|
+
let rowFallbackStyle = false;
|
|
756
|
+
function ensureRowFallbackStyle() {
|
|
757
|
+
if (rowFallbackStyle) return;
|
|
758
|
+
if (typeof document === "undefined" || document.head === void 0 || document.head === null) return;
|
|
759
|
+
try {
|
|
760
|
+
const style = document.createElement("style");
|
|
761
|
+
style.setAttribute("data-plugin", "dsh-git-badge");
|
|
762
|
+
style.textContent = ROW_FALLBACK_CSS;
|
|
763
|
+
document.head.appendChild(style);
|
|
764
|
+
rowFallbackStyle = true;
|
|
765
|
+
} catch {
|
|
766
|
+
// a missing or closed <head> must never break the badge
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
/**
|
|
771
|
+
* The marker that carries the fallback. It MUST ride
|
|
772
|
+
* `sidebar.workspaces.session.row.action`, because that strip is hidden by
|
|
773
|
+
* CSS rather than unmounted: it is the one per-row seat React mounts in
|
|
774
|
+
* every state, so it is the only place from which a row can be marked while
|
|
775
|
+
* upstream's dot owns the leading cell. It renders a hidden span and adds
|
|
776
|
+
* no button.
|
|
777
|
+
*/
|
|
778
|
+
function RowFallbackGlyph({ sessionId }) {
|
|
779
|
+
const ref = react.useRef(null);
|
|
780
|
+
const [occupied, setOccupied] = react.useState(false);
|
|
781
|
+
// `enabled` is the LAZY gate: a row with no solid dot keeps costing
|
|
782
|
+
// nothing, exactly as it did before the fallback existed.
|
|
783
|
+
const info = useGitStatus(sessionId === void 0 ? void 0 : { kind: "session", id: sessionId }, { pr: true, enabled: occupied });
|
|
784
|
+
react.useEffect(() => {
|
|
785
|
+
ensureRowFallbackStyle();
|
|
786
|
+
const host = rowHost(ref.current);
|
|
787
|
+
if (host === void 0) return;
|
|
788
|
+
const cell = host.firstElementChild;
|
|
789
|
+
if (cell === void 0 || cell === null || typeof cell.querySelector !== "function") return;
|
|
790
|
+
const sync = () => setOccupied(cell.querySelector(ROW_FALLBACK_DOT) !== null);
|
|
791
|
+
sync();
|
|
792
|
+
if (typeof MutationObserver !== "function") return;
|
|
793
|
+
const observer = new MutationObserver(sync);
|
|
794
|
+
// The dot swaps in as a child; a state change edits `data-state` in
|
|
795
|
+
// place, so both must be watched. The row's own attributes are NOT
|
|
796
|
+
// observed, which is what keeps the mark from re-triggering itself.
|
|
797
|
+
observer.observe(cell, { childList: true, subtree: true, attributes: true, attributeFilter: ["data-state"] });
|
|
798
|
+
return () => observer.disconnect();
|
|
799
|
+
}, [sessionId]);
|
|
800
|
+
// Layout effect: the mark lands in the same commit that reveals the dot,
|
|
801
|
+
// so the fallback does not flash the green/orange dot first.
|
|
802
|
+
react.useLayoutEffect(() => {
|
|
803
|
+
syncRowFallback(rowHost(ref.current), info);
|
|
804
|
+
}, [info, occupied]);
|
|
805
|
+
return react_jsx_runtime.jsx("span", { ref, "aria-hidden": true, style: { display: "none" } });
|
|
806
|
+
}
|
|
807
|
+
//#endregion
|
|
808
|
+
|
|
809
|
+
const CARD_CONTAINER = {
|
|
810
|
+
display: "flex",
|
|
811
|
+
flexDirection: "column",
|
|
812
|
+
gap: "4px",
|
|
813
|
+
fontSize: "12px",
|
|
814
|
+
lineHeight: "18px",
|
|
815
|
+
// Auto-width: the card fits its widest row instead of a guessed
|
|
816
|
+
// constant, capped at 600px. The Tooltip's maxWidth must stay >= the
|
|
817
|
+
// cap or IT becomes the clamp.
|
|
818
|
+
width: "max-content",
|
|
819
|
+
maxWidth: "600px"
|
|
820
|
+
};
|
|
821
|
+
const CARD_ROW = { display: "flex", gap: "8px", alignItems: "baseline" };
|
|
822
|
+
const CARD_LABEL = { color: "var(--dsw-alias-label-tertiary, #9ea7ad)", flex: "none", minWidth: "62px" };
|
|
823
|
+
const CARD_VALUE = { minWidth: 0, overflowWrap: "anywhere", whiteSpace: "normal" };
|
|
824
|
+
// The chip card's HEADER: the action sentence as a heading, not a labelled
|
|
825
|
+
// row. `fontWeight` is also the hook the suite uses to find it.
|
|
826
|
+
const CHIP_HEADER = { display: "flex", alignItems: "baseline", gap: "6px", fontWeight: 600 };
|
|
827
|
+
// A section rule between the checkout rows and the branch rows. The label
|
|
828
|
+
// makes the split explicit instead of leaving `state`/`base` to look like
|
|
829
|
+
// more checkout detail.
|
|
830
|
+
const CARD_DIVIDER = {
|
|
831
|
+
marginTop: "4px",
|
|
832
|
+
paddingTop: "6px",
|
|
833
|
+
borderTop: ".5px solid var(--dsw-alias-border-l4, #e4e8eb)",
|
|
834
|
+
color: "var(--dsw-alias-label-tertiary, #9ea7ad)",
|
|
835
|
+
fontSize: "10px",
|
|
836
|
+
lineHeight: "14px",
|
|
837
|
+
letterSpacing: ".08em",
|
|
838
|
+
textTransform: "uppercase"
|
|
839
|
+
};
|
|
840
|
+
|
|
841
|
+
/** One label/value row — the shape every hover body shares. */
|
|
842
|
+
function cardRow(label, value) {
|
|
843
|
+
return react_jsx_runtime.jsxs("div", {
|
|
844
|
+
style: CARD_ROW,
|
|
845
|
+
children: [
|
|
846
|
+
react_jsx_runtime.jsx("span", { style: CARD_LABEL, children: label }),
|
|
847
|
+
react_jsx_runtime.jsx("span", { style: CARD_VALUE, children: value })
|
|
848
|
+
]
|
|
849
|
+
}, label);
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
/** Push one label/value row unless the value is empty. */
|
|
853
|
+
function addRow(rows, label, value) {
|
|
854
|
+
if (value === void 0 || value === null || value === "") return;
|
|
855
|
+
rows.push(cardRow(label, value));
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
/**
|
|
859
|
+
* The action SENTENCE as a raw value. SHARED by the session-row card
|
|
860
|
+
* (actionRow wraps it in the card label/value shape) and the chip card
|
|
861
|
+
* (actionHeader wraps it in a heading), so the two cannot describe the icon
|
|
862
|
+
* differently. Returns undefined when the checkout has no token.
|
|
863
|
+
*
|
|
864
|
+
* The glyph is DECORATION here: the word follows it, so `aria-hidden` keeps
|
|
865
|
+
* the accessible name the bare action.
|
|
866
|
+
*/
|
|
867
|
+
function actionRow(info) {
|
|
868
|
+
const token = info?.rowToken;
|
|
869
|
+
if (typeof token !== "string" || token === "") return void 0;
|
|
870
|
+
const why = typeof info.rowTokenWhy === "string" && info.rowTokenWhy !== "" ? info.rowTokenWhy : void 0;
|
|
871
|
+
const colour = SEVERITY_FILL[info.rowSeverity];
|
|
872
|
+
const value = [];
|
|
873
|
+
if (DRAWN_ICONS[token] !== void 0 || ACTION_GLYPH[token] !== void 0) {
|
|
874
|
+
value.push(react_jsx_runtime.jsx("span", {
|
|
875
|
+
"aria-hidden": true,
|
|
876
|
+
style: { color: colour === void 0 ? META_STYLE.color : colour, fontWeight: 700, display: "inline-flex", alignItems: "center" },
|
|
877
|
+
children: actionGlyphNode(token)
|
|
878
|
+
}, "action-glyph"));
|
|
879
|
+
value.push(" ");
|
|
880
|
+
}
|
|
881
|
+
value.push(token + (why === void 0 ? "" : " \u2014 " + why));
|
|
882
|
+
return value;
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
/**
|
|
886
|
+
* The chip card's HEADER: the same sentence actionRow produces, rendered as a
|
|
887
|
+
* heading so the checkout's verdict leads the card instead of sitting in a
|
|
888
|
+
* labelled row. It calls actionRow, so the two surfaces cannot drift.
|
|
889
|
+
*/
|
|
890
|
+
function actionHeader(info) {
|
|
891
|
+
const value = actionRow(info);
|
|
892
|
+
if (value === void 0) return void 0;
|
|
893
|
+
return react_jsx_runtime.jsx("div", { style: CHIP_HEADER, children: value }, "action-header");
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
/**
|
|
897
|
+
* Session-row hover card — everything the action glyph cannot say: what the
|
|
898
|
+
* action means and why, which checkout the row's status describes, and which
|
|
899
|
+
* pull request it refers to. It renders through the upstream
|
|
900
|
+
* sidebar.session.row.hover seat, inside the card upstream already opens for
|
|
901
|
+
* a row.
|
|
902
|
+
*
|
|
903
|
+
* LABEL/VALUE ROWS, the shape the chip card uses. The `action` row comes from
|
|
904
|
+
* the shared actionRow() helper, so the two cards cannot describe the icon
|
|
905
|
+
* differently; `checkout` and `pull request` follow.
|
|
906
|
+
*
|
|
907
|
+
* This matters MORE now that the row shows only a glyph: `merge` with no way
|
|
908
|
+
* to see merge WHAT would be a nag rather than a hint, so the card renders
|
|
909
|
+
* whenever there is something to name — the row's action, a worktree, a pull
|
|
910
|
+
* request, or any combination.
|
|
911
|
+
*
|
|
912
|
+
* It makes no request of its own: the same `{ pr: true }` query as the
|
|
913
|
+
* leading glyph resolves to the same cache key, so a card that mounts for
|
|
914
|
+
* every row still costs one fetch per row, and a cold cache is served by
|
|
915
|
+
* the glyph's own in-flight request.
|
|
683
916
|
*
|
|
684
917
|
* Only a WORKTREE's checkout is spelled out. For a main checkout the branch is
|
|
685
918
|
* the ordinary case and repeating it on every session's card would be noise;
|
|
@@ -688,18 +921,35 @@ window.__ModuleLoader__.load({
|
|
|
688
921
|
function SessionGitDetail({ sessionId }) {
|
|
689
922
|
const info = useGitStatus(sessionId === void 0 ? void 0 : { kind: "session", id: sessionId }, { pr: true });
|
|
690
923
|
if (info === void 0 || info.git !== true) return null;
|
|
691
|
-
const
|
|
924
|
+
const rows = [];
|
|
925
|
+
const add = (label, value) => addRow(rows, label, value);
|
|
926
|
+
// The HEADER, the shape the chip card leads with: the same sentence from
|
|
927
|
+
// the same actionRow, so the two cards cannot describe the icon
|
|
928
|
+
// differently, and the sentence gains the 62px label column's width.
|
|
929
|
+
const header = actionHeader(info);
|
|
930
|
+
if (header !== void 0) rows.push(header);
|
|
692
931
|
if (info.isWorktree === true) {
|
|
932
|
+
// WHICH tree, not just that there is one: the row's glyph cannot say
|
|
933
|
+
// it, and the branch is ordinary for a main checkout.
|
|
693
934
|
const name = typeof info.worktreeName === "string" ? info.worktreeName : "";
|
|
694
|
-
|
|
695
|
-
// the node half followed this tree because it is the one whose branch
|
|
696
|
-
// has the open PR — say so, because nothing on the row can
|
|
697
|
-
|
|
698
|
-
bits.push("checkout: " + checkout.join(" \u00B7 "));
|
|
935
|
+
add("checkout", name === "" ? String(info.branch) : name + " on " + info.branch);
|
|
699
936
|
}
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
937
|
+
// The dirty breakdown, so a higher-ranked token cannot hide uncommitted
|
|
938
|
+
// work. formatFileBreakdown says "clean" when there is none; a clean row
|
|
939
|
+
// keeps its short card.
|
|
940
|
+
const changes = formatFileBreakdown(info);
|
|
941
|
+
if (changes !== "clean") add("changes", changes);
|
|
942
|
+
add("pull request", formatPrDetail(info.pr));
|
|
943
|
+
if (rows.length === 0) return null;
|
|
944
|
+
// The row's hover card is upstream's FIXED-WIDTH box: HoverCard.module.css
|
|
945
|
+
// pins `.card` to 244px. `width: max-content` (CARD_CONTAINER) would
|
|
946
|
+
// overflow it, so the stack FILLS the box instead and the value cells wrap
|
|
947
|
+
// inside it. The chip card is different — its Tooltip sizes to the content,
|
|
948
|
+
// so it keeps max-content.
|
|
949
|
+
return react_jsx_runtime.jsx("div", {
|
|
950
|
+
style: { ...CARD_CONTAINER, width: "100%", maxWidth: "100%" },
|
|
951
|
+
children: rows
|
|
952
|
+
});
|
|
703
953
|
}
|
|
704
954
|
|
|
705
955
|
/**
|
|
@@ -722,7 +972,7 @@ window.__ModuleLoader__.load({
|
|
|
722
972
|
/** The PR's full state as text, for the card: "#142 · checks failing · draft · review approved". */
|
|
723
973
|
function formatPrDetail(pr) {
|
|
724
974
|
if (pr === void 0 || pr === null || pr.number === void 0) return void 0;
|
|
725
|
-
const state =
|
|
975
|
+
const state = prStateLabel(pr);
|
|
726
976
|
const bits = ["#" + pr.number, state];
|
|
727
977
|
if (pr.draft === true) bits.push("draft");
|
|
728
978
|
if (pr.review !== void 0) bits.push(String(pr.review).toLowerCase().replace(/_/g, " "));
|
|
@@ -810,50 +1060,6 @@ window.__ModuleLoader__.load({
|
|
|
810
1060
|
});
|
|
811
1061
|
}
|
|
812
1062
|
|
|
813
|
-
/**
|
|
814
|
-
* The PR token's own hover body — the commits IN THIS PULL REQUEST
|
|
815
|
-
* (`base..HEAD`), and nothing else: the token already says the number and
|
|
816
|
-
* CI state, so the one question left is "what is in it?". Same line format
|
|
817
|
-
* as the branch hover's commit list (see CommitLine — one renderer, so the
|
|
818
|
-
* columns line up in both by construction), and the card's own width
|
|
819
|
-
* behaviour: full-width lines, nowrap, ellipsis.
|
|
820
|
-
*/
|
|
821
|
-
function PrCommitsList({ info, pending = false }) {
|
|
822
|
-
const commits = info?.prCommits;
|
|
823
|
-
if (!Array.isArray(commits) || commits.length === 0) {
|
|
824
|
-
// `pending`: the detail payload has not landed yet, so "no commits"
|
|
825
|
-
// would be a false negative — name the pull request the token points
|
|
826
|
-
// at instead, the way the mark's card degrades to the base response.
|
|
827
|
-
if (pending) {
|
|
828
|
-
return react_jsx_runtime.jsx("span", { style: CARD_CONTAINER, children: formatPrDetail(info?.pr) ?? "loading\u2026" });
|
|
829
|
-
}
|
|
830
|
-
return react_jsx_runtime.jsx("span", { style: CARD_CONTAINER, children: "no commits beyond the base branch" });
|
|
831
|
-
}
|
|
832
|
-
return react_jsx_runtime.jsxs("div", { style: CARD_CONTAINER, children: [
|
|
833
|
-
react_jsx_runtime.jsxs("div", {
|
|
834
|
-
style: { display: "flex", flexDirection: "column", gap: "2px", minWidth: 0 },
|
|
835
|
-
children: [
|
|
836
|
-
...commits.map((commit) => react_jsx_runtime.jsx(CommitLine, { commit }, commit.hash)),
|
|
837
|
-
(typeof info.prCommitsTotal === "number" && info.prCommitsTotal > commits.length
|
|
838
|
-
? "\u2026 and " + (info.prCommitsTotal - commits.length) + " more"
|
|
839
|
-
: null)
|
|
840
|
-
]
|
|
841
|
-
})
|
|
842
|
-
] });
|
|
843
|
-
}
|
|
844
|
-
|
|
845
|
-
const CARD_CONTAINER = {
|
|
846
|
-
display: "flex",
|
|
847
|
-
flexDirection: "column",
|
|
848
|
-
gap: "4px",
|
|
849
|
-
fontSize: "12px",
|
|
850
|
-
lineHeight: "18px",
|
|
851
|
-
// Auto-width: the card fits its widest row instead of a guessed
|
|
852
|
-
// constant, capped at 600px. The Tooltip's maxWidth must stay >= the
|
|
853
|
-
// cap or IT becomes the clamp.
|
|
854
|
-
width: "max-content",
|
|
855
|
-
maxWidth: "600px"
|
|
856
|
-
};
|
|
857
1063
|
/**
|
|
858
1064
|
* The ✎n count's own hover body — the changed-file lists and NOTHING
|
|
859
1065
|
* else: the full card stays on the status mark, and this tooltip answers
|
|
@@ -925,35 +1131,31 @@ window.__ModuleLoader__.load({
|
|
|
925
1131
|
}
|
|
926
1132
|
|
|
927
1133
|
/**
|
|
928
|
-
* The branch
|
|
929
|
-
* renders
|
|
930
|
-
*
|
|
931
|
-
* stop — no base-relative filtering, which
|
|
932
|
-
* merge). The pull request has no row here — the
|
|
933
|
-
* already carries number, CI state and the link.
|
|
1134
|
+
* The branch LINEAGE rows, for the chip's one card. The state row always
|
|
1135
|
+
* renders (the pull-request verdict, or the base-branch note); the base row
|
|
1136
|
+
* follows when the counts are known; then the branch's last 10 commits
|
|
1137
|
+
* (WHAT THIS BRANCH HAS, full stop — no base-relative filtering, which
|
|
1138
|
+
* empties on every push or merge). The pull request has no row here — the
|
|
1139
|
+
* chip's own PR token already carries number, CI state and the link.
|
|
934
1140
|
*/
|
|
935
|
-
function
|
|
1141
|
+
function lineageRows(info) {
|
|
936
1142
|
const rows = [];
|
|
937
|
-
const add = (label, value) =>
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
//
|
|
942
|
-
// rows spoke about the push axis (the branch's own remote), which the
|
|
943
|
-
// merge row subsumes: "1 ahead, 0 behind main" is the same fact stated
|
|
944
|
-
// against the branch that matters, in words rather than arrows. A row
|
|
945
|
-
// that can be absent is a row the reader has to reconstruct.
|
|
1143
|
+
const add = (label, value) => addRow(rows, label, value);
|
|
1144
|
+
// The STATE row always renders. The old upstream and sync rows spoke
|
|
1145
|
+
// about the push axis (the branch's own remote), which the state row
|
|
1146
|
+
// subsumes: the merge verdict is the fact that matters. The base counts
|
|
1147
|
+
// stay a SEPARATE row below, so a reader never reconstructs either.
|
|
946
1148
|
const pr = info?.pr;
|
|
947
|
-
const
|
|
1149
|
+
const stateParts = [];
|
|
948
1150
|
if (pr !== void 0 && pr !== null && pr.number !== void 0) {
|
|
949
|
-
if (pr.draft === true)
|
|
1151
|
+
if (pr.draft === true) stateParts.push("blocked: draft");
|
|
950
1152
|
else if (pr.mergeState === "BLOCKED") {
|
|
951
|
-
|
|
1153
|
+
stateParts.push(pr.review === "REVIEW_REQUIRED" ? "blocked: review required"
|
|
952
1154
|
: pr.state === "failing" ? "blocked: checks failing"
|
|
953
1155
|
: "blocked");
|
|
954
|
-
} else if (pr.state === "failing")
|
|
955
|
-
else if (pr.mergeState === "CLEAN" || (pr.state === "passing" && pr.review === "APPROVED"))
|
|
956
|
-
else if (pr.review === "REVIEW_REQUIRED")
|
|
1156
|
+
} else if (pr.state === "failing") stateParts.push("blocked: checks failing");
|
|
1157
|
+
else if (pr.mergeState === "CLEAN" || (pr.state === "passing" && pr.review === "APPROVED")) stateParts.push("ready to merge");
|
|
1158
|
+
else if (pr.review === "REVIEW_REQUIRED") stateParts.push("blocked: review required");
|
|
957
1159
|
} else if (info?.defaultBranch === true) {
|
|
958
1160
|
// The default branch is the BASE, not a branch with a missing PR:
|
|
959
1161
|
// pull requests merge INTO it and none is ever opened FROM it, so
|
|
@@ -961,18 +1163,21 @@ window.__ModuleLoader__.load({
|
|
|
961
1163
|
// exist. The node half computes `defaultBranch` for exactly this
|
|
962
1164
|
// suppression (see the comment where it is set in lib/index.js);
|
|
963
1165
|
// until now only the action rules consulted it, not this row.
|
|
964
|
-
|
|
1166
|
+
stateParts.push("base branch \u00B7 pull requests merge into this");
|
|
965
1167
|
} else {
|
|
966
|
-
|
|
1168
|
+
stateParts.push("no pull request");
|
|
967
1169
|
}
|
|
968
|
-
// The
|
|
969
|
-
|
|
970
|
-
//
|
|
971
|
-
//
|
|
1170
|
+
// The state row states the PR verdict and nothing else.
|
|
1171
|
+
add("state", stateParts.join(" \u00B7 "));
|
|
1172
|
+
// The counts are this branch's standing against its BASE, so on the base
|
|
1173
|
+
// itself they are self-referential ("N ahead, M behind main" while ON
|
|
1174
|
+
// main) and the divergence they would state is already the chip's own
|
|
1175
|
+
// \u2191a \u2193b token. Suppress them on the default branch, with or without a
|
|
1176
|
+
// PR. A SEPARATE row from state: whether the PR can merge and how far the
|
|
1177
|
+
// branch is from its base are two facts, so neither hides the other.
|
|
972
1178
|
if (info?.defaultBranch !== true && (info?.mainAhead !== void 0 || info?.mainBehind !== void 0)) {
|
|
973
|
-
|
|
1179
|
+
add("base", (info.mainAhead || 0) + " ahead, " + (info.mainBehind || 0) + " behind " + baseNameFor(info));
|
|
974
1180
|
}
|
|
975
|
-
add("merge", mergeParts.join(" \u00B7 "));
|
|
976
1181
|
const commits = info?.branchCommits;
|
|
977
1182
|
if (Array.isArray(commits) && commits.length > 0) {
|
|
978
1183
|
// Full-width block, not the label/value two-column row: commit lines
|
|
@@ -993,13 +1198,9 @@ window.__ModuleLoader__.load({
|
|
|
993
1198
|
}
|
|
994
1199
|
// NO pull-request row: the chip's own PR token (number, CI state, link)
|
|
995
1200
|
// carries it — a second rendering two pixels away is pure duplication.
|
|
996
|
-
return
|
|
1201
|
+
return rows;
|
|
997
1202
|
}
|
|
998
1203
|
|
|
999
|
-
const CARD_ROW = { display: "flex", gap: "8px", alignItems: "baseline" };
|
|
1000
|
-
const CARD_LABEL = { color: "var(--dsw-alias-label-tertiary, #9ea7ad)", flex: "none", minWidth: "62px" };
|
|
1001
|
-
const CARD_VALUE = { minWidth: 0, overflowWrap: "anywhere" };
|
|
1002
|
-
|
|
1003
1204
|
// The host's plugin-card chrome, reproduced inline: its CSS module is not
|
|
1004
1205
|
// exported to plugins, so these values mirror PluginCard.module.css so our
|
|
1005
1206
|
// card sits in the same list as its siblings.
|
|
@@ -1045,52 +1246,43 @@ window.__ModuleLoader__.load({
|
|
|
1045
1246
|
flex: "none"
|
|
1046
1247
|
};
|
|
1047
1248
|
|
|
1048
|
-
/** One label/value row — the shape every hover body shares. */
|
|
1049
|
-
function cardRow(label, value) {
|
|
1050
|
-
return react_jsx_runtime.jsxs("div", {
|
|
1051
|
-
style: CARD_ROW,
|
|
1052
|
-
children: [
|
|
1053
|
-
react_jsx_runtime.jsx("span", { style: CARD_LABEL, children: label }),
|
|
1054
|
-
react_jsx_runtime.jsx("span", { style: CARD_VALUE, children: value })
|
|
1055
|
-
]
|
|
1056
|
-
}, label);
|
|
1057
|
-
}
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
1249
|
/**
|
|
1061
|
-
*
|
|
1062
|
-
* over fields the status response already carries, plus
|
|
1063
|
-
* additions (recent commits, stash) when they have arrived.
|
|
1064
|
-
* spelled out in text, so no fact here depends on a colour or
|
|
1250
|
+
* The checkout's verdict rows, for the chip's one card — INPUT CHIP ONLY.
|
|
1251
|
+
* Pure presentation over fields the status response already carries, plus
|
|
1252
|
+
* the `detail=1` additions (recent commits, stash) when they have arrived.
|
|
1253
|
+
* Everything is spelled out in text, so no fact here depends on a colour or
|
|
1254
|
+
* a glyph.
|
|
1065
1255
|
*/
|
|
1066
|
-
function
|
|
1256
|
+
function statusRows(info, detail) {
|
|
1067
1257
|
// the detail payload is a superset of the plain one, so it wins when it
|
|
1068
1258
|
// has landed; until then the card still shows the base facts
|
|
1069
1259
|
const data = detail === void 0 ? info : detail;
|
|
1070
1260
|
const rows = [];
|
|
1071
|
-
const add = (label, value) =>
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
//
|
|
1078
|
-
//
|
|
1079
|
-
//
|
|
1080
|
-
//
|
|
1081
|
-
//
|
|
1261
|
+
const add = (label, value) => addRow(rows, label, value);
|
|
1262
|
+
// The ACTION header, first: the SAME sentence the session-row card renders,
|
|
1263
|
+
// from the same helper, so the chip card explains the icon the row shows.
|
|
1264
|
+
// It is a heading, not a labelled row, so the verdict leads the card.
|
|
1265
|
+
const header = actionHeader(data);
|
|
1266
|
+
if (header !== void 0) rows.push(header);
|
|
1267
|
+
// The "next step" row — the SERVER's ranked suggestion, and nothing else. The
|
|
1268
|
+
// client's extras (secondary picker entries) must never promote
|
|
1269
|
+
// themselves into this line: when the server says there is nothing to do
|
|
1270
|
+
// — clean and synced — the row is omitted, and "no suggestion" stays a
|
|
1271
|
+
// suggestion. Plain text, no affordance: the actionable surface is the
|
|
1272
|
+
// (+) menu's /gh picker, and this row is the read-only pointer to it.
|
|
1082
1273
|
const topAction = data.next;
|
|
1083
1274
|
if (topAction !== void 0) {
|
|
1084
1275
|
// git-comment convention: the invocation, two spaces, then "# "
|
|
1085
|
-
// plus what it does
|
|
1086
|
-
|
|
1276
|
+
// plus what it does. The marker makes the one actionable line findable
|
|
1277
|
+
// among the detail rows.
|
|
1278
|
+
add("next step", "\u25B8 /gh " + topAction.args + " # " + topAction.what);
|
|
1087
1279
|
}
|
|
1088
1280
|
// The branch lineage rows (branch, upstream, sync, pull request) moved
|
|
1089
1281
|
// to the branch NAME's hover — the card is the checkout's verdict, not
|
|
1090
1282
|
// the branch's CV. The branch name is also the chip's own text, so a
|
|
1091
1283
|
// card row for it was pure repetition.
|
|
1092
|
-
add("
|
|
1093
|
-
add("operation",
|
|
1284
|
+
add("changes", formatFileBreakdown(data));
|
|
1285
|
+
add("operation", data.operation === void 0 || data.operation === null ? void 0 : String(data.operation));
|
|
1094
1286
|
// WHICH checkout the badge describes, when it is a worktree. The chip shows
|
|
1095
1287
|
// the branch alone, so this row is the only place the tree is named.
|
|
1096
1288
|
add("worktree", worktreeDetail(data));
|
|
@@ -1102,10 +1294,36 @@ window.__ModuleLoader__.load({
|
|
|
1102
1294
|
// The conversation's OWN directory, present only when the badge is
|
|
1103
1295
|
// describing a followed worktree — the chip's branch and counts are the
|
|
1104
1296
|
// tree's in that case, so this is where the checkout's own state stays
|
|
1105
|
-
// visible.
|
|
1106
|
-
|
|
1297
|
+
// visible. Labelled `own checkout` because `checkout` now names the
|
|
1298
|
+
// checkout a session-row card describes. Absent until the `detail=1`
|
|
1299
|
+
// response lands, like commits/stash.
|
|
1300
|
+
if (data.worktreeFollowed === true) add("own checkout", formatCheckoutDetail(data.checkout));
|
|
1107
1301
|
add("stash", data.stashCount === void 0 ? void 0 : data.stashCount + " stashed");
|
|
1108
|
-
return
|
|
1302
|
+
return rows;
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
/**
|
|
1306
|
+
* The chip's ONE card, behind the leading cluster (the action glyph and the
|
|
1307
|
+
* branch name together). It stacks the checkout's verdict rows over the
|
|
1308
|
+
* branch's lineage rows, so one hover answers both "what is this checkout
|
|
1309
|
+
* doing" and "what is on this branch".
|
|
1310
|
+
*
|
|
1311
|
+
* One card, not two: the glyph and the branch are one target, and the branch
|
|
1312
|
+
* is the element that always exists — so a clean checkout, which has no
|
|
1313
|
+
* glyph, still opens the card from its branch name.
|
|
1314
|
+
*/
|
|
1315
|
+
function ChipCard({ info, detail }) {
|
|
1316
|
+
const data = detail === void 0 ? info : detail;
|
|
1317
|
+
const rows = statusRows(info, detail);
|
|
1318
|
+
const lineage = lineageRows(data);
|
|
1319
|
+
return react_jsx_runtime.jsx("div", {
|
|
1320
|
+
style: CARD_CONTAINER,
|
|
1321
|
+
children: [
|
|
1322
|
+
...rows,
|
|
1323
|
+
lineage.length === 0 ? null : react_jsx_runtime.jsx("div", { style: CARD_DIVIDER, children: "branch" }, "branch-divider"),
|
|
1324
|
+
...lineage
|
|
1325
|
+
]
|
|
1326
|
+
});
|
|
1109
1327
|
}
|
|
1110
1328
|
|
|
1111
1329
|
//#region composer chip (upstream additive surface: conversation.input.left)
|
|
@@ -1128,211 +1346,152 @@ window.__ModuleLoader__.load({
|
|
|
1128
1346
|
}
|
|
1129
1347
|
|
|
1130
1348
|
/**
|
|
1131
|
-
*
|
|
1132
|
-
*
|
|
1133
|
-
*
|
|
1134
|
-
*
|
|
1135
|
-
* (conversation.input.left is an upstream additive slot), and stays useful
|
|
1136
|
-
* next to the sidebar rows because it is context-anchored ("where am I")
|
|
1137
|
-
* rather than surveying.
|
|
1138
|
-
*
|
|
1139
|
-
* This is the ONLY surface that asks the node half for the extras (`pr=1`
|
|
1140
|
-
* always, `detail=1` on hover), because it is the only one that renders
|
|
1141
|
-
* them — see targetQuery.
|
|
1349
|
+
* The chip's leading action glyph, or null when the checkout needs none.
|
|
1350
|
+
* The SAME token the sidebar row draws, in the node half's severity colour.
|
|
1351
|
+
* No token means NO GLYPH: a clean, synced checkout has no action to triage,
|
|
1352
|
+
* so it shows no icon rather than a decorative dot.
|
|
1142
1353
|
*/
|
|
1354
|
+
function chipGlyphNode(info) {
|
|
1355
|
+
const token = info.rowToken;
|
|
1356
|
+
if (typeof token !== "string" || token === "") return null;
|
|
1357
|
+
return react_jsx_runtime.jsx("span", {
|
|
1358
|
+
key: "glyph",
|
|
1359
|
+
role: "img",
|
|
1360
|
+
"aria-label": "git action: " + token,
|
|
1361
|
+
style: { color: SEVERITY_FILL[info.rowSeverity] ?? META_STYLE.color, flex: "none", ...GLYPH_TEXT, display: "inline-flex", alignItems: "center" },
|
|
1362
|
+
children: actionGlyphNode(token)
|
|
1363
|
+
});
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1143
1366
|
/**
|
|
1144
|
-
*
|
|
1145
|
-
*
|
|
1146
|
-
*
|
|
1147
|
-
*
|
|
1367
|
+
* The shared anchor for the chip's two links — the branch compare link and
|
|
1368
|
+
* the PR token. Both were near-identical `<a>` elements; this is the one
|
|
1369
|
+
* copy of target, rel, cursor and handler wiring, so they cannot drift.
|
|
1370
|
+
* `underline` is the hover/focus state and `children` is the label.
|
|
1148
1371
|
*/
|
|
1149
|
-
|
|
1372
|
+
function chipAnchor(props) {
|
|
1373
|
+
return react_jsx_runtime.jsx("a", {
|
|
1374
|
+
href: props.href,
|
|
1375
|
+
target: "_blank",
|
|
1376
|
+
rel: "noopener noreferrer",
|
|
1377
|
+
"aria-label": props.ariaLabel,
|
|
1378
|
+
onPointerEnter: props.onPointerEnter,
|
|
1379
|
+
onPointerLeave: props.onPointerLeave,
|
|
1380
|
+
onFocus: props.onFocus,
|
|
1381
|
+
onBlur: props.onBlur,
|
|
1382
|
+
style: {
|
|
1383
|
+
color: "inherit",
|
|
1384
|
+
textDecoration: props.underline ? "underline" : "none",
|
|
1385
|
+
cursor: "pointer"
|
|
1386
|
+
},
|
|
1387
|
+
children: props.children
|
|
1388
|
+
});
|
|
1389
|
+
}
|
|
1150
1390
|
|
|
1151
1391
|
/**
|
|
1152
|
-
*
|
|
1153
|
-
*
|
|
1154
|
-
*
|
|
1155
|
-
*
|
|
1156
|
-
* purpose — it resets at the next boot, so a seam that is STILL absent
|
|
1157
|
-
* asks again, while one fixed by apply + restart never nags at all.
|
|
1392
|
+
* The branch name, as a compare anchor when the detail payload vouches an
|
|
1393
|
+
* http(s) URL. The sync arrows ride WITH the branch: they are the branch's
|
|
1394
|
+
* standing against its upstream, so the same hover and the same link cover
|
|
1395
|
+
* them. The ✎n count keeps its own surface.
|
|
1158
1396
|
*/
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
// re-render that no fetch will schedule, so the × bumps a counter here.
|
|
1175
|
-
const [, bumpNotice] = react.useState(0);
|
|
1176
|
-
const seamNotice = seamHint !== null && !seamNoticeDismissed ? seamHint : null;
|
|
1177
|
-
// the branch link's hover underline — the PR token's pattern: the
|
|
1178
|
-
// affordance appears on hover/focus, never at rest. Its OWN state, not
|
|
1179
|
-
// the PR token's linkHover: two links sharing one flag would
|
|
1180
|
-
// underline both when either is hovered.
|
|
1181
|
-
const [branchLinkHover, setBranchLinkHover] = react.useState(false);
|
|
1182
|
-
const detail = useGitStatus(target, { pr: true, detail: true, enabled: hovered });
|
|
1183
|
-
if (info === void 0 || info.git !== true) return null;
|
|
1397
|
+
function chipBranchInner(info, detail, isBranchLink, hoveredLink, setHovered, setHoveredLink) {
|
|
1398
|
+
const sync = formatBaseSync(info);
|
|
1399
|
+
const branchLabel = info.branch + sync;
|
|
1400
|
+
if (!isBranchLink) return branchLabel;
|
|
1401
|
+
return chipAnchor({
|
|
1402
|
+
href: detail.compareUrl,
|
|
1403
|
+
ariaLabel: "Compare " + info.branch + " with " + baseNameFor(info) + " on GitHub" + (sync === "" ? "" : ", " + (info.mainAhead || 0) + " ahead and " + (info.mainBehind || 0) + " behind " + baseNameFor(info)),
|
|
1404
|
+
underline: hoveredLink === "branch",
|
|
1405
|
+
onPointerEnter: () => setHoveredLink("branch"),
|
|
1406
|
+
onPointerLeave: () => setHoveredLink(null),
|
|
1407
|
+
onFocus: () => { setHovered(true); setHoveredLink("branch"); },
|
|
1408
|
+
onBlur: () => setHoveredLink(null),
|
|
1409
|
+
children: branchLabel
|
|
1410
|
+
});
|
|
1411
|
+
}
|
|
1184
1412
|
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1413
|
+
/**
|
|
1414
|
+
* The LEADING CLUSTER — the action glyph (when the checkout needs one) and
|
|
1415
|
+
* the branch name — wrapped in the one hover card. It is the chip card's ONLY
|
|
1416
|
+
* anchor: pointer rest here enables the detail=1 fetch.
|
|
1417
|
+
*/
|
|
1418
|
+
function chipLeadingHover(info, detail, tokenGlyph, branchInner, isBranchLink, setHovered) {
|
|
1419
|
+
return withTooltip(
|
|
1191
1420
|
react_jsx_runtime.jsx("span", {
|
|
1192
1421
|
onPointerEnter: () => setHovered(true),
|
|
1193
|
-
style: { display: "inline-flex", alignItems: "center", flex: "none", cursor: "default" },
|
|
1194
|
-
children: react_jsx_runtime.jsx(
|
|
1422
|
+
style: { display: "inline-flex", alignItems: "center", gap: "6px", flex: "none", cursor: isBranchLink ? "pointer" : "default" },
|
|
1423
|
+
children: [tokenGlyph, react_jsx_runtime.jsx("span", { key: "branch", children: branchInner })]
|
|
1195
1424
|
}),
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
() => react_jsx_runtime.jsx(HoverCard, { info, detail }),
|
|
1199
|
-
600 // matches the body cap: the tooltip must never be the clamp
|
|
1425
|
+
() => react_jsx_runtime.jsx(ChipCard, { info, detail }),
|
|
1426
|
+
600
|
|
1200
1427
|
);
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
const
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
},
|
|
1243
|
-
children: label
|
|
1244
|
-
})
|
|
1245
|
-
: label;
|
|
1246
|
-
const branch = react_jsx_runtime.jsx("span", {
|
|
1247
|
-
onPointerEnter: () => setHovered(true),
|
|
1248
|
-
style: { cursor: isLink ? "pointer" : "default" },
|
|
1249
|
-
children: inner
|
|
1250
|
-
});
|
|
1251
|
-
// ALWAYS wrapped. A wrapper mounted only once the lazy detail arrives
|
|
1252
|
-
// cannot open for the visit that fetched it (see the child-stability
|
|
1253
|
-
// note above); until the payload lands the lineage degrades to the base
|
|
1254
|
-
// response, the way the mark's own card already does.
|
|
1255
|
-
return withTooltip(branch, () => react_jsx_runtime.jsx(BranchLineage, { info: detail ?? info }), 480);
|
|
1256
|
-
})();
|
|
1257
|
-
const text = formatOperationToken(info);
|
|
1258
|
-
// The ✎n count is its OWN hover surface: a names-only tooltip (see
|
|
1259
|
-
// UntrackedList). It shares the mark's lazy detail fetch — resting on
|
|
1260
|
-
// either triggers it once — and degrades to a plain count on a shell
|
|
1261
|
-
// without the Tooltip primitive.
|
|
1262
|
-
const countHover = (() => {
|
|
1263
|
-
// ✎n only: the sync arrows moved to the branch's surface above
|
|
1264
|
-
const countText = formatFileCount(info);
|
|
1265
|
-
const count = react_jsx_runtime.jsx("span", {
|
|
1266
|
-
onPointerEnter: () => setHovered(true),
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
/**
|
|
1431
|
+
* The ✎n count as its OWN hover surface: a names-only tooltip. It shares
|
|
1432
|
+
* the mark's lazy detail fetch and degrades to a plain count without the
|
|
1433
|
+
* Tooltip primitive.
|
|
1434
|
+
*/
|
|
1435
|
+
function chipCountHover(info, detail, setHovered) {
|
|
1436
|
+
const countText = formatFileCount(info);
|
|
1437
|
+
const count = react_jsx_runtime.jsx("span", {
|
|
1438
|
+
onPointerEnter: () => setHovered(true),
|
|
1439
|
+
style: { cursor: "default" },
|
|
1440
|
+
children: countText
|
|
1441
|
+
});
|
|
1442
|
+
const pending = detail === void 0 || detail === null;
|
|
1443
|
+
return withTooltip(count, () => react_jsx_runtime.jsx(CountNames, { info: detail ?? info, pending }), 480);
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
/**
|
|
1447
|
+
* The PR token: the number, the CI state glyph and the GitHub link, and
|
|
1448
|
+
* nothing else. It has NO hover card of its own — the chip/branch card
|
|
1449
|
+
* already lists the branch's commits. ONE element, split so the STATE GLYPH
|
|
1450
|
+
* can take the CI colour while the number keeps the chip's muted colour.
|
|
1451
|
+
*/
|
|
1452
|
+
function chipPrTokenHover(info, hoveredLink, setHoveredLink) {
|
|
1453
|
+
const prParts = prTokenParts(info);
|
|
1454
|
+
const prToken = prParts === null ? null : [
|
|
1455
|
+
prParts.number,
|
|
1456
|
+
prParts.glyph === "" ? null : react_jsx_runtime.jsx("span", {
|
|
1457
|
+
key: "pr-state",
|
|
1458
|
+
style: { color: prParts.colour },
|
|
1459
|
+
children: " " + prParts.glyph
|
|
1460
|
+
}),
|
|
1461
|
+
prParts.draft
|
|
1462
|
+
];
|
|
1463
|
+
const prUrl = prLinkUrl(info);
|
|
1464
|
+
if (prUrl === void 0) {
|
|
1465
|
+
// plain token: no hover, so it wires no pointer handlers
|
|
1466
|
+
return react_jsx_runtime.jsx("span", {
|
|
1467
|
+
// the glyph is not the only channel: the token says what the CI state IS
|
|
1468
|
+
"aria-label": prTokenLabel(info),
|
|
1267
1469
|
style: { cursor: "default" },
|
|
1268
|
-
children:
|
|
1470
|
+
children: prToken
|
|
1269
1471
|
});
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
// open at all — the data it renders is detail-only.
|
|
1287
|
-
const prTokenHover = (() => {
|
|
1288
|
-
const token = prUrl === void 0
|
|
1289
|
-
? react_jsx_runtime.jsx("span", {
|
|
1290
|
-
// the glyph is not the only channel: the token says what the
|
|
1291
|
-
// CI state IS, for anyone who cannot see it
|
|
1292
|
-
"aria-label": prTokenLabel(info),
|
|
1293
|
-
onPointerEnter: () => setHovered(true),
|
|
1294
|
-
style: { cursor: "default" },
|
|
1295
|
-
children: prToken
|
|
1296
|
-
})
|
|
1297
|
-
: react_jsx_runtime.jsx("a", {
|
|
1298
|
-
href: prUrl,
|
|
1299
|
-
// a new tab, because navigating THIS one away from the app
|
|
1300
|
-
// would lose the conversation; noopener/noreferrer keep the
|
|
1301
|
-
// opened tab from getting a handle on it
|
|
1302
|
-
target: "_blank",
|
|
1303
|
-
rel: "noopener noreferrer",
|
|
1304
|
-
"aria-label": prTokenLabel(info) + ", opens on GitHub in a new tab",
|
|
1305
|
-
// the chip's own colour rather than the browser's link blue /
|
|
1306
|
-
// visited purple, so the token still reads as one row. The
|
|
1307
|
-
// affordance is the pointer cursor plus an underline on hover
|
|
1308
|
-
// or focus, set HERE rather than left to the host stylesheet:
|
|
1309
|
-
// a shell that resets anchors would otherwise drop it silently.
|
|
1310
|
-
style: {
|
|
1311
|
-
color: "inherit",
|
|
1312
|
-
textDecoration: linkHover ? "underline" : "none",
|
|
1313
|
-
cursor: "pointer"
|
|
1314
|
-
},
|
|
1315
|
-
// focus joins hover — the token is keyboard-reachable now, and a
|
|
1316
|
-
// keyboard user needs the same "this is a link" signal
|
|
1317
|
-
onPointerEnter: () => { setHovered(true); setLinkHover(true); },
|
|
1318
|
-
onPointerLeave: () => setLinkHover(false),
|
|
1319
|
-
onFocus: () => { setHovered(true); setLinkHover(true); },
|
|
1320
|
-
onBlur: () => setLinkHover(false),
|
|
1321
|
-
children: prToken
|
|
1322
|
-
});
|
|
1323
|
-
// always wrapped (see the branch): while the commits are in flight the
|
|
1324
|
-
// label names the PR state the base response already carries
|
|
1325
|
-
const pending = detail === void 0 || detail === null;
|
|
1326
|
-
return withTooltip(token, () => react_jsx_runtime.jsx(PrCommitsList, { info: detail ?? info, pending }), 480);
|
|
1327
|
-
})();
|
|
1328
|
-
const chip = react_jsx_runtime.jsxs("span", {
|
|
1472
|
+
}
|
|
1473
|
+
return chipAnchor({
|
|
1474
|
+
href: prUrl,
|
|
1475
|
+
ariaLabel: prTokenLabel(info) + ", opens on GitHub in a new tab",
|
|
1476
|
+
underline: hoveredLink === "pr",
|
|
1477
|
+
onPointerEnter: () => setHoveredLink("pr"),
|
|
1478
|
+
onPointerLeave: () => setHoveredLink(null),
|
|
1479
|
+
onFocus: () => setHoveredLink("pr"),
|
|
1480
|
+
onBlur: () => setHoveredLink(null),
|
|
1481
|
+
children: prToken
|
|
1482
|
+
});
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
/** The chip row itself: leading cluster, operation text, count and PR token. */
|
|
1486
|
+
function chipAssemble(leadingHover, text, countHover, prTokenHover) {
|
|
1487
|
+
return react_jsx_runtime.jsxs("span", {
|
|
1329
1488
|
style: {
|
|
1330
1489
|
display: "inline-flex",
|
|
1331
1490
|
alignItems: "center",
|
|
1332
1491
|
flex: "none",
|
|
1333
|
-
// 6px, not 4px: the PR token leads with a space, and CSS drops
|
|
1334
|
-
//
|
|
1335
|
-
//
|
|
1492
|
+
// 6px, not 4px: the PR token leads with a space, and CSS drops leading
|
|
1493
|
+
// whitespace at the start of a flex item, so 4px read as no separation
|
|
1494
|
+
// at all between the branch and `PR#…`
|
|
1336
1495
|
gap: "6px",
|
|
1337
1496
|
color: "var(--dsw-alias-label-secondary, #5b6570)",
|
|
1338
1497
|
fontSize: "12px",
|
|
@@ -1340,92 +1499,57 @@ window.__ModuleLoader__.load({
|
|
|
1340
1499
|
whiteSpace: "nowrap",
|
|
1341
1500
|
cursor: "default"
|
|
1342
1501
|
},
|
|
1343
|
-
// the long-form explanation, on hover; the inline seam-notice below is
|
|
1344
|
-
// the visible channel (its text is the short form, its title this).
|
|
1345
|
-
// null until the delayed check in apply() proves the seam never
|
|
1346
|
-
// appeared.
|
|
1347
|
-
title: seamHint === null ? undefined : seamHint,
|
|
1348
1502
|
children: [
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
// opening sideways from a hover on the branch text got in the way of
|
|
1352
|
-
// the very pull-down this row now carries. The detail fetch
|
|
1353
|
-
// (enabled: hovered) follows the same pointer, so nothing pays for
|
|
1354
|
-
// extras the card no longer shows. The seam-absent native title
|
|
1355
|
-
// stays on the whole chip — it is about the badge, not the mark.
|
|
1356
|
-
hoverableMark,
|
|
1357
|
-
|
|
1358
|
-
branchHover,
|
|
1359
|
-
react_jsx_runtime.jsx("span", { key: "text", children: text }),
|
|
1503
|
+
leadingHover,
|
|
1504
|
+
react_jsx_runtime.jsx("span", { key: "text", children: text }),
|
|
1360
1505
|
countHover,
|
|
1361
|
-
prTokenHover
|
|
1362
|
-
/**
|
|
1363
|
-
* The seam-absent notice: the market-install notification for the
|
|
1364
|
-
* state a blocked postinstall leaves behind — chip working, sidebar
|
|
1365
|
-
* rows structurally off, nothing patched, nothing to restart INTO.
|
|
1366
|
-
* It is only ever TRUE (seamHint is set 5s after boot, only after
|
|
1367
|
-
* the composed graph is given its chance), and it is self-resolving:
|
|
1368
|
-
* apply + restart makes the seam declare, and this never renders
|
|
1369
|
-
* again. That is what distinguishes it from the old restart notice,
|
|
1370
|
-
* which outlived the restart it asked for. Dismissal is a session
|
|
1371
|
-
* value (see seamNoticeDismissed), so the × means "stop asking this
|
|
1372
|
-
* boot", never "stop asking forever".
|
|
1373
|
-
*/
|
|
1374
|
-
seamNotice === null
|
|
1375
|
-
? null
|
|
1376
|
-
: react_jsx_runtime.jsxs(
|
|
1377
|
-
"span",
|
|
1378
|
-
{
|
|
1379
|
-
style: {
|
|
1380
|
-
display: "inline-flex",
|
|
1381
|
-
alignItems: "center",
|
|
1382
|
-
gap: "6px",
|
|
1383
|
-
paddingLeft: "6px",
|
|
1384
|
-
borderLeft: "1px solid var(--dsw-alias-border-secondary, #d0d7de)",
|
|
1385
|
-
whiteSpace: "normal",
|
|
1386
|
-
maxWidth: "320px"
|
|
1387
|
-
},
|
|
1388
|
-
children: [
|
|
1389
|
-
react_jsx_runtime.jsx(
|
|
1390
|
-
"span",
|
|
1391
|
-
{
|
|
1392
|
-
title: seamHint,
|
|
1393
|
-
children: "sidebar badges off — run `npx dsh-git-badge apply`"
|
|
1394
|
-
},
|
|
1395
|
-
"explain"
|
|
1396
|
-
),
|
|
1397
|
-
react_jsx_runtime.jsx(
|
|
1398
|
-
"button",
|
|
1399
|
-
{
|
|
1400
|
-
"aria-label": "Dismiss the sidebar-badges notice (returns next boot while the seam is absent)",
|
|
1401
|
-
onClick: () => {
|
|
1402
|
-
seamNoticeDismissed = true;
|
|
1403
|
-
bumpNotice((n) => n + 1);
|
|
1404
|
-
},
|
|
1405
|
-
style: {
|
|
1406
|
-
cursor: "pointer",
|
|
1407
|
-
color: "inherit",
|
|
1408
|
-
background: "none",
|
|
1409
|
-
border: "1px solid var(--dsw-alias-border-secondary, #d0d7de)",
|
|
1410
|
-
borderRadius: "4px",
|
|
1411
|
-
fontSize: "11px",
|
|
1412
|
-
lineHeight: "18px",
|
|
1413
|
-
padding: "0 6px",
|
|
1414
|
-
flex: "none"
|
|
1415
|
-
},
|
|
1416
|
-
children: "×"
|
|
1417
|
-
},
|
|
1418
|
-
"dismiss"
|
|
1419
|
-
)
|
|
1420
|
-
]
|
|
1421
|
-
},
|
|
1422
|
-
"seam-notice"
|
|
1423
|
-
),
|
|
1506
|
+
prTokenHover
|
|
1424
1507
|
]
|
|
1425
1508
|
});
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
/**
|
|
1512
|
+
* Chip line in the input row: git state of the workspace the CURRENT
|
|
1513
|
+
* conversation is attached to. It targets the session id and lets the node
|
|
1514
|
+
* half resolve the workspace, so this surface needs no `workspaces`
|
|
1515
|
+
* service, no cwd and no path plumbing at all. `conversation.input.left`
|
|
1516
|
+
* is an upstream additive slot, so it works on every install, and it stays
|
|
1517
|
+
* useful next to the session row because it is context-anchored ("where am
|
|
1518
|
+
* I") rather than surveying.
|
|
1519
|
+
*
|
|
1520
|
+
* This is the ONLY surface that asks the node half for the extras (`pr=1`
|
|
1521
|
+
* always, `detail=1` on hover), because it is the only one that renders
|
|
1522
|
+
* them — see targetQuery.
|
|
1523
|
+
*/
|
|
1524
|
+
function ComposerGitChip({ sessionId }) {
|
|
1525
|
+
const target = sessionId === void 0 ? void 0 : { kind: "session", id: sessionId };
|
|
1526
|
+
// the PR/CI token is always on the chip, so its fetch is not gated
|
|
1527
|
+
const info = useGitStatus(target, { pr: true });
|
|
1528
|
+
// The card's extras cost a `log -10` plus a stash list, so they are fetched
|
|
1529
|
+
// only once a pointer actually RESTS on the chip. A refresh fires on every
|
|
1530
|
+
// file edit, so eagerly asking would add both invocations to every refresh.
|
|
1531
|
+
const [hovered, setHovered] = react.useState(false);
|
|
1532
|
+
// Which of the chip's two links is hovered/focused — ONE enum, not two
|
|
1533
|
+
// booleans: "branch" | "pr" | null. The anchor underlines only when its
|
|
1534
|
+
// own value matches, so hovering one link never underlines the other.
|
|
1535
|
+
const [hoveredLink, setHoveredLink] = react.useState(null);
|
|
1536
|
+
const detail = useGitStatus(target, { pr: true, detail: true, enabled: hovered });
|
|
1537
|
+
if (info === void 0 || info.git !== true) return null;
|
|
1538
|
+
|
|
1539
|
+
const tokenGlyph = chipGlyphNode(info);
|
|
1540
|
+
// The branch name links to the compare view (base...branch). Vouched http(s)
|
|
1541
|
+
// only; new tab, never navigating the conversation away.
|
|
1542
|
+
const isBranchLink = typeof detail?.compareUrl === "string" && detail.compareUrl.startsWith("https://");
|
|
1543
|
+
const branchInner = chipBranchInner(info, detail, isBranchLink, hoveredLink, setHovered, setHoveredLink);
|
|
1544
|
+
// The Tooltip's CHILD must not change element type once the detail payload
|
|
1545
|
+
// lands, so the cluster span is the stable hover surface. ALWAYS wrapped.
|
|
1546
|
+
const leadingHover = chipLeadingHover(info, detail, tokenGlyph, branchInner, isBranchLink, setHovered);
|
|
1547
|
+
const text = formatOperationToken(info);
|
|
1548
|
+
const countHover = chipCountHover(info, detail, setHovered);
|
|
1549
|
+
const prTokenHover = chipPrTokenHover(info, hoveredLink, setHoveredLink);
|
|
1550
|
+
const chip = chipAssemble(leadingHover, text, countHover, prTokenHover);
|
|
1551
|
+
// No Tooltip primitive (a shell that does not seed it): the leading cluster
|
|
1552
|
+
// renders bare and the badge still works — the card is an enhancement.
|
|
1429
1553
|
return chip;
|
|
1430
1554
|
}
|
|
1431
1555
|
|
|
@@ -1435,7 +1559,7 @@ window.__ModuleLoader__.load({
|
|
|
1435
1559
|
// node half resolves the workspace, so the client never needs a service
|
|
1436
1560
|
// lookup. Fewer declared services also means fewer ways to fail to load.
|
|
1437
1561
|
/** The built-in ranking order — what "Reset to default" restores. */
|
|
1438
|
-
const DEFAULT_ORDER = ["operation", "unmerged", "sync", "publish", "merge", "commit", "checks"];
|
|
1562
|
+
const DEFAULT_ORDER = ["operation", "unmerged", "conflict", "review", "sync", "publish", "merge", "commit", "checks"];
|
|
1439
1563
|
|
|
1440
1564
|
/**
|
|
1441
1565
|
* Move one category within the ranking order — the card's only reorder
|
|
@@ -1455,6 +1579,8 @@ window.__ModuleLoader__.load({
|
|
|
1455
1579
|
const CATEGORY_LABELS = {
|
|
1456
1580
|
operation: ["operation", "resume a paused merge/rebase"],
|
|
1457
1581
|
unmerged: ["unmerged", "resolve conflict markers"],
|
|
1582
|
+
conflict: ["conflict", "a pull request that conflicts with its base"],
|
|
1583
|
+
review: ["review", "a review to address, or a review to wait for"],
|
|
1458
1584
|
sync: ["sync", "pull, or rebase + confirmed force-push"],
|
|
1459
1585
|
publish: ["publish", "push local commits"],
|
|
1460
1586
|
merge: ["merge", "a merge-ready pull request"],
|
|
@@ -1590,12 +1716,12 @@ window.__ModuleLoader__.load({
|
|
|
1590
1716
|
});
|
|
1591
1717
|
}
|
|
1592
1718
|
|
|
1593
|
-
const inject = ["slots"
|
|
1719
|
+
const inject = ["slots"];
|
|
1594
1720
|
|
|
1595
1721
|
/**
|
|
1596
|
-
* Register the badge into
|
|
1597
|
-
* owner share as props; the row
|
|
1598
|
-
* deliberately
|
|
1722
|
+
* Register the badge into upstream's own seats. The slot owner hands each
|
|
1723
|
+
* entry the row owner share as props; the row surfaces destructure
|
|
1724
|
+
* `{ sessionId }` and deliberately ignore the cwd they may also be given.
|
|
1599
1725
|
*/
|
|
1600
1726
|
function apply(ctx) {
|
|
1601
1727
|
// The /gh command contribution (the (+) commands menu): the state-aware
|
|
@@ -1810,33 +1936,31 @@ window.__ModuleLoader__.load({
|
|
|
1810
1936
|
}
|
|
1811
1937
|
}
|
|
1812
1938
|
|
|
1813
|
-
//
|
|
1814
|
-
//
|
|
1815
|
-
//
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
//
|
|
1821
|
-
//
|
|
1822
|
-
//
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
// The row is ALREADY a HoverCard anchor upstream, so a
|
|
1835
|
-
// its own tooltip would nest two cards. This renders the
|
|
1939
|
+
// The row's token rides upstream's OWN `sidebar.session.row.leading`
|
|
1940
|
+
// seat: a 16px cell the workspace browser renders in the row's leading
|
|
1941
|
+
// cluster. No patch and no declaration to make.
|
|
1942
|
+
ctx.slots.inject("sidebar.session.row.leading", () => ctx.slots.register({
|
|
1943
|
+
name: "sidebar.session.row.leading",
|
|
1944
|
+
id: "git-badge-row-leading"
|
|
1945
|
+
}, LeadingTokenGlyph));
|
|
1946
|
+
// The seat above is mounted ONLY while the row's primary state is idle
|
|
1947
|
+
// (upstream's own contract). A row that owns a solid status dot — an
|
|
1948
|
+
// unread completion, or a pending interaction — keeps that cell to
|
|
1949
|
+
// itself, so the token glyph would be invisible exactly when the turn
|
|
1950
|
+
// that produced it just ended, until the session is opened. This strip is
|
|
1951
|
+
// ALWAYS mounted (hidden by CSS, not unmounted), so a marker rides it and
|
|
1952
|
+
// the stylesheet paints the token over the dot. The rule stays honest: no
|
|
1953
|
+
// upstream byte changes, and the cell still belongs to upstream.
|
|
1954
|
+
ctx.slots.inject("sidebar.workspaces.session.row.action", () => ctx.slots.register({
|
|
1955
|
+
name: "sidebar.workspaces.session.row.action",
|
|
1956
|
+
id: "git-badge-row-fallback",
|
|
1957
|
+
order: 300
|
|
1958
|
+
}, RowFallbackGlyph));
|
|
1959
|
+
ensureRowFallbackStyle();
|
|
1960
|
+
// The row is ALREADY a HoverCard anchor upstream, so a glyph that opened
|
|
1961
|
+
// its own tooltip would nest two cards. This renders the explanation
|
|
1836
1962
|
// inside that card instead, through the hover seat upstream declares
|
|
1837
|
-
// itself (sidebar.session.row.hover) — no
|
|
1838
|
-
// its own. Upstream 0.2.0-rc.2 threads renderSlot into the card and adds
|
|
1839
|
-
// the seat; before that this needed a patched sessionRow.detail seam.
|
|
1963
|
+
// itself (sidebar.session.row.hover) — no request of its own.
|
|
1840
1964
|
ctx.slots.inject("sidebar.session.row.hover", () => ctx.slots.register({
|
|
1841
1965
|
name: "sidebar.session.row.hover",
|
|
1842
1966
|
id: "git-badge-row-detail"
|
|
@@ -1852,29 +1976,9 @@ window.__ModuleLoader__.load({
|
|
|
1852
1976
|
id: "git-badge-chip",
|
|
1853
1977
|
inject: (sessionId) => ({ sessionId })
|
|
1854
1978
|
}, ComposerGitChip));
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
(seamDeclared
|
|
1859
|
-
? "on (seam present)."
|
|
1860
|
-
: // discoverability: the chip works everywhere, but the session rows
|
|
1861
|
-
// need the seam patch, and on a market install nothing else says so.
|
|
1862
|
-
// The patcher ships IN this package — no clone needed.
|
|
1863
|
-
"off (seam absent) — run `npx dsh-git-badge apply` to add them, then restart dsh web.")
|
|
1864
|
-
);
|
|
1865
|
-
// The chip tooltip can only be set LATE. At apply() time the workspace
|
|
1866
|
-
// browser may not have declared the seam yet (the boot race documented
|
|
1867
|
-
// above), so claiming absence here would lie to patched installs for the
|
|
1868
|
-
// whole session — the exact mistake that made the old one-shot line
|
|
1869
|
-
// claim "sidebar rows = off" while five row badges were rendering. Give
|
|
1870
|
-
// the composed graph a beat to settle; only a seam that is STILL absent
|
|
1871
|
-
// earns the hint, and every chip render after that carries it.
|
|
1872
|
-
setTimeout(() => {
|
|
1873
|
-
if (rowsReported) return; // seam present; badges rendered — nothing to hint about
|
|
1874
|
-
if (ctx.slots.spec("sidebar.workspaces.sessionRow") !== void 0) return;
|
|
1875
|
-
seamHint =
|
|
1876
|
-
"Session-row badges are off: run `npx dsh-git-badge apply`, then restart dsh web. (Input-chip badges are unaffected.)";
|
|
1877
|
-
}, 5000);
|
|
1979
|
+
// No seam, no apply step: the leading seat and the hover seat are both
|
|
1980
|
+
// upstream's own. Report the surfaces once, for diagnostics.
|
|
1981
|
+
console.info("[dsh-git-badge] surfaces: input chip = on; sidebar row token = on (upstream leading seat).");
|
|
1878
1982
|
// The hover card depends on a primitive the SHELL seeds, not on anything
|
|
1879
1983
|
// this plugin declares. Report the outcome rather than letting a missing
|
|
1880
1984
|
// seed look like a missing feature: the try/catch above deliberately keeps
|
|
@@ -1893,7 +1997,7 @@ window.__ModuleLoader__.load({
|
|
|
1893
1997
|
// Additive; the host reads apply/inject and ignores the rest. The suite
|
|
1894
1998
|
// drives these to assert the REQUEST contract — which surface asks for the
|
|
1895
1999
|
// expensive extras — without a browser, a fetch or a network.
|
|
1896
|
-
exports.__internals = { targetQuery,
|
|
2000
|
+
exports.__internals = { targetQuery, worktreeDetail, linkedWorktreesDetail, ghSkillActions, cleanGate, moveCategory, fallbackGlyph, glyphMaskDataUri, syncRowFallback, clearFallbackMark, ROW_FALLBACK_CSS, ROW_FALLBACK_DOT };
|
|
1897
2001
|
return module.exports;
|
|
1898
2002
|
}
|
|
1899
2003
|
});
|