dsh-git-badge 0.20.0 → 0.20.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/lib/client.js +219 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,22 @@ npm. 0.6.0 is the first release recorded here.
|
|
|
10
10
|
|
|
11
11
|
## [Unreleased]
|
|
12
12
|
|
|
13
|
+
## [0.20.1] - 2026-10-13
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- **The row's token survives a status dot.** Upstream mounts the leading seat
|
|
18
|
+
only while a row's primary state is idle, and gives the 16px cell to its own
|
|
19
|
+
solid status dot otherwise: green for an unread completion, orange for a
|
|
20
|
+
pending interaction. A busy session that went idle therefore showed the dot
|
|
21
|
+
until the session was opened, while its checkout already had something to say.
|
|
22
|
+
The plugin now rides the row-action strip — the one per-row seat React mounts
|
|
23
|
+
in every state, hidden by CSS rather than unmounted — and paints the node
|
|
24
|
+
half's token over the dot from that marker plus one injected stylesheet. The
|
|
25
|
+
cell still decides: the fallback appears only when a solid dot occupies it, a
|
|
26
|
+
running spinner is left alone, `in sync` never replaces the dot, and the mark
|
|
27
|
+
is removed the moment the real seat returns. No upstream byte changes.
|
|
28
|
+
|
|
13
29
|
## [0.20.0] - 2026-10-13
|
|
14
30
|
|
|
15
31
|
### Added
|
package/lib/client.js
CHANGED
|
@@ -595,6 +595,9 @@ window.__ModuleLoader__.load({
|
|
|
595
595
|
return react_jsx_runtime.jsx("span", {
|
|
596
596
|
role: "img",
|
|
597
597
|
"aria-label": "git action: " + token,
|
|
598
|
+
// The fallback overlay in the SAME cell is suppressed by this
|
|
599
|
+
// marker, so a status transition can never paint two glyphs.
|
|
600
|
+
"data-git-badge-leading": "",
|
|
598
601
|
style: { ...META_STYLE, ...(colour === void 0 ? {} : { color: colour }), ...GLYPH_TEXT, display: "inline-flex", alignItems: "center" },
|
|
599
602
|
children: actionGlyphNode(token)
|
|
600
603
|
});
|
|
@@ -602,6 +605,207 @@ window.__ModuleLoader__.load({
|
|
|
602
605
|
return null;
|
|
603
606
|
}
|
|
604
607
|
|
|
608
|
+
//#region leading-cell fallback (a solid status dot owns the cell)
|
|
609
|
+
/**
|
|
610
|
+
* The leading cell's SOLID status dots. Upstream replaces the whole
|
|
611
|
+
* `sidebar.session.row.leading` seat with one of these whenever the row's
|
|
612
|
+
* primary state is not idle, so the plugin's glyph is not mounted at all:
|
|
613
|
+
* `done` (green) when a background turn finished unread, `warning` (orange)
|
|
614
|
+
* when a pending interaction waits. `ongoing` is the running spinner, an
|
|
615
|
+
* `svg` with no solid dot, and stays upstream's alone.
|
|
616
|
+
*/
|
|
617
|
+
const ROW_FALLBACK_DOT = '[data-state="done"], [data-state="warning"]';
|
|
618
|
+
|
|
619
|
+
/**
|
|
620
|
+
* The fallback's stylesheet. The plugin paints the token into the occupied
|
|
621
|
+
* 16px cell from a marker attribute, so it adds NO foreign node to a
|
|
622
|
+
* React-managed cell: the dot element steps aside and a `::after` draws the
|
|
623
|
+
* node half's glyph, coloured by the severity custom property.
|
|
624
|
+
*
|
|
625
|
+
* `:has()` keeps the real seat and the fallback exclusive even during the
|
|
626
|
+
* one commit where a row flips back to idle: a cell that already holds
|
|
627
|
+
* `data-git-badge-leading` never paints the overlay. A browser without
|
|
628
|
+
* `:has()` drops the whole rule, so the fallback is simply absent — it can
|
|
629
|
+
* never double-draw.
|
|
630
|
+
*/
|
|
631
|
+
const ROW_FALLBACK_OVERLAY = "[data-git-badge-overlay]:not(:has([data-git-badge-leading]))";
|
|
632
|
+
const ROW_FALLBACK_CSS = [
|
|
633
|
+
ROW_FALLBACK_OVERLAY + " > span:first-child > " + ROW_FALLBACK_DOT + " { display: none; }",
|
|
634
|
+
ROW_FALLBACK_OVERLAY + " > span:first-child::after {",
|
|
635
|
+
' content: var(--git-badge-glyph, "");',
|
|
636
|
+
" display: inline-flex;",
|
|
637
|
+
" align-items: center;",
|
|
638
|
+
" justify-content: center;",
|
|
639
|
+
" width: 16px;",
|
|
640
|
+
" height: 20px;",
|
|
641
|
+
" color: var(--git-badge-color, currentColor);",
|
|
642
|
+
" font-size: 13px;",
|
|
643
|
+
" line-height: 1;",
|
|
644
|
+
"}",
|
|
645
|
+
ROW_FALLBACK_OVERLAY + "[data-git-badge-drawn] > span:first-child::after {",
|
|
646
|
+
' content: "";',
|
|
647
|
+
" background-color: var(--git-badge-color, currentColor);",
|
|
648
|
+
" -webkit-mask-image: var(--git-badge-mask-src);",
|
|
649
|
+
" mask-image: var(--git-badge-mask-src);",
|
|
650
|
+
" -webkit-mask-repeat: no-repeat;",
|
|
651
|
+
" mask-repeat: no-repeat;",
|
|
652
|
+
" -webkit-mask-position: center;",
|
|
653
|
+
" mask-position: center;",
|
|
654
|
+
" -webkit-mask-size: var(--git-badge-mask-size, 12px);",
|
|
655
|
+
" mask-size: var(--git-badge-mask-size, 12px);",
|
|
656
|
+
"}"
|
|
657
|
+
].join("\n");
|
|
658
|
+
|
|
659
|
+
/**
|
|
660
|
+
* One `DRAWN_ICONS` entry as a CSS mask URL, so the fallback paints the
|
|
661
|
+
* EXACT artwork the real seat draws. The mask supplies the shape and the
|
|
662
|
+
* element's `background-color` supplies the severity colour, which is why
|
|
663
|
+
* the SVG is painted black: only its alpha is read.
|
|
664
|
+
*/
|
|
665
|
+
function glyphMaskDataUri(icon) {
|
|
666
|
+
const shapes = icon.shapes.map((shape) => {
|
|
667
|
+
if (shape.circle === true) return '<circle cx="6" cy="6" r="5"/>';
|
|
668
|
+
if (shape.stroke === true) return '<path d="' + shape.d + '" fill="none" stroke="#000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>';
|
|
669
|
+
return '<path d="' + shape.d + '" fill="#000"/>';
|
|
670
|
+
}).join("");
|
|
671
|
+
const svg = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="' + icon.viewBox + '">' + shapes + "</svg>";
|
|
672
|
+
return 'url("data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svg) + '")';
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
/**
|
|
676
|
+
* The fallback's mark for a token, or undefined when the dot should stay.
|
|
677
|
+
*
|
|
678
|
+
* `in sync` is deliberately excluded: it is a disc, the row's own dot is
|
|
679
|
+
* also a dot, and replacing an unread marker with "nothing to do" would
|
|
680
|
+
* throw away the only signal that the turn ended. Only a real action earns
|
|
681
|
+
* the cell.
|
|
682
|
+
*/
|
|
683
|
+
function fallbackGlyph(token, severity) {
|
|
684
|
+
if (typeof token !== "string" || token === "" || token === "in sync") return void 0;
|
|
685
|
+
const colour = SEVERITY_FILL[severity];
|
|
686
|
+
const drawn = DRAWN_ICONS[token];
|
|
687
|
+
if (drawn !== void 0) return { mask: glyphMaskDataUri(drawn), size: drawn.size + "px", colour };
|
|
688
|
+
const glyph = ACTION_GLYPH[token];
|
|
689
|
+
if (glyph === void 0) return void 0;
|
|
690
|
+
return { glyph, colour };
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
/** Write one mark onto the row element. */
|
|
694
|
+
function applyFallbackMark(host, mark) {
|
|
695
|
+
host.setAttribute("data-git-badge-overlay", "");
|
|
696
|
+
if (mark.mask !== void 0) {
|
|
697
|
+
host.setAttribute("data-git-badge-drawn", "");
|
|
698
|
+
host.style.setProperty("--git-badge-mask-src", mark.mask);
|
|
699
|
+
host.style.setProperty("--git-badge-mask-size", mark.size);
|
|
700
|
+
host.style.removeProperty("--git-badge-glyph");
|
|
701
|
+
} else {
|
|
702
|
+
host.removeAttribute("data-git-badge-drawn");
|
|
703
|
+
host.style.removeProperty("--git-badge-mask-src");
|
|
704
|
+
host.style.removeProperty("--git-badge-mask-size");
|
|
705
|
+
// JSON.stringify keeps the quotes `content: var(...)` needs.
|
|
706
|
+
host.style.setProperty("--git-badge-glyph", JSON.stringify(mark.glyph));
|
|
707
|
+
}
|
|
708
|
+
if (mark.colour === void 0) host.style.removeProperty("--git-badge-color");
|
|
709
|
+
else host.style.setProperty("--git-badge-color", mark.colour);
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
/** Remove every trace of a mark, so the row is exactly as upstream left it. */
|
|
713
|
+
function clearFallbackMark(host) {
|
|
714
|
+
if (host === void 0 || host === null) return;
|
|
715
|
+
host.removeAttribute("data-git-badge-overlay");
|
|
716
|
+
host.removeAttribute("data-git-badge-drawn");
|
|
717
|
+
host.style.removeProperty("--git-badge-glyph");
|
|
718
|
+
host.style.removeProperty("--git-badge-mask-src");
|
|
719
|
+
host.style.removeProperty("--git-badge-mask-size");
|
|
720
|
+
host.style.removeProperty("--git-badge-color");
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
/** The row (`[data-row-key]`) an element sits in, or undefined. */
|
|
724
|
+
function rowHost(node) {
|
|
725
|
+
const closest = node?.closest;
|
|
726
|
+
if (typeof closest !== "function") return void 0;
|
|
727
|
+
return closest.call(node, "[data-row-key]") ?? void 0;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
/**
|
|
731
|
+
* The whole fallback decision in one testable call: does the cell hold a
|
|
732
|
+
* SOLID dot, and does the node half have an action to show? The cell is the
|
|
733
|
+
* authority, so no client-side copy of upstream's status precedence can
|
|
734
|
+
* drift. Returns whether the overlay is on.
|
|
735
|
+
*/
|
|
736
|
+
function syncRowFallback(host, info) {
|
|
737
|
+
if (host === void 0 || host === null) return false;
|
|
738
|
+
const cell = host.firstElementChild;
|
|
739
|
+
const occupied = cell !== void 0 && cell !== null && typeof cell.querySelector === "function" && cell.querySelector(ROW_FALLBACK_DOT) !== null;
|
|
740
|
+
if (!occupied) {
|
|
741
|
+
clearFallbackMark(host);
|
|
742
|
+
return false;
|
|
743
|
+
}
|
|
744
|
+
const token = info !== void 0 && info !== null && info.git === true ? info.rowToken : void 0;
|
|
745
|
+
const mark = fallbackGlyph(token, info?.rowSeverity);
|
|
746
|
+
if (mark === void 0) {
|
|
747
|
+
clearFallbackMark(host);
|
|
748
|
+
return false;
|
|
749
|
+
}
|
|
750
|
+
applyFallbackMark(host, mark);
|
|
751
|
+
return true;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
/** One stylesheet per page; the loader tags it for its own bookkeeping. */
|
|
755
|
+
let rowFallbackStyle = false;
|
|
756
|
+
function ensureRowFallbackStyle() {
|
|
757
|
+
if (rowFallbackStyle) return;
|
|
758
|
+
if (typeof document === "undefined" || document.head === void 0 || document.head === null) return;
|
|
759
|
+
try {
|
|
760
|
+
const style = document.createElement("style");
|
|
761
|
+
style.setAttribute("data-plugin", "dsh-git-badge");
|
|
762
|
+
style.textContent = ROW_FALLBACK_CSS;
|
|
763
|
+
document.head.appendChild(style);
|
|
764
|
+
rowFallbackStyle = true;
|
|
765
|
+
} catch {
|
|
766
|
+
// a missing or closed <head> must never break the badge
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
/**
|
|
771
|
+
* The marker that carries the fallback. It MUST ride
|
|
772
|
+
* `sidebar.workspaces.session.row.action`, because that strip is hidden by
|
|
773
|
+
* CSS rather than unmounted: it is the one per-row seat React mounts in
|
|
774
|
+
* every state, so it is the only place from which a row can be marked while
|
|
775
|
+
* upstream's dot owns the leading cell. It renders a hidden span and adds
|
|
776
|
+
* no button.
|
|
777
|
+
*/
|
|
778
|
+
function RowFallbackGlyph({ sessionId }) {
|
|
779
|
+
const ref = react.useRef(null);
|
|
780
|
+
const [occupied, setOccupied] = react.useState(false);
|
|
781
|
+
// `enabled` is the LAZY gate: a row with no solid dot keeps costing
|
|
782
|
+
// nothing, exactly as it did before the fallback existed.
|
|
783
|
+
const info = useGitStatus(sessionId === void 0 ? void 0 : { kind: "session", id: sessionId }, { pr: true, enabled: occupied });
|
|
784
|
+
react.useEffect(() => {
|
|
785
|
+
ensureRowFallbackStyle();
|
|
786
|
+
const host = rowHost(ref.current);
|
|
787
|
+
if (host === void 0) return;
|
|
788
|
+
const cell = host.firstElementChild;
|
|
789
|
+
if (cell === void 0 || cell === null || typeof cell.querySelector !== "function") return;
|
|
790
|
+
const sync = () => setOccupied(cell.querySelector(ROW_FALLBACK_DOT) !== null);
|
|
791
|
+
sync();
|
|
792
|
+
if (typeof MutationObserver !== "function") return;
|
|
793
|
+
const observer = new MutationObserver(sync);
|
|
794
|
+
// The dot swaps in as a child; a state change edits `data-state` in
|
|
795
|
+
// place, so both must be watched. The row's own attributes are NOT
|
|
796
|
+
// observed, which is what keeps the mark from re-triggering itself.
|
|
797
|
+
observer.observe(cell, { childList: true, subtree: true, attributes: true, attributeFilter: ["data-state"] });
|
|
798
|
+
return () => observer.disconnect();
|
|
799
|
+
}, [sessionId]);
|
|
800
|
+
// Layout effect: the mark lands in the same commit that reveals the dot,
|
|
801
|
+
// so the fallback does not flash the green/orange dot first.
|
|
802
|
+
react.useLayoutEffect(() => {
|
|
803
|
+
syncRowFallback(rowHost(ref.current), info);
|
|
804
|
+
}, [info, occupied]);
|
|
805
|
+
return react_jsx_runtime.jsx("span", { ref, "aria-hidden": true, style: { display: "none" } });
|
|
806
|
+
}
|
|
807
|
+
//#endregion
|
|
808
|
+
|
|
605
809
|
const CARD_CONTAINER = {
|
|
606
810
|
display: "flex",
|
|
607
811
|
flexDirection: "column",
|
|
@@ -1739,6 +1943,20 @@ window.__ModuleLoader__.load({
|
|
|
1739
1943
|
name: "sidebar.session.row.leading",
|
|
1740
1944
|
id: "git-badge-row-leading"
|
|
1741
1945
|
}, LeadingTokenGlyph));
|
|
1946
|
+
// The seat above is mounted ONLY while the row's primary state is idle
|
|
1947
|
+
// (upstream's own contract). A row that owns a solid status dot — an
|
|
1948
|
+
// unread completion, or a pending interaction — keeps that cell to
|
|
1949
|
+
// itself, so the token glyph would be invisible exactly when the turn
|
|
1950
|
+
// that produced it just ended, until the session is opened. This strip is
|
|
1951
|
+
// ALWAYS mounted (hidden by CSS, not unmounted), so a marker rides it and
|
|
1952
|
+
// the stylesheet paints the token over the dot. The rule stays honest: no
|
|
1953
|
+
// upstream byte changes, and the cell still belongs to upstream.
|
|
1954
|
+
ctx.slots.inject("sidebar.workspaces.session.row.action", () => ctx.slots.register({
|
|
1955
|
+
name: "sidebar.workspaces.session.row.action",
|
|
1956
|
+
id: "git-badge-row-fallback",
|
|
1957
|
+
order: 300
|
|
1958
|
+
}, RowFallbackGlyph));
|
|
1959
|
+
ensureRowFallbackStyle();
|
|
1742
1960
|
// The row is ALREADY a HoverCard anchor upstream, so a glyph that opened
|
|
1743
1961
|
// its own tooltip would nest two cards. This renders the explanation
|
|
1744
1962
|
// inside that card instead, through the hover seat upstream declares
|
|
@@ -1779,7 +1997,7 @@ window.__ModuleLoader__.load({
|
|
|
1779
1997
|
// Additive; the host reads apply/inject and ignores the rest. The suite
|
|
1780
1998
|
// drives these to assert the REQUEST contract — which surface asks for the
|
|
1781
1999
|
// expensive extras — without a browser, a fetch or a network.
|
|
1782
|
-
exports.__internals = { targetQuery, worktreeDetail, linkedWorktreesDetail, ghSkillActions, cleanGate, moveCategory };
|
|
2000
|
+
exports.__internals = { targetQuery, worktreeDetail, linkedWorktreesDetail, ghSkillActions, cleanGate, moveCategory, fallbackGlyph, glyphMaskDataUri, syncRowFallback, clearFallbackMark, ROW_FALLBACK_CSS, ROW_FALLBACK_DOT };
|
|
1783
2001
|
return module.exports;
|
|
1784
2002
|
}
|
|
1785
2003
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-git-badge",
|
|
3
|
-
"version": "0.20.
|
|
3
|
+
"version": "0.20.1",
|
|
4
4
|
"description": "Git status badges for DeepSeek Harness: an input-row chip and a sidebar session-row action glyph, on every install — no patch and no manual step. Event-driven freshness over SSE.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|