dsh-git-badge 0.19.0 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +133 -0
- package/README.md +15 -7
- package/lib/client.js +608 -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,153 @@ window.__ModuleLoader__.load({
|
|
|
626
562
|
}
|
|
627
563
|
|
|
628
564
|
/**
|
|
629
|
-
* The
|
|
630
|
-
*
|
|
631
|
-
*
|
|
632
|
-
*
|
|
633
|
-
*
|
|
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.
|
|
567
|
+
*
|
|
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.
|
|
634
573
|
*
|
|
635
|
-
*
|
|
636
|
-
*
|
|
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
|
-
// glyph and weight, and only the tint is lost.
|
|
654
|
-
style.background = `color-mix(in srgb, ${colour} 18%, transparent)`;
|
|
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
|
+
style: { ...META_STYLE, ...(colour === void 0 ? {} : { color: colour }), ...GLYPH_TEXT, display: "inline-flex", alignItems: "center" },
|
|
599
|
+
children: actionGlyphNode(token)
|
|
600
|
+
});
|
|
655
601
|
}
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
602
|
+
return null;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
const CARD_CONTAINER = {
|
|
606
|
+
display: "flex",
|
|
607
|
+
flexDirection: "column",
|
|
608
|
+
gap: "4px",
|
|
609
|
+
fontSize: "12px",
|
|
610
|
+
lineHeight: "18px",
|
|
611
|
+
// Auto-width: the card fits its widest row instead of a guessed
|
|
612
|
+
// constant, capped at 600px. The Tooltip's maxWidth must stay >= the
|
|
613
|
+
// cap or IT becomes the clamp.
|
|
614
|
+
width: "max-content",
|
|
615
|
+
maxWidth: "600px"
|
|
616
|
+
};
|
|
617
|
+
const CARD_ROW = { display: "flex", gap: "8px", alignItems: "baseline" };
|
|
618
|
+
const CARD_LABEL = { color: "var(--dsw-alias-label-tertiary, #9ea7ad)", flex: "none", minWidth: "62px" };
|
|
619
|
+
const CARD_VALUE = { minWidth: 0, overflowWrap: "anywhere", whiteSpace: "normal" };
|
|
620
|
+
// The chip card's HEADER: the action sentence as a heading, not a labelled
|
|
621
|
+
// row. `fontWeight` is also the hook the suite uses to find it.
|
|
622
|
+
const CHIP_HEADER = { display: "flex", alignItems: "baseline", gap: "6px", fontWeight: 600 };
|
|
623
|
+
// A section rule between the checkout rows and the branch rows. The label
|
|
624
|
+
// makes the split explicit instead of leaving `state`/`base` to look like
|
|
625
|
+
// more checkout detail.
|
|
626
|
+
const CARD_DIVIDER = {
|
|
627
|
+
marginTop: "4px",
|
|
628
|
+
paddingTop: "6px",
|
|
629
|
+
borderTop: ".5px solid var(--dsw-alias-border-l4, #e4e8eb)",
|
|
630
|
+
color: "var(--dsw-alias-label-tertiary, #9ea7ad)",
|
|
631
|
+
fontSize: "10px",
|
|
632
|
+
lineHeight: "14px",
|
|
633
|
+
letterSpacing: ".08em",
|
|
634
|
+
textTransform: "uppercase"
|
|
635
|
+
};
|
|
636
|
+
|
|
637
|
+
/** One label/value row — the shape every hover body shares. */
|
|
638
|
+
function cardRow(label, value) {
|
|
639
|
+
return react_jsx_runtime.jsxs("div", {
|
|
640
|
+
style: CARD_ROW,
|
|
641
|
+
children: [
|
|
642
|
+
react_jsx_runtime.jsx("span", { style: CARD_LABEL, children: label }),
|
|
643
|
+
react_jsx_runtime.jsx("span", { style: CARD_VALUE, children: value })
|
|
644
|
+
]
|
|
645
|
+
}, label);
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
/** Push one label/value row unless the value is empty. */
|
|
649
|
+
function addRow(rows, label, value) {
|
|
650
|
+
if (value === void 0 || value === null || value === "") return;
|
|
651
|
+
rows.push(cardRow(label, value));
|
|
667
652
|
}
|
|
668
653
|
|
|
669
654
|
/**
|
|
670
|
-
*
|
|
671
|
-
*
|
|
672
|
-
*
|
|
673
|
-
*
|
|
655
|
+
* The action SENTENCE as a raw value. SHARED by the session-row card
|
|
656
|
+
* (actionRow wraps it in the card label/value shape) and the chip card
|
|
657
|
+
* (actionHeader wraps it in a heading), so the two cannot describe the icon
|
|
658
|
+
* differently. Returns undefined when the checkout has no token.
|
|
674
659
|
*
|
|
675
|
-
*
|
|
676
|
-
*
|
|
677
|
-
|
|
660
|
+
* The glyph is DECORATION here: the word follows it, so `aria-hidden` keeps
|
|
661
|
+
* the accessible name the bare action.
|
|
662
|
+
*/
|
|
663
|
+
function actionRow(info) {
|
|
664
|
+
const token = info?.rowToken;
|
|
665
|
+
if (typeof token !== "string" || token === "") return void 0;
|
|
666
|
+
const why = typeof info.rowTokenWhy === "string" && info.rowTokenWhy !== "" ? info.rowTokenWhy : void 0;
|
|
667
|
+
const colour = SEVERITY_FILL[info.rowSeverity];
|
|
668
|
+
const value = [];
|
|
669
|
+
if (DRAWN_ICONS[token] !== void 0 || ACTION_GLYPH[token] !== void 0) {
|
|
670
|
+
value.push(react_jsx_runtime.jsx("span", {
|
|
671
|
+
"aria-hidden": true,
|
|
672
|
+
style: { color: colour === void 0 ? META_STYLE.color : colour, fontWeight: 700, display: "inline-flex", alignItems: "center" },
|
|
673
|
+
children: actionGlyphNode(token)
|
|
674
|
+
}, "action-glyph"));
|
|
675
|
+
value.push(" ");
|
|
676
|
+
}
|
|
677
|
+
value.push(token + (why === void 0 ? "" : " \u2014 " + why));
|
|
678
|
+
return value;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
/**
|
|
682
|
+
* The chip card's HEADER: the same sentence actionRow produces, rendered as a
|
|
683
|
+
* heading so the checkout's verdict leads the card instead of sitting in a
|
|
684
|
+
* labelled row. It calls actionRow, so the two surfaces cannot drift.
|
|
685
|
+
*/
|
|
686
|
+
function actionHeader(info) {
|
|
687
|
+
const value = actionRow(info);
|
|
688
|
+
if (value === void 0) return void 0;
|
|
689
|
+
return react_jsx_runtime.jsx("div", { style: CHIP_HEADER, children: value }, "action-header");
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
/**
|
|
693
|
+
* Session-row hover card — everything the action glyph cannot say: what the
|
|
694
|
+
* action means and why, which checkout the row's status describes, and which
|
|
695
|
+
* pull request it refers to. It renders through the upstream
|
|
696
|
+
* sidebar.session.row.hover seat, inside the card upstream already opens for
|
|
697
|
+
* a row.
|
|
698
|
+
*
|
|
699
|
+
* LABEL/VALUE ROWS, the shape the chip card uses. The `action` row comes from
|
|
700
|
+
* the shared actionRow() helper, so the two cards cannot describe the icon
|
|
701
|
+
* differently; `checkout` and `pull request` follow.
|
|
678
702
|
*
|
|
679
|
-
*
|
|
680
|
-
*
|
|
681
|
-
*
|
|
682
|
-
*
|
|
703
|
+
* This matters MORE now that the row shows only a glyph: `merge` with no way
|
|
704
|
+
* to see merge WHAT would be a nag rather than a hint, so the card renders
|
|
705
|
+
* whenever there is something to name — the row's action, a worktree, a pull
|
|
706
|
+
* request, or any combination.
|
|
707
|
+
*
|
|
708
|
+
* It makes no request of its own: the same `{ pr: true }` query as the
|
|
709
|
+
* leading glyph resolves to the same cache key, so a card that mounts for
|
|
710
|
+
* every row still costs one fetch per row, and a cold cache is served by
|
|
711
|
+
* the glyph's own in-flight request.
|
|
683
712
|
*
|
|
684
713
|
* Only a WORKTREE's checkout is spelled out. For a main checkout the branch is
|
|
685
714
|
* the ordinary case and repeating it on every session's card would be noise;
|
|
@@ -688,18 +717,35 @@ window.__ModuleLoader__.load({
|
|
|
688
717
|
function SessionGitDetail({ sessionId }) {
|
|
689
718
|
const info = useGitStatus(sessionId === void 0 ? void 0 : { kind: "session", id: sessionId }, { pr: true });
|
|
690
719
|
if (info === void 0 || info.git !== true) return null;
|
|
691
|
-
const
|
|
720
|
+
const rows = [];
|
|
721
|
+
const add = (label, value) => addRow(rows, label, value);
|
|
722
|
+
// The HEADER, the shape the chip card leads with: the same sentence from
|
|
723
|
+
// the same actionRow, so the two cards cannot describe the icon
|
|
724
|
+
// differently, and the sentence gains the 62px label column's width.
|
|
725
|
+
const header = actionHeader(info);
|
|
726
|
+
if (header !== void 0) rows.push(header);
|
|
692
727
|
if (info.isWorktree === true) {
|
|
728
|
+
// WHICH tree, not just that there is one: the row's glyph cannot say
|
|
729
|
+
// it, and the branch is ordinary for a main checkout.
|
|
693
730
|
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 "));
|
|
731
|
+
add("checkout", name === "" ? String(info.branch) : name + " on " + info.branch);
|
|
699
732
|
}
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
733
|
+
// The dirty breakdown, so a higher-ranked token cannot hide uncommitted
|
|
734
|
+
// work. formatFileBreakdown says "clean" when there is none; a clean row
|
|
735
|
+
// keeps its short card.
|
|
736
|
+
const changes = formatFileBreakdown(info);
|
|
737
|
+
if (changes !== "clean") add("changes", changes);
|
|
738
|
+
add("pull request", formatPrDetail(info.pr));
|
|
739
|
+
if (rows.length === 0) return null;
|
|
740
|
+
// The row's hover card is upstream's FIXED-WIDTH box: HoverCard.module.css
|
|
741
|
+
// pins `.card` to 244px. `width: max-content` (CARD_CONTAINER) would
|
|
742
|
+
// overflow it, so the stack FILLS the box instead and the value cells wrap
|
|
743
|
+
// inside it. The chip card is different — its Tooltip sizes to the content,
|
|
744
|
+
// so it keeps max-content.
|
|
745
|
+
return react_jsx_runtime.jsx("div", {
|
|
746
|
+
style: { ...CARD_CONTAINER, width: "100%", maxWidth: "100%" },
|
|
747
|
+
children: rows
|
|
748
|
+
});
|
|
703
749
|
}
|
|
704
750
|
|
|
705
751
|
/**
|
|
@@ -722,7 +768,7 @@ window.__ModuleLoader__.load({
|
|
|
722
768
|
/** The PR's full state as text, for the card: "#142 · checks failing · draft · review approved". */
|
|
723
769
|
function formatPrDetail(pr) {
|
|
724
770
|
if (pr === void 0 || pr === null || pr.number === void 0) return void 0;
|
|
725
|
-
const state =
|
|
771
|
+
const state = prStateLabel(pr);
|
|
726
772
|
const bits = ["#" + pr.number, state];
|
|
727
773
|
if (pr.draft === true) bits.push("draft");
|
|
728
774
|
if (pr.review !== void 0) bits.push(String(pr.review).toLowerCase().replace(/_/g, " "));
|
|
@@ -810,50 +856,6 @@ window.__ModuleLoader__.load({
|
|
|
810
856
|
});
|
|
811
857
|
}
|
|
812
858
|
|
|
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
859
|
/**
|
|
858
860
|
* The ✎n count's own hover body — the changed-file lists and NOTHING
|
|
859
861
|
* else: the full card stays on the status mark, and this tooltip answers
|
|
@@ -925,35 +927,31 @@ window.__ModuleLoader__.load({
|
|
|
925
927
|
}
|
|
926
928
|
|
|
927
929
|
/**
|
|
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.
|
|
930
|
+
* The branch LINEAGE rows, for the chip's one card. The state row always
|
|
931
|
+
* renders (the pull-request verdict, or the base-branch note); the base row
|
|
932
|
+
* follows when the counts are known; then the branch's last 10 commits
|
|
933
|
+
* (WHAT THIS BRANCH HAS, full stop — no base-relative filtering, which
|
|
934
|
+
* empties on every push or merge). The pull request has no row here — the
|
|
935
|
+
* chip's own PR token already carries number, CI state and the link.
|
|
934
936
|
*/
|
|
935
|
-
function
|
|
937
|
+
function lineageRows(info) {
|
|
936
938
|
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.
|
|
939
|
+
const add = (label, value) => addRow(rows, label, value);
|
|
940
|
+
// The STATE row always renders. The old upstream and sync rows spoke
|
|
941
|
+
// about the push axis (the branch's own remote), which the state row
|
|
942
|
+
// subsumes: the merge verdict is the fact that matters. The base counts
|
|
943
|
+
// stay a SEPARATE row below, so a reader never reconstructs either.
|
|
946
944
|
const pr = info?.pr;
|
|
947
|
-
const
|
|
945
|
+
const stateParts = [];
|
|
948
946
|
if (pr !== void 0 && pr !== null && pr.number !== void 0) {
|
|
949
|
-
if (pr.draft === true)
|
|
947
|
+
if (pr.draft === true) stateParts.push("blocked: draft");
|
|
950
948
|
else if (pr.mergeState === "BLOCKED") {
|
|
951
|
-
|
|
949
|
+
stateParts.push(pr.review === "REVIEW_REQUIRED" ? "blocked: review required"
|
|
952
950
|
: pr.state === "failing" ? "blocked: checks failing"
|
|
953
951
|
: "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")
|
|
952
|
+
} else if (pr.state === "failing") stateParts.push("blocked: checks failing");
|
|
953
|
+
else if (pr.mergeState === "CLEAN" || (pr.state === "passing" && pr.review === "APPROVED")) stateParts.push("ready to merge");
|
|
954
|
+
else if (pr.review === "REVIEW_REQUIRED") stateParts.push("blocked: review required");
|
|
957
955
|
} else if (info?.defaultBranch === true) {
|
|
958
956
|
// The default branch is the BASE, not a branch with a missing PR:
|
|
959
957
|
// pull requests merge INTO it and none is ever opened FROM it, so
|
|
@@ -961,18 +959,21 @@ window.__ModuleLoader__.load({
|
|
|
961
959
|
// exist. The node half computes `defaultBranch` for exactly this
|
|
962
960
|
// suppression (see the comment where it is set in lib/index.js);
|
|
963
961
|
// until now only the action rules consulted it, not this row.
|
|
964
|
-
|
|
962
|
+
stateParts.push("base branch \u00B7 pull requests merge into this");
|
|
965
963
|
} else {
|
|
966
|
-
|
|
964
|
+
stateParts.push("no pull request");
|
|
967
965
|
}
|
|
968
|
-
// The
|
|
969
|
-
|
|
970
|
-
//
|
|
971
|
-
//
|
|
966
|
+
// The state row states the PR verdict and nothing else.
|
|
967
|
+
add("state", stateParts.join(" \u00B7 "));
|
|
968
|
+
// The counts are this branch's standing against its BASE, so on the base
|
|
969
|
+
// itself they are self-referential ("N ahead, M behind main" while ON
|
|
970
|
+
// main) and the divergence they would state is already the chip's own
|
|
971
|
+
// \u2191a \u2193b token. Suppress them on the default branch, with or without a
|
|
972
|
+
// PR. A SEPARATE row from state: whether the PR can merge and how far the
|
|
973
|
+
// branch is from its base are two facts, so neither hides the other.
|
|
972
974
|
if (info?.defaultBranch !== true && (info?.mainAhead !== void 0 || info?.mainBehind !== void 0)) {
|
|
973
|
-
|
|
975
|
+
add("base", (info.mainAhead || 0) + " ahead, " + (info.mainBehind || 0) + " behind " + baseNameFor(info));
|
|
974
976
|
}
|
|
975
|
-
add("merge", mergeParts.join(" \u00B7 "));
|
|
976
977
|
const commits = info?.branchCommits;
|
|
977
978
|
if (Array.isArray(commits) && commits.length > 0) {
|
|
978
979
|
// Full-width block, not the label/value two-column row: commit lines
|
|
@@ -993,13 +994,9 @@ window.__ModuleLoader__.load({
|
|
|
993
994
|
}
|
|
994
995
|
// NO pull-request row: the chip's own PR token (number, CI state, link)
|
|
995
996
|
// carries it — a second rendering two pixels away is pure duplication.
|
|
996
|
-
return
|
|
997
|
+
return rows;
|
|
997
998
|
}
|
|
998
999
|
|
|
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
1000
|
// The host's plugin-card chrome, reproduced inline: its CSS module is not
|
|
1004
1001
|
// exported to plugins, so these values mirror PluginCard.module.css so our
|
|
1005
1002
|
// card sits in the same list as its siblings.
|
|
@@ -1045,52 +1042,43 @@ window.__ModuleLoader__.load({
|
|
|
1045
1042
|
flex: "none"
|
|
1046
1043
|
};
|
|
1047
1044
|
|
|
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
1045
|
/**
|
|
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
|
|
1046
|
+
* The checkout's verdict rows, for the chip's one card — INPUT CHIP ONLY.
|
|
1047
|
+
* Pure presentation over fields the status response already carries, plus
|
|
1048
|
+
* the `detail=1` additions (recent commits, stash) when they have arrived.
|
|
1049
|
+
* Everything is spelled out in text, so no fact here depends on a colour or
|
|
1050
|
+
* a glyph.
|
|
1065
1051
|
*/
|
|
1066
|
-
function
|
|
1052
|
+
function statusRows(info, detail) {
|
|
1067
1053
|
// the detail payload is a superset of the plain one, so it wins when it
|
|
1068
1054
|
// has landed; until then the card still shows the base facts
|
|
1069
1055
|
const data = detail === void 0 ? info : detail;
|
|
1070
1056
|
const rows = [];
|
|
1071
|
-
const add = (label, value) =>
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
//
|
|
1078
|
-
//
|
|
1079
|
-
//
|
|
1080
|
-
//
|
|
1081
|
-
//
|
|
1057
|
+
const add = (label, value) => addRow(rows, label, value);
|
|
1058
|
+
// The ACTION header, first: the SAME sentence the session-row card renders,
|
|
1059
|
+
// from the same helper, so the chip card explains the icon the row shows.
|
|
1060
|
+
// It is a heading, not a labelled row, so the verdict leads the card.
|
|
1061
|
+
const header = actionHeader(data);
|
|
1062
|
+
if (header !== void 0) rows.push(header);
|
|
1063
|
+
// The "next step" row — the SERVER's ranked suggestion, and nothing else. The
|
|
1064
|
+
// client's extras (secondary picker entries) must never promote
|
|
1065
|
+
// themselves into this line: when the server says there is nothing to do
|
|
1066
|
+
// — clean and synced — the row is omitted, and "no suggestion" stays a
|
|
1067
|
+
// suggestion. Plain text, no affordance: the actionable surface is the
|
|
1068
|
+
// (+) menu's /gh picker, and this row is the read-only pointer to it.
|
|
1082
1069
|
const topAction = data.next;
|
|
1083
1070
|
if (topAction !== void 0) {
|
|
1084
1071
|
// git-comment convention: the invocation, two spaces, then "# "
|
|
1085
|
-
// plus what it does
|
|
1086
|
-
|
|
1072
|
+
// plus what it does. The marker makes the one actionable line findable
|
|
1073
|
+
// among the detail rows.
|
|
1074
|
+
add("next step", "\u25B8 /gh " + topAction.args + " # " + topAction.what);
|
|
1087
1075
|
}
|
|
1088
1076
|
// The branch lineage rows (branch, upstream, sync, pull request) moved
|
|
1089
1077
|
// to the branch NAME's hover — the card is the checkout's verdict, not
|
|
1090
1078
|
// the branch's CV. The branch name is also the chip's own text, so a
|
|
1091
1079
|
// card row for it was pure repetition.
|
|
1092
|
-
add("
|
|
1093
|
-
add("operation",
|
|
1080
|
+
add("changes", formatFileBreakdown(data));
|
|
1081
|
+
add("operation", data.operation === void 0 || data.operation === null ? void 0 : String(data.operation));
|
|
1094
1082
|
// WHICH checkout the badge describes, when it is a worktree. The chip shows
|
|
1095
1083
|
// the branch alone, so this row is the only place the tree is named.
|
|
1096
1084
|
add("worktree", worktreeDetail(data));
|
|
@@ -1102,10 +1090,36 @@ window.__ModuleLoader__.load({
|
|
|
1102
1090
|
// The conversation's OWN directory, present only when the badge is
|
|
1103
1091
|
// describing a followed worktree — the chip's branch and counts are the
|
|
1104
1092
|
// tree's in that case, so this is where the checkout's own state stays
|
|
1105
|
-
// visible.
|
|
1106
|
-
|
|
1093
|
+
// visible. Labelled `own checkout` because `checkout` now names the
|
|
1094
|
+
// checkout a session-row card describes. Absent until the `detail=1`
|
|
1095
|
+
// response lands, like commits/stash.
|
|
1096
|
+
if (data.worktreeFollowed === true) add("own checkout", formatCheckoutDetail(data.checkout));
|
|
1107
1097
|
add("stash", data.stashCount === void 0 ? void 0 : data.stashCount + " stashed");
|
|
1108
|
-
return
|
|
1098
|
+
return rows;
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
/**
|
|
1102
|
+
* The chip's ONE card, behind the leading cluster (the action glyph and the
|
|
1103
|
+
* branch name together). It stacks the checkout's verdict rows over the
|
|
1104
|
+
* branch's lineage rows, so one hover answers both "what is this checkout
|
|
1105
|
+
* doing" and "what is on this branch".
|
|
1106
|
+
*
|
|
1107
|
+
* One card, not two: the glyph and the branch are one target, and the branch
|
|
1108
|
+
* is the element that always exists — so a clean checkout, which has no
|
|
1109
|
+
* glyph, still opens the card from its branch name.
|
|
1110
|
+
*/
|
|
1111
|
+
function ChipCard({ info, detail }) {
|
|
1112
|
+
const data = detail === void 0 ? info : detail;
|
|
1113
|
+
const rows = statusRows(info, detail);
|
|
1114
|
+
const lineage = lineageRows(data);
|
|
1115
|
+
return react_jsx_runtime.jsx("div", {
|
|
1116
|
+
style: CARD_CONTAINER,
|
|
1117
|
+
children: [
|
|
1118
|
+
...rows,
|
|
1119
|
+
lineage.length === 0 ? null : react_jsx_runtime.jsx("div", { style: CARD_DIVIDER, children: "branch" }, "branch-divider"),
|
|
1120
|
+
...lineage
|
|
1121
|
+
]
|
|
1122
|
+
});
|
|
1109
1123
|
}
|
|
1110
1124
|
|
|
1111
1125
|
//#region composer chip (upstream additive surface: conversation.input.left)
|
|
@@ -1128,211 +1142,152 @@ window.__ModuleLoader__.load({
|
|
|
1128
1142
|
}
|
|
1129
1143
|
|
|
1130
1144
|
/**
|
|
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.
|
|
1145
|
+
* The chip's leading action glyph, or null when the checkout needs none.
|
|
1146
|
+
* The SAME token the sidebar row draws, in the node half's severity colour.
|
|
1147
|
+
* No token means NO GLYPH: a clean, synced checkout has no action to triage,
|
|
1148
|
+
* so it shows no icon rather than a decorative dot.
|
|
1142
1149
|
*/
|
|
1150
|
+
function chipGlyphNode(info) {
|
|
1151
|
+
const token = info.rowToken;
|
|
1152
|
+
if (typeof token !== "string" || token === "") return null;
|
|
1153
|
+
return react_jsx_runtime.jsx("span", {
|
|
1154
|
+
key: "glyph",
|
|
1155
|
+
role: "img",
|
|
1156
|
+
"aria-label": "git action: " + token,
|
|
1157
|
+
style: { color: SEVERITY_FILL[info.rowSeverity] ?? META_STYLE.color, flex: "none", ...GLYPH_TEXT, display: "inline-flex", alignItems: "center" },
|
|
1158
|
+
children: actionGlyphNode(token)
|
|
1159
|
+
});
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1143
1162
|
/**
|
|
1144
|
-
*
|
|
1145
|
-
*
|
|
1146
|
-
*
|
|
1147
|
-
*
|
|
1163
|
+
* The shared anchor for the chip's two links — the branch compare link and
|
|
1164
|
+
* the PR token. Both were near-identical `<a>` elements; this is the one
|
|
1165
|
+
* copy of target, rel, cursor and handler wiring, so they cannot drift.
|
|
1166
|
+
* `underline` is the hover/focus state and `children` is the label.
|
|
1148
1167
|
*/
|
|
1149
|
-
|
|
1168
|
+
function chipAnchor(props) {
|
|
1169
|
+
return react_jsx_runtime.jsx("a", {
|
|
1170
|
+
href: props.href,
|
|
1171
|
+
target: "_blank",
|
|
1172
|
+
rel: "noopener noreferrer",
|
|
1173
|
+
"aria-label": props.ariaLabel,
|
|
1174
|
+
onPointerEnter: props.onPointerEnter,
|
|
1175
|
+
onPointerLeave: props.onPointerLeave,
|
|
1176
|
+
onFocus: props.onFocus,
|
|
1177
|
+
onBlur: props.onBlur,
|
|
1178
|
+
style: {
|
|
1179
|
+
color: "inherit",
|
|
1180
|
+
textDecoration: props.underline ? "underline" : "none",
|
|
1181
|
+
cursor: "pointer"
|
|
1182
|
+
},
|
|
1183
|
+
children: props.children
|
|
1184
|
+
});
|
|
1185
|
+
}
|
|
1150
1186
|
|
|
1151
1187
|
/**
|
|
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.
|
|
1188
|
+
* The branch name, as a compare anchor when the detail payload vouches an
|
|
1189
|
+
* http(s) URL. The sync arrows ride WITH the branch: they are the branch's
|
|
1190
|
+
* standing against its upstream, so the same hover and the same link cover
|
|
1191
|
+
* them. The ✎n count keeps its own surface.
|
|
1158
1192
|
*/
|
|
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;
|
|
1193
|
+
function chipBranchInner(info, detail, isBranchLink, hoveredLink, setHovered, setHoveredLink) {
|
|
1194
|
+
const sync = formatBaseSync(info);
|
|
1195
|
+
const branchLabel = info.branch + sync;
|
|
1196
|
+
if (!isBranchLink) return branchLabel;
|
|
1197
|
+
return chipAnchor({
|
|
1198
|
+
href: detail.compareUrl,
|
|
1199
|
+
ariaLabel: "Compare " + info.branch + " with " + baseNameFor(info) + " on GitHub" + (sync === "" ? "" : ", " + (info.mainAhead || 0) + " ahead and " + (info.mainBehind || 0) + " behind " + baseNameFor(info)),
|
|
1200
|
+
underline: hoveredLink === "branch",
|
|
1201
|
+
onPointerEnter: () => setHoveredLink("branch"),
|
|
1202
|
+
onPointerLeave: () => setHoveredLink(null),
|
|
1203
|
+
onFocus: () => { setHovered(true); setHoveredLink("branch"); },
|
|
1204
|
+
onBlur: () => setHoveredLink(null),
|
|
1205
|
+
children: branchLabel
|
|
1206
|
+
});
|
|
1207
|
+
}
|
|
1184
1208
|
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1209
|
+
/**
|
|
1210
|
+
* The LEADING CLUSTER — the action glyph (when the checkout needs one) and
|
|
1211
|
+
* the branch name — wrapped in the one hover card. It is the chip card's ONLY
|
|
1212
|
+
* anchor: pointer rest here enables the detail=1 fetch.
|
|
1213
|
+
*/
|
|
1214
|
+
function chipLeadingHover(info, detail, tokenGlyph, branchInner, isBranchLink, setHovered) {
|
|
1215
|
+
return withTooltip(
|
|
1191
1216
|
react_jsx_runtime.jsx("span", {
|
|
1192
1217
|
onPointerEnter: () => setHovered(true),
|
|
1193
|
-
style: { display: "inline-flex", alignItems: "center", flex: "none", cursor: "default" },
|
|
1194
|
-
children: react_jsx_runtime.jsx(
|
|
1218
|
+
style: { display: "inline-flex", alignItems: "center", gap: "6px", flex: "none", cursor: isBranchLink ? "pointer" : "default" },
|
|
1219
|
+
children: [tokenGlyph, react_jsx_runtime.jsx("span", { key: "branch", children: branchInner })]
|
|
1195
1220
|
}),
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
() => react_jsx_runtime.jsx(HoverCard, { info, detail }),
|
|
1199
|
-
600 // matches the body cap: the tooltip must never be the clamp
|
|
1221
|
+
() => react_jsx_runtime.jsx(ChipCard, { info, detail }),
|
|
1222
|
+
600
|
|
1200
1223
|
);
|
|
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),
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
/**
|
|
1227
|
+
* The ✎n count as its OWN hover surface: a names-only tooltip. It shares
|
|
1228
|
+
* the mark's lazy detail fetch and degrades to a plain count without the
|
|
1229
|
+
* Tooltip primitive.
|
|
1230
|
+
*/
|
|
1231
|
+
function chipCountHover(info, detail, setHovered) {
|
|
1232
|
+
const countText = formatFileCount(info);
|
|
1233
|
+
const count = react_jsx_runtime.jsx("span", {
|
|
1234
|
+
onPointerEnter: () => setHovered(true),
|
|
1235
|
+
style: { cursor: "default" },
|
|
1236
|
+
children: countText
|
|
1237
|
+
});
|
|
1238
|
+
const pending = detail === void 0 || detail === null;
|
|
1239
|
+
return withTooltip(count, () => react_jsx_runtime.jsx(CountNames, { info: detail ?? info, pending }), 480);
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
/**
|
|
1243
|
+
* The PR token: the number, the CI state glyph and the GitHub link, and
|
|
1244
|
+
* nothing else. It has NO hover card of its own — the chip/branch card
|
|
1245
|
+
* already lists the branch's commits. ONE element, split so the STATE GLYPH
|
|
1246
|
+
* can take the CI colour while the number keeps the chip's muted colour.
|
|
1247
|
+
*/
|
|
1248
|
+
function chipPrTokenHover(info, hoveredLink, setHoveredLink) {
|
|
1249
|
+
const prParts = prTokenParts(info);
|
|
1250
|
+
const prToken = prParts === null ? null : [
|
|
1251
|
+
prParts.number,
|
|
1252
|
+
prParts.glyph === "" ? null : react_jsx_runtime.jsx("span", {
|
|
1253
|
+
key: "pr-state",
|
|
1254
|
+
style: { color: prParts.colour },
|
|
1255
|
+
children: " " + prParts.glyph
|
|
1256
|
+
}),
|
|
1257
|
+
prParts.draft
|
|
1258
|
+
];
|
|
1259
|
+
const prUrl = prLinkUrl(info);
|
|
1260
|
+
if (prUrl === void 0) {
|
|
1261
|
+
// plain token: no hover, so it wires no pointer handlers
|
|
1262
|
+
return react_jsx_runtime.jsx("span", {
|
|
1263
|
+
// the glyph is not the only channel: the token says what the CI state IS
|
|
1264
|
+
"aria-label": prTokenLabel(info),
|
|
1267
1265
|
style: { cursor: "default" },
|
|
1268
|
-
children:
|
|
1266
|
+
children: prToken
|
|
1269
1267
|
});
|
|
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", {
|
|
1268
|
+
}
|
|
1269
|
+
return chipAnchor({
|
|
1270
|
+
href: prUrl,
|
|
1271
|
+
ariaLabel: prTokenLabel(info) + ", opens on GitHub in a new tab",
|
|
1272
|
+
underline: hoveredLink === "pr",
|
|
1273
|
+
onPointerEnter: () => setHoveredLink("pr"),
|
|
1274
|
+
onPointerLeave: () => setHoveredLink(null),
|
|
1275
|
+
onFocus: () => setHoveredLink("pr"),
|
|
1276
|
+
onBlur: () => setHoveredLink(null),
|
|
1277
|
+
children: prToken
|
|
1278
|
+
});
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
/** The chip row itself: leading cluster, operation text, count and PR token. */
|
|
1282
|
+
function chipAssemble(leadingHover, text, countHover, prTokenHover) {
|
|
1283
|
+
return react_jsx_runtime.jsxs("span", {
|
|
1329
1284
|
style: {
|
|
1330
1285
|
display: "inline-flex",
|
|
1331
1286
|
alignItems: "center",
|
|
1332
1287
|
flex: "none",
|
|
1333
|
-
// 6px, not 4px: the PR token leads with a space, and CSS drops
|
|
1334
|
-
//
|
|
1335
|
-
//
|
|
1288
|
+
// 6px, not 4px: the PR token leads with a space, and CSS drops leading
|
|
1289
|
+
// whitespace at the start of a flex item, so 4px read as no separation
|
|
1290
|
+
// at all between the branch and `PR#…`
|
|
1336
1291
|
gap: "6px",
|
|
1337
1292
|
color: "var(--dsw-alias-label-secondary, #5b6570)",
|
|
1338
1293
|
fontSize: "12px",
|
|
@@ -1340,92 +1295,57 @@ window.__ModuleLoader__.load({
|
|
|
1340
1295
|
whiteSpace: "nowrap",
|
|
1341
1296
|
cursor: "default"
|
|
1342
1297
|
},
|
|
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
1298
|
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 }),
|
|
1299
|
+
leadingHover,
|
|
1300
|
+
react_jsx_runtime.jsx("span", { key: "text", children: text }),
|
|
1360
1301
|
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
|
-
),
|
|
1302
|
+
prTokenHover
|
|
1424
1303
|
]
|
|
1425
1304
|
});
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
/**
|
|
1308
|
+
* Chip line in the input row: git state of the workspace the CURRENT
|
|
1309
|
+
* conversation is attached to. It targets the session id and lets the node
|
|
1310
|
+
* half resolve the workspace, so this surface needs no `workspaces`
|
|
1311
|
+
* service, no cwd and no path plumbing at all. `conversation.input.left`
|
|
1312
|
+
* is an upstream additive slot, so it works on every install, and it stays
|
|
1313
|
+
* useful next to the session row because it is context-anchored ("where am
|
|
1314
|
+
* I") rather than surveying.
|
|
1315
|
+
*
|
|
1316
|
+
* This is the ONLY surface that asks the node half for the extras (`pr=1`
|
|
1317
|
+
* always, `detail=1` on hover), because it is the only one that renders
|
|
1318
|
+
* them — see targetQuery.
|
|
1319
|
+
*/
|
|
1320
|
+
function ComposerGitChip({ sessionId }) {
|
|
1321
|
+
const target = sessionId === void 0 ? void 0 : { kind: "session", id: sessionId };
|
|
1322
|
+
// the PR/CI token is always on the chip, so its fetch is not gated
|
|
1323
|
+
const info = useGitStatus(target, { pr: true });
|
|
1324
|
+
// The card's extras cost a `log -10` plus a stash list, so they are fetched
|
|
1325
|
+
// only once a pointer actually RESTS on the chip. A refresh fires on every
|
|
1326
|
+
// file edit, so eagerly asking would add both invocations to every refresh.
|
|
1327
|
+
const [hovered, setHovered] = react.useState(false);
|
|
1328
|
+
// Which of the chip's two links is hovered/focused — ONE enum, not two
|
|
1329
|
+
// booleans: "branch" | "pr" | null. The anchor underlines only when its
|
|
1330
|
+
// own value matches, so hovering one link never underlines the other.
|
|
1331
|
+
const [hoveredLink, setHoveredLink] = react.useState(null);
|
|
1332
|
+
const detail = useGitStatus(target, { pr: true, detail: true, enabled: hovered });
|
|
1333
|
+
if (info === void 0 || info.git !== true) return null;
|
|
1334
|
+
|
|
1335
|
+
const tokenGlyph = chipGlyphNode(info);
|
|
1336
|
+
// The branch name links to the compare view (base...branch). Vouched http(s)
|
|
1337
|
+
// only; new tab, never navigating the conversation away.
|
|
1338
|
+
const isBranchLink = typeof detail?.compareUrl === "string" && detail.compareUrl.startsWith("https://");
|
|
1339
|
+
const branchInner = chipBranchInner(info, detail, isBranchLink, hoveredLink, setHovered, setHoveredLink);
|
|
1340
|
+
// The Tooltip's CHILD must not change element type once the detail payload
|
|
1341
|
+
// lands, so the cluster span is the stable hover surface. ALWAYS wrapped.
|
|
1342
|
+
const leadingHover = chipLeadingHover(info, detail, tokenGlyph, branchInner, isBranchLink, setHovered);
|
|
1343
|
+
const text = formatOperationToken(info);
|
|
1344
|
+
const countHover = chipCountHover(info, detail, setHovered);
|
|
1345
|
+
const prTokenHover = chipPrTokenHover(info, hoveredLink, setHoveredLink);
|
|
1346
|
+
const chip = chipAssemble(leadingHover, text, countHover, prTokenHover);
|
|
1347
|
+
// No Tooltip primitive (a shell that does not seed it): the leading cluster
|
|
1348
|
+
// renders bare and the badge still works — the card is an enhancement.
|
|
1429
1349
|
return chip;
|
|
1430
1350
|
}
|
|
1431
1351
|
|
|
@@ -1435,7 +1355,7 @@ window.__ModuleLoader__.load({
|
|
|
1435
1355
|
// node half resolves the workspace, so the client never needs a service
|
|
1436
1356
|
// lookup. Fewer declared services also means fewer ways to fail to load.
|
|
1437
1357
|
/** The built-in ranking order — what "Reset to default" restores. */
|
|
1438
|
-
const DEFAULT_ORDER = ["operation", "unmerged", "sync", "publish", "merge", "commit", "checks"];
|
|
1358
|
+
const DEFAULT_ORDER = ["operation", "unmerged", "conflict", "review", "sync", "publish", "merge", "commit", "checks"];
|
|
1439
1359
|
|
|
1440
1360
|
/**
|
|
1441
1361
|
* Move one category within the ranking order — the card's only reorder
|
|
@@ -1455,6 +1375,8 @@ window.__ModuleLoader__.load({
|
|
|
1455
1375
|
const CATEGORY_LABELS = {
|
|
1456
1376
|
operation: ["operation", "resume a paused merge/rebase"],
|
|
1457
1377
|
unmerged: ["unmerged", "resolve conflict markers"],
|
|
1378
|
+
conflict: ["conflict", "a pull request that conflicts with its base"],
|
|
1379
|
+
review: ["review", "a review to address, or a review to wait for"],
|
|
1458
1380
|
sync: ["sync", "pull, or rebase + confirmed force-push"],
|
|
1459
1381
|
publish: ["publish", "push local commits"],
|
|
1460
1382
|
merge: ["merge", "a merge-ready pull request"],
|
|
@@ -1590,12 +1512,12 @@ window.__ModuleLoader__.load({
|
|
|
1590
1512
|
});
|
|
1591
1513
|
}
|
|
1592
1514
|
|
|
1593
|
-
const inject = ["slots"
|
|
1515
|
+
const inject = ["slots"];
|
|
1594
1516
|
|
|
1595
1517
|
/**
|
|
1596
|
-
* Register the badge into
|
|
1597
|
-
* owner share as props; the row
|
|
1598
|
-
* deliberately
|
|
1518
|
+
* Register the badge into upstream's own seats. The slot owner hands each
|
|
1519
|
+
* entry the row owner share as props; the row surfaces destructure
|
|
1520
|
+
* `{ sessionId }` and deliberately ignore the cwd they may also be given.
|
|
1599
1521
|
*/
|
|
1600
1522
|
function apply(ctx) {
|
|
1601
1523
|
// The /gh command contribution (the (+) commands menu): the state-aware
|
|
@@ -1810,33 +1732,17 @@ window.__ModuleLoader__.load({
|
|
|
1810
1732
|
}
|
|
1811
1733
|
}
|
|
1812
1734
|
|
|
1813
|
-
//
|
|
1814
|
-
//
|
|
1815
|
-
//
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
//
|
|
1821
|
-
//
|
|
1822
|
-
// = off" while five row badges were rendering.
|
|
1823
|
-
let rowsReported = false;
|
|
1824
|
-
ctx.slots.inject("sidebar.workspaces.sessionRow", () => {
|
|
1825
|
-
if (!rowsReported) {
|
|
1826
|
-
rowsReported = true;
|
|
1827
|
-
console.info("[dsh-git-badge] surfaces: sidebar session rows = on (seam present).");
|
|
1828
|
-
}
|
|
1829
|
-
return ctx.slots.register({
|
|
1830
|
-
name: "sidebar.workspaces.sessionRow",
|
|
1831
|
-
id: "git-badge-row"
|
|
1832
|
-
}, SessionGitBadge);
|
|
1833
|
-
});
|
|
1834
|
-
// The row is ALREADY a HoverCard anchor upstream, so a badge that opened
|
|
1835
|
-
// its own tooltip would nest two cards. This renders the provenance line
|
|
1735
|
+
// The row's token rides upstream's OWN `sidebar.session.row.leading`
|
|
1736
|
+
// seat: a 16px cell the workspace browser renders in the row's leading
|
|
1737
|
+
// cluster. No patch and no declaration to make.
|
|
1738
|
+
ctx.slots.inject("sidebar.session.row.leading", () => ctx.slots.register({
|
|
1739
|
+
name: "sidebar.session.row.leading",
|
|
1740
|
+
id: "git-badge-row-leading"
|
|
1741
|
+
}, LeadingTokenGlyph));
|
|
1742
|
+
// The row is ALREADY a HoverCard anchor upstream, so a glyph that opened
|
|
1743
|
+
// its own tooltip would nest two cards. This renders the explanation
|
|
1836
1744
|
// 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.
|
|
1745
|
+
// itself (sidebar.session.row.hover) — no request of its own.
|
|
1840
1746
|
ctx.slots.inject("sidebar.session.row.hover", () => ctx.slots.register({
|
|
1841
1747
|
name: "sidebar.session.row.hover",
|
|
1842
1748
|
id: "git-badge-row-detail"
|
|
@@ -1852,29 +1758,9 @@ window.__ModuleLoader__.load({
|
|
|
1852
1758
|
id: "git-badge-chip",
|
|
1853
1759
|
inject: (sessionId) => ({ sessionId })
|
|
1854
1760
|
}, 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);
|
|
1761
|
+
// No seam, no apply step: the leading seat and the hover seat are both
|
|
1762
|
+
// upstream's own. Report the surfaces once, for diagnostics.
|
|
1763
|
+
console.info("[dsh-git-badge] surfaces: input chip = on; sidebar row token = on (upstream leading seat).");
|
|
1878
1764
|
// The hover card depends on a primitive the SHELL seeds, not on anything
|
|
1879
1765
|
// this plugin declares. Report the outcome rather than letting a missing
|
|
1880
1766
|
// seed look like a missing feature: the try/catch above deliberately keeps
|
|
@@ -1893,7 +1779,7 @@ window.__ModuleLoader__.load({
|
|
|
1893
1779
|
// Additive; the host reads apply/inject and ignores the rest. The suite
|
|
1894
1780
|
// drives these to assert the REQUEST contract — which surface asks for the
|
|
1895
1781
|
// expensive extras — without a browser, a fetch or a network.
|
|
1896
|
-
exports.__internals = { targetQuery,
|
|
1782
|
+
exports.__internals = { targetQuery, worktreeDetail, linkedWorktreesDetail, ghSkillActions, cleanGate, moveCategory };
|
|
1897
1783
|
return module.exports;
|
|
1898
1784
|
}
|
|
1899
1785
|
});
|