dsh-git-badge 0.17.0 → 0.17.2
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 +59 -7
- package/README.md +1 -1
- package/lib/client.js +104 -68
- package/lib/index.js +98 -120
- package/package.json +2 -4
- package/seam/apply.js +34 -154
- package/seam/checkout.js +3 -5
- package/seam/patch.js +142 -0
- package/seam/postinstall.js +38 -50
- package/seam/store.js +8 -4
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,47 @@ npm. 0.6.0 is the first release recorded here.
|
|
|
10
10
|
|
|
11
11
|
## [Unreleased]
|
|
12
12
|
|
|
13
|
+
## [0.17.2] - 2026-09-17
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- **The default branch's branch hover no longer says `no pull request`.** The
|
|
18
|
+
always-render merge row (0.17.0) only ever made sense on a feature branch:
|
|
19
|
+
`main` is the base pull requests merge INTO, and none is ever opened FROM it,
|
|
20
|
+
so `no pull request` there read as a nag for something that cannot exist. When
|
|
21
|
+
the node half flags the branch as the default one (`defaultBranch: true`, the
|
|
22
|
+
same flag the action rules use to suppress "open a pull request" on `main`),
|
|
23
|
+
the row now reads `base branch · pull requests merge into this`. Its
|
|
24
|
+
base-relative counts are suppressed there too — `N ahead, M behind main` while
|
|
25
|
+
ON `main` is self-referential, and that divergence is already the chip's own
|
|
26
|
+
`↑a ↓b` token. Feature branches are unchanged: still `no pull request`, still
|
|
27
|
+
the counts when they are known.
|
|
28
|
+
|
|
29
|
+
## [0.17.1] - 2026-09-17
|
|
30
|
+
|
|
31
|
+
### Fixed
|
|
32
|
+
|
|
33
|
+
- **The branch, file-count and PR hovers open on the FIRST hover.** Those three
|
|
34
|
+
surfaces were only wrapped in the shell's `Tooltip` once the lazy `detail=1`
|
|
35
|
+
payload arrived. That primitive is uncontrolled — it has no `open` prop and
|
|
36
|
+
opens from a `mouseenter` it attaches to its child — so a wrapper mounted after
|
|
37
|
+
the pointer had already arrived could never open for that visit: the first hover
|
|
38
|
+
did nothing, and worked only after leaving and returning (or after hovering the
|
|
39
|
+
status mark had already warmed the cache). All four surfaces are now wrapped
|
|
40
|
+
from the first render, and a label whose detail is still in flight degrades to
|
|
41
|
+
the base response instead of claiming an empty list — the count restates the
|
|
42
|
+
file breakdown, the PR token names the PR. The branch's hover child also stays a
|
|
43
|
+
`span`, so the compare link appearing underneath a resting pointer cannot
|
|
44
|
+
remount it. The status mark's own card was never affected.
|
|
45
|
+
|
|
46
|
+
### Changed
|
|
47
|
+
|
|
48
|
+
- **Internal simplification pass, no user-facing change** (#52): one
|
|
49
|
+
`singleFlight` now backs the status cache and its in-flight coalescing, one
|
|
50
|
+
patch engine (`seam/patch.js`) backs both `seam/apply.js` and
|
|
51
|
+
`seam/postinstall.js`, and the server returns one `nextActions` shape
|
|
52
|
+
(`{actions, next}`) for the row's action list.
|
|
53
|
+
|
|
13
54
|
## [0.17.0] - 2026-09-16
|
|
14
55
|
|
|
15
56
|
### Added
|
|
@@ -278,22 +319,18 @@ npm. 0.6.0 is the first release recorded here.
|
|
|
278
319
|
|
|
279
320
|
## [0.14.1] - 2026-09-15
|
|
280
321
|
|
|
322
|
+
### Changed
|
|
323
|
+
|
|
281
324
|
- **Skill alignment:** the `gh` skill gains an explicit handoff — one verb per
|
|
282
325
|
invocation is its whole job; the multi-step PR lifecycle (review, merge,
|
|
283
326
|
verify, cleanup) belongs to the `gh-pr` skill. The `gh-pr` skill states the
|
|
284
327
|
mirror scope: one-shot verbs are `gh`'s, lifecycle is its own. The catalog
|
|
285
328
|
stays global-only; repo conventions stay in `AGENTS.md`.
|
|
286
|
-
|
|
287
329
|
- **The hover card is 33% wider** (360px → 480px) and its `action:` row now
|
|
288
330
|
carries a trailing what-comment in git-comment convention — e.g.
|
|
289
331
|
`action: /gh push # push local commits to the remote` — so the row says
|
|
290
332
|
what the invocation does, not just which invocation it is. The picker in the
|
|
291
333
|
(+) menu shares the same descriptions.
|
|
292
|
-
|
|
293
|
-
## [0.14.1] - 2026-09-15
|
|
294
|
-
|
|
295
|
-
### Changed
|
|
296
|
-
|
|
297
334
|
- **The chip's hover card top line is now an `action:` row** — the gh skill
|
|
298
335
|
invocation the checkout justifies (e.g. `/gh push`), in the card's ordinary
|
|
299
336
|
label/value layout, replacing the next-step line's why-plus-copy-chip. The
|
|
@@ -685,7 +722,22 @@ npm. 0.6.0 is the first release recorded here.
|
|
|
685
722
|
- `detail=1` is annotated as having no consumer yet (the hover card remains
|
|
686
723
|
pending). Its comment previously claimed `log -1` while the code ran `log -3`.
|
|
687
724
|
|
|
688
|
-
[Unreleased]: https://github.com/Kevin-McIsaac/dsh-workspace-git-badge/compare/v0.
|
|
725
|
+
[Unreleased]: https://github.com/Kevin-McIsaac/dsh-workspace-git-badge/compare/v0.17.2...HEAD
|
|
726
|
+
[0.17.2]: https://github.com/Kevin-McIsaac/dsh-workspace-git-badge/releases/tag/v0.17.2
|
|
727
|
+
[0.17.1]: https://github.com/Kevin-McIsaac/dsh-workspace-git-badge/releases/tag/v0.17.1
|
|
728
|
+
[0.17.0]: https://github.com/Kevin-McIsaac/dsh-workspace-git-badge/releases/tag/v0.17.0
|
|
729
|
+
[0.16.1]: https://github.com/Kevin-McIsaac/dsh-workspace-git-badge/releases/tag/v0.16.1
|
|
730
|
+
[0.16.0]: https://github.com/Kevin-McIsaac/dsh-workspace-git-badge/releases/tag/v0.16.0
|
|
731
|
+
[0.15.0]: https://github.com/Kevin-McIsaac/dsh-workspace-git-badge/releases/tag/v0.15.0
|
|
732
|
+
[0.14.1]: https://github.com/Kevin-McIsaac/dsh-workspace-git-badge/releases/tag/v0.14.1
|
|
733
|
+
[0.14.0]: https://github.com/Kevin-McIsaac/dsh-workspace-git-badge/releases/tag/v0.14.0
|
|
734
|
+
[0.13.0]: https://github.com/Kevin-McIsaac/dsh-workspace-git-badge/releases/tag/v0.13.0
|
|
735
|
+
[0.12.0]: https://github.com/Kevin-McIsaac/dsh-workspace-git-badge/releases/tag/v0.12.0
|
|
736
|
+
[0.11.0]: https://github.com/Kevin-McIsaac/dsh-workspace-git-badge/releases/tag/v0.11.0
|
|
737
|
+
[0.10.1]: https://github.com/Kevin-McIsaac/dsh-workspace-git-badge/releases/tag/v0.10.1
|
|
738
|
+
[0.10.0]: https://github.com/Kevin-McIsaac/dsh-workspace-git-badge/releases/tag/v0.10.0
|
|
739
|
+
[0.9.1]: https://github.com/Kevin-McIsaac/dsh-workspace-git-badge/releases/tag/v0.9.1
|
|
740
|
+
[0.9.0]: https://github.com/Kevin-McIsaac/dsh-workspace-git-badge/releases/tag/v0.9.0
|
|
689
741
|
[0.8.0]: https://github.com/Kevin-McIsaac/dsh-workspace-git-badge/releases/tag/v0.8.0
|
|
690
742
|
[0.7.0]: https://github.com/Kevin-McIsaac/dsh-workspace-git-badge/releases/tag/v0.7.0
|
|
691
743
|
[0.6.0]: https://github.com/Kevin-McIsaac/dsh-workspace-git-badge/releases/tag/v0.6.0
|
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
Git status badges for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness):
|
|
4
4
|
|
|
5
5
|
- **Input-row chip** — `● main ↑0 ↓2 ✎3` next to the access picker, showing the git state of the workspace the current conversation is attached to. Works on every install.
|
|
6
|
-
- **Hover card** — rest a pointer on the chip for what won't fit: the file breakdown (`✎3` split into staged / unstaged / untracked), the last
|
|
6
|
+
- **Hover card** — rest a pointer on the chip for what won't fit: the file breakdown (`✎3` split into staged / unstaged / untracked), the last ten commits, and the stash. Fetched only on hover, so the everyday badge pays nothing for it. Works on every install.
|
|
7
7
|
- **PR / CI token** — `● main ↑0 ↓2 PR#142 ✗` when the branch has a GitHub pull request, read through your own `gh` CLI (the plugin never handles a credential). The token **links to the PR** — it opens in a new tab, and underlines when you hover or focus it. Invisible when `gh` cannot answer: not installed, not logged in, no GitHub remote, offline or rate-limited all render the unchanged chip, and a token with no URL to link to stays plain text.
|
|
8
8
|
- **Worktree-aware** — a conversation's directory is fixed when it is created, so a conversation that does its work in a linked `git worktree` would otherwise keep reporting the main checkout. The session registers its checkout (`npx dsh-git-badge-checkout <path>`, run by the git-worktree and `/gh checkout` paths), and the badge describes **that worktree** instead — its branch, counts, tree mark and PR/CI token (`🌳 chore/global-skills-tiering ↑0 ↓2 PR#391 ✓` rather than `● main`). The hover card names the tree in a `worktree` row and adds a `checkout` row with the conversation's own directory, so the main checkout's branch and dirty state stay one hover away. Registrations are advisory: git's own worktree list validates the path, entries expire after 24h, and a workspace-targeted row always reports the checkout the registry owns.
|
|
9
9
|
- **Sidebar session-row action token** — each session row shows the ONE thing that conversation needs from you, or nothing at all: `merge`, `fix CI`, `review`, `resolve`, `pull`, `push`. It is triage rather than status: `merge` appears only when GitHub itself reports `mergeStateStatus: CLEAN`, and every waiting state (draft, blocked, behind, checks still running) stays silent. Hovering the row adds a line naming the checkout and the pull request the action refers to. The token floats to the right of the row, is set heavier than the timestamp beside it, and is coloured by how much it needs you — red for a conflict or a failing build, amber for a requested review, green for `merge`, quiet grey for routine sync. It appears only when the session is **idle** — while a turn is running the row stays quiet, because the checkout is still moving and a suggestion would describe a state that is about to change. Requires the `sidebar.workspaces.sessionRow` seam; without it the plugin degrades to chip-only and logs what's active to the browser console.
|
package/lib/client.js
CHANGED
|
@@ -56,8 +56,17 @@ window.__ModuleLoader__.load({
|
|
|
56
56
|
const Menu = primitives === null ? void 0 : primitives.Menu;
|
|
57
57
|
const rankByName = primitives === null ? void 0 : primitives.rankByName;
|
|
58
58
|
|
|
59
|
-
/**
|
|
59
|
+
/**
|
|
60
|
+
* Module-level git-status cache. Entries are invalidated by SSE events,
|
|
61
|
+
* never by age — and BOUNDED, because a long-lived tab opens and closes
|
|
62
|
+
* many sessions and nothing else removes an entry. When the cap is
|
|
63
|
+
* exceeded the oldest-INSERTED key is evicted (Map order is insertion,
|
|
64
|
+
* not use); an evicted surface simply refetches, which the in-flight map
|
|
65
|
+
* collapses like any other request.
|
|
66
|
+
*/
|
|
60
67
|
const GIT_CACHE = new Map();
|
|
68
|
+
/** The cache's entry bound — each surface is one key, plus lazy detail keys. */
|
|
69
|
+
const GIT_CACHE_LIMIT = 64;
|
|
61
70
|
/** In-flight fetches per cacheKey: bursts collapse into one request. */
|
|
62
71
|
const GIT_INFLIGHT = new Map();
|
|
63
72
|
/** Slow safety-net poll: refreshes even if the SSE stream is silently dead. */
|
|
@@ -138,7 +147,7 @@ window.__ModuleLoader__.load({
|
|
|
138
147
|
* `options.detail` / `options.pr` ask the node half for the chip's extras
|
|
139
148
|
* (see targetQuery). `options.enabled: false` holds the fetch back
|
|
140
149
|
* entirely — that is what makes the hover card's extra fields LAZY: the
|
|
141
|
-
* chip renders without them and only starts paying for `log -
|
|
150
|
+
* chip renders without them and only starts paying for a `log -10` plus a
|
|
142
151
|
* stash list once a pointer actually rests on it. The hook is still called
|
|
143
152
|
* unconditionally (hooks may not be conditional); the GATE is inside.
|
|
144
153
|
*/
|
|
@@ -173,6 +182,9 @@ window.__ModuleLoader__.load({
|
|
|
173
182
|
if (data !== null && data.error !== void 0 && GIT_CACHE.has(cacheKey)) return;
|
|
174
183
|
if (data !== null && typeof data.workspace === "string") resolvedWorkspace = data.workspace;
|
|
175
184
|
GIT_CACHE.set(cacheKey, { at: Date.now(), data });
|
|
185
|
+
if (GIT_CACHE.size > GIT_CACHE_LIMIT) {
|
|
186
|
+
GIT_CACHE.delete(GIT_CACHE.keys().next().value);
|
|
187
|
+
}
|
|
176
188
|
if (alive) setState({ key: cacheKey, data });
|
|
177
189
|
};
|
|
178
190
|
const load = () => {
|
|
@@ -240,13 +252,10 @@ window.__ModuleLoader__.load({
|
|
|
240
252
|
* `/gh pr` was offered on the default branch. The node half owns the
|
|
241
253
|
* rules and the ranking now; the client adds presentation only — the risk
|
|
242
254
|
* gate for `clean`, and the labels.
|
|
243
|
-
*
|
|
244
|
-
* Falls back to the single `next` for a payload from an older node half.
|
|
245
255
|
*/
|
|
246
256
|
function ghSkillActions(info) {
|
|
247
257
|
if (info === void 0 || info === null || info.git !== true) return [];
|
|
248
|
-
|
|
249
|
-
return info.next === void 0 || info.next === null ? [] : [info.next];
|
|
258
|
+
return Array.isArray(info.actions) ? info.actions : [];
|
|
250
259
|
}
|
|
251
260
|
|
|
252
261
|
/**
|
|
@@ -747,9 +756,15 @@ window.__ModuleLoader__.load({
|
|
|
747
756
|
* columns line up in both by construction), and the card's own width
|
|
748
757
|
* behaviour: full-width lines, nowrap, ellipsis.
|
|
749
758
|
*/
|
|
750
|
-
function PrCommitsList({ info }) {
|
|
759
|
+
function PrCommitsList({ info, pending = false }) {
|
|
751
760
|
const commits = info?.prCommits;
|
|
752
761
|
if (!Array.isArray(commits) || commits.length === 0) {
|
|
762
|
+
// `pending`: the detail payload has not landed yet, so "no commits"
|
|
763
|
+
// would be a false negative — name the pull request the token points
|
|
764
|
+
// at instead, the way the mark's card degrades to the base response.
|
|
765
|
+
if (pending) {
|
|
766
|
+
return react_jsx_runtime.jsx("span", { style: CARD_CONTAINER, children: formatPrDetail(info?.pr) ?? "loading\u2026" });
|
|
767
|
+
}
|
|
753
768
|
return react_jsx_runtime.jsx("span", { style: CARD_CONTAINER, children: "no commits beyond the base branch" });
|
|
754
769
|
}
|
|
755
770
|
return react_jsx_runtime.jsxs("div", { style: CARD_CONTAINER, children: [
|
|
@@ -783,12 +798,13 @@ window.__ModuleLoader__.load({
|
|
|
783
798
|
* the one question the count poses ("what are these?"). One stacked
|
|
784
799
|
* column with sub-headers: UNSTAGED (tracked, edited — valid regardless
|
|
785
800
|
* of the collapsed untracked retry) then UNTRACKED. Reads the same lazy
|
|
786
|
-
* detail payload the mark's card uses
|
|
787
|
-
*
|
|
788
|
-
*
|
|
789
|
-
*
|
|
801
|
+
* detail payload the mark's card uses. The wrapper is always mounted (see
|
|
802
|
+
* withTooltip) and the label degrades while the names are in flight: the
|
|
803
|
+
* COUNTS are already in the base response, so it restates the breakdown
|
|
804
|
+
* rather than claiming an empty list. "no changed files" is reserved for a
|
|
805
|
+
* landed payload that genuinely has no names.
|
|
790
806
|
*/
|
|
791
|
-
function CountNames({ info }) {
|
|
807
|
+
function CountNames({ info, pending = false }) {
|
|
792
808
|
const sections = [
|
|
793
809
|
{
|
|
794
810
|
label: "staged",
|
|
@@ -807,6 +823,11 @@ window.__ModuleLoader__.load({
|
|
|
807
823
|
}
|
|
808
824
|
].filter((section) => Array.isArray(section.names) && section.names.length > 0);
|
|
809
825
|
if (sections.length === 0) {
|
|
826
|
+
// `pending`: the names have not been fetched yet, but the COUNTS have —
|
|
827
|
+
// restate those rather than the false "no changed files".
|
|
828
|
+
if (pending) {
|
|
829
|
+
return react_jsx_runtime.jsx("span", { style: CARD_CONTAINER, children: formatFileBreakdown(info) });
|
|
830
|
+
}
|
|
810
831
|
return react_jsx_runtime.jsx("span", { style: CARD_CONTAINER, children: "no changed files" });
|
|
811
832
|
}
|
|
812
833
|
return react_jsx_runtime.jsxs("div", { style: CARD_CONTAINER, children:
|
|
@@ -871,10 +892,22 @@ window.__ModuleLoader__.load({
|
|
|
871
892
|
} else if (pr.state === "failing") mergeParts.push("blocked: checks failing");
|
|
872
893
|
else if (pr.mergeState === "CLEAN" || (pr.state === "passing" && pr.review === "APPROVED")) mergeParts.push("ready to merge");
|
|
873
894
|
else if (pr.review === "REVIEW_REQUIRED") mergeParts.push("blocked: review required");
|
|
895
|
+
} else if (info?.defaultBranch === true) {
|
|
896
|
+
// The default branch is the BASE, not a branch with a missing PR:
|
|
897
|
+
// pull requests merge INTO it and none is ever opened FROM it, so
|
|
898
|
+
// "no pull request" here reads as a nag for something that cannot
|
|
899
|
+
// exist. The node half computes `defaultBranch` for exactly this
|
|
900
|
+
// suppression (see the comment where it is set in lib/index.js);
|
|
901
|
+
// until now only the action rules consulted it, not this row.
|
|
902
|
+
mergeParts.push("base branch \u00B7 pull requests merge into this");
|
|
874
903
|
} else {
|
|
875
904
|
mergeParts.push("no pull request");
|
|
876
905
|
}
|
|
877
|
-
|
|
906
|
+
// The counts are this branch's standing against its BASE, so on the
|
|
907
|
+
// base itself they are self-referential ("N ahead, M behind main" while
|
|
908
|
+
// ON main) and the divergence they would state is already the chip's own
|
|
909
|
+
// \u2191a \u2193b token. Suppress them on the default branch, with or without a PR.
|
|
910
|
+
if (info?.defaultBranch !== true && (info?.mainAhead !== void 0 || info?.mainBehind !== void 0)) {
|
|
878
911
|
mergeParts.push((info.mainAhead || 0) + " ahead, " + (info.mainBehind || 0) + " behind main");
|
|
879
912
|
}
|
|
880
913
|
add("merge", mergeParts.join(" \u00B7 "));
|
|
@@ -1009,6 +1042,24 @@ window.__ModuleLoader__.load({
|
|
|
1009
1042
|
}
|
|
1010
1043
|
|
|
1011
1044
|
//#region composer chip (upstream additive surface: conversation.input.left)
|
|
1045
|
+
/**
|
|
1046
|
+
* The ONE copy of the hover-card degrade rule: wrap a node in the shell's
|
|
1047
|
+
* Tooltip when the primitive exists, and render the node bare otherwise — a
|
|
1048
|
+
* missing card is a smaller failure than a missing badge. `label` is the
|
|
1049
|
+
* thunk the Tooltip calls only when it opens, so the card's element tree
|
|
1050
|
+
* stays out of closed tooltips.
|
|
1051
|
+
*
|
|
1052
|
+
* ALWAYS mount the wrapper — never conditionally, once its data arrives.
|
|
1053
|
+
* The primitive is uncontrolled (it has no `open` prop) and opens on the
|
|
1054
|
+
* CHILD's mouseenter, so a wrapper mounted after the pointer is already
|
|
1055
|
+
* inside can never open for that visit: the pointer has to leave and
|
|
1056
|
+
* return. Gate the LABEL's content instead.
|
|
1057
|
+
*/
|
|
1058
|
+
function withTooltip(node, label, maxWidth) {
|
|
1059
|
+
if (Tooltip === void 0) return node;
|
|
1060
|
+
return react_jsx_runtime.jsx(Tooltip, { side: "top", maxWidth, label, children: node });
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1012
1063
|
/**
|
|
1013
1064
|
* Chip line in the input row: git state of the workspace the CURRENT
|
|
1014
1065
|
* conversation is attached to. It targets the session id and lets the node
|
|
@@ -1044,7 +1095,7 @@ window.__ModuleLoader__.load({
|
|
|
1044
1095
|
const target = sessionId === void 0 ? void 0 : { kind: "session", id: sessionId };
|
|
1045
1096
|
// the PR/CI token is always on the chip, so its fetch is not gated
|
|
1046
1097
|
const info = useGitStatus(target, { pr: true });
|
|
1047
|
-
// The card's extras cost a `log -
|
|
1098
|
+
// The card's extras cost a `log -10` plus a stash list, so they are
|
|
1048
1099
|
// fetched only once a pointer actually RESTS on the chip, then kept
|
|
1049
1100
|
// fresh by the same SSE path. Eagerly asking would add both invocations
|
|
1050
1101
|
// to every refresh — and a refresh fires on every file edit.
|
|
@@ -1069,31 +1120,25 @@ window.__ModuleLoader__.load({
|
|
|
1069
1120
|
// so the extras are paid for exactly when the card that can render them
|
|
1070
1121
|
// is about to open. Defined inside the component: it closes over the
|
|
1071
1122
|
// hovered state that gates the detail fetch.
|
|
1072
|
-
const hoverableMark = (
|
|
1073
|
-
|
|
1123
|
+
const hoverableMark = withTooltip(
|
|
1124
|
+
react_jsx_runtime.jsx("span", {
|
|
1074
1125
|
onPointerEnter: () => setHovered(true),
|
|
1075
1126
|
style: { display: "inline-flex", alignItems: "center", flex: "none", cursor: "default" },
|
|
1076
1127
|
children: react_jsx_runtime.jsx(StatusMark, { info })
|
|
1077
|
-
})
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
// a function label keeps the card's element tree out of every render
|
|
1084
|
-
// until the tooltip actually opens
|
|
1085
|
-
label: () => react_jsx_runtime.jsx(HoverCard, { info, detail }),
|
|
1086
|
-
children: mark
|
|
1087
|
-
});
|
|
1088
|
-
})();
|
|
1128
|
+
}),
|
|
1129
|
+
// a function label keeps the card's element tree out of every render
|
|
1130
|
+
// until the tooltip actually opens
|
|
1131
|
+
() => react_jsx_runtime.jsx(HoverCard, { info, detail }),
|
|
1132
|
+
600 // matches the body cap: the tooltip must never be the clamp
|
|
1133
|
+
);
|
|
1089
1134
|
// the mark is an element now rather than a leading glyph in the string, so
|
|
1090
1135
|
// the SAME StatusMark the sidebar row draws carries the status here too;
|
|
1091
1136
|
// the container's 4px gap supplies the space the emoji's own did
|
|
1092
1137
|
// The branch NAME is its own hover surface: the commits this branch adds
|
|
1093
1138
|
// (BranchCommitsList), sharing the mark's lazy detail fetch. The rest of
|
|
1094
1139
|
// the leading text (operation token) and the count keep their own
|
|
1095
|
-
// surfaces.
|
|
1096
|
-
//
|
|
1140
|
+
// surfaces. The wrapper is always mounted (see withTooltip); without the
|
|
1141
|
+
// Tooltip primitive the name renders plain.
|
|
1097
1142
|
const branchHover = (() => {
|
|
1098
1143
|
// The branch name links to the compare view (base...branch) — the web
|
|
1099
1144
|
// page of exactly what the lineage tooltip lists. Vouched http(s)
|
|
@@ -1106,14 +1151,21 @@ window.__ModuleLoader__.load({
|
|
|
1106
1151
|
// count stays its own surface — it answers a different question.
|
|
1107
1152
|
const sync = formatSync(info);
|
|
1108
1153
|
const label = info.branch + sync;
|
|
1109
|
-
|
|
1154
|
+
// The Tooltip's CHILD must not change element type once the detail
|
|
1155
|
+
// payload lands: the primitive opens on the child's mouseenter and has
|
|
1156
|
+
// no `open` prop, so a node replaced under the pointer may never be
|
|
1157
|
+
// re-entered. The outer span is the stable hover surface; only this
|
|
1158
|
+
// inner node swaps (plain text -> anchor) as the compare URL arrives.
|
|
1159
|
+
const inner = isLink
|
|
1110
1160
|
? react_jsx_runtime.jsx("a", {
|
|
1111
1161
|
href: detail.compareUrl,
|
|
1112
1162
|
target: "_blank",
|
|
1113
1163
|
rel: "noopener noreferrer",
|
|
1114
1164
|
"aria-label": "Compare " + info.branch + " with main on GitHub" + (sync === "" ? "" : ", " + (info.ahead || 0) + " ahead and " + (info.behind || 0) + " behind upstream"),
|
|
1115
|
-
onPointerEnter: () =>
|
|
1165
|
+
onPointerEnter: () => setBranchLinkHover(true),
|
|
1116
1166
|
onPointerLeave: () => setBranchLinkHover(false),
|
|
1167
|
+
// focus joins hover: a keyboard user needs the same underline, and
|
|
1168
|
+
// focus (unlike a pointer) must also start the detail fetch
|
|
1117
1169
|
onFocus: () => { setHovered(true); setBranchLinkHover(true); },
|
|
1118
1170
|
onBlur: () => setBranchLinkHover(false),
|
|
1119
1171
|
style: {
|
|
@@ -1123,22 +1175,17 @@ window.__ModuleLoader__.load({
|
|
|
1123
1175
|
},
|
|
1124
1176
|
children: label
|
|
1125
1177
|
})
|
|
1126
|
-
:
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
if (Tooltip === void 0 || detail === void 0 || detail === null) {
|
|
1132
|
-
// mid-fetch the branch shows plain; the lineage tooltip is
|
|
1133
|
-
// unconditional once the payload lands
|
|
1134
|
-
return branch;
|
|
1135
|
-
}
|
|
1136
|
-
return react_jsx_runtime.jsx(Tooltip, {
|
|
1137
|
-
side: "top",
|
|
1138
|
-
maxWidth: 480,
|
|
1139
|
-
label: () => react_jsx_runtime.jsx(BranchLineage, { info: detail }),
|
|
1140
|
-
children: branch
|
|
1178
|
+
: label;
|
|
1179
|
+
const branch = react_jsx_runtime.jsx("span", {
|
|
1180
|
+
onPointerEnter: () => setHovered(true),
|
|
1181
|
+
style: { cursor: isLink ? "pointer" : "default" },
|
|
1182
|
+
children: inner
|
|
1141
1183
|
});
|
|
1184
|
+
// ALWAYS wrapped. A wrapper mounted only once the lazy detail arrives
|
|
1185
|
+
// cannot open for the visit that fetched it (see the child-stability
|
|
1186
|
+
// note above); until the payload lands the lineage degrades to the base
|
|
1187
|
+
// response, the way the mark's own card already does.
|
|
1188
|
+
return withTooltip(branch, () => react_jsx_runtime.jsx(BranchLineage, { info: detail ?? info }), 480);
|
|
1142
1189
|
})();
|
|
1143
1190
|
const text = formatOperationToken(info);
|
|
1144
1191
|
// The ✎n count is its OWN hover surface: a names-only tooltip (see
|
|
@@ -1153,17 +1200,12 @@ window.__ModuleLoader__.load({
|
|
|
1153
1200
|
style: { cursor: "default" },
|
|
1154
1201
|
children: countText
|
|
1155
1202
|
});
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
return react_jsx_runtime.jsx(
|
|
1162
|
-
side: "top",
|
|
1163
|
-
maxWidth: 480,
|
|
1164
|
-
label: () => react_jsx_runtime.jsx(CountNames, { info: detail }),
|
|
1165
|
-
children: count
|
|
1166
|
-
});
|
|
1203
|
+
// always wrapped, for the same reason as the branch: the primitive
|
|
1204
|
+
// opens on the child's mouseenter, so a wrapper added after the
|
|
1205
|
+
// pointer arrives never opens. While the names are in flight the label
|
|
1206
|
+
// restates the breakdown the base response already carries.
|
|
1207
|
+
const pending = detail === void 0 || detail === null;
|
|
1208
|
+
return withTooltip(count, () => react_jsx_runtime.jsx(CountNames, { info: detail ?? info, pending }), 480);
|
|
1167
1209
|
})();
|
|
1168
1210
|
const prToken = formatPrToken(info);
|
|
1169
1211
|
const prUrl = prLinkUrl(info);
|
|
@@ -1211,16 +1253,10 @@ window.__ModuleLoader__.load({
|
|
|
1211
1253
|
onBlur: () => setLinkHover(false),
|
|
1212
1254
|
children: prToken
|
|
1213
1255
|
});
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
}
|
|
1218
|
-
return react_jsx_runtime.jsx(Tooltip, {
|
|
1219
|
-
side: "top",
|
|
1220
|
-
maxWidth: 480,
|
|
1221
|
-
label: () => react_jsx_runtime.jsx(PrCommitsList, { info: detail }),
|
|
1222
|
-
children: token
|
|
1223
|
-
});
|
|
1256
|
+
// always wrapped (see the branch): while the commits are in flight the
|
|
1257
|
+
// label names the PR state the base response already carries
|
|
1258
|
+
const pending = detail === void 0 || detail === null;
|
|
1259
|
+
return withTooltip(token, () => react_jsx_runtime.jsx(PrCommitsList, { info: detail ?? info, pending }), 480);
|
|
1224
1260
|
})();
|
|
1225
1261
|
const chip = react_jsx_runtime.jsxs("span", {
|
|
1226
1262
|
style: {
|