dsh-git-badge 0.14.1 → 0.16.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 +165 -0
- package/README.md +2 -2
- package/lib/client.js +604 -72
- package/lib/index.js +387 -187
- package/package.json +8 -3
- package/seam/checkout.js +82 -0
- package/seam/store.js +66 -1
- package/skill/gh/SKILL.md +46 -0
package/lib/client.js
CHANGED
|
@@ -233,41 +233,42 @@ window.__ModuleLoader__.load({
|
|
|
233
233
|
|
|
234
234
|
/**
|
|
235
235
|
* The /gh picker's sub-actions — the skill invocations the checkout
|
|
236
|
-
* justifies, most urgent first.
|
|
237
|
-
*
|
|
238
|
-
*
|
|
239
|
-
*
|
|
236
|
+
* justifies, most urgent first. The server's `next` (the ranked top rule,
|
|
237
|
+
* already carrying args/why/what) always leads; the client adds the other
|
|
238
|
+
* justified actions as secondary entries, mirroring the server's category
|
|
239
|
+
* rules but never re-deciding the winner.
|
|
240
240
|
*/
|
|
241
241
|
function ghSkillActions(info) {
|
|
242
242
|
if (info === void 0 || info === null || info.git !== true) return [];
|
|
243
243
|
const subs = [];
|
|
244
244
|
const seen = new Set();
|
|
245
|
-
const add = (args, why) => {
|
|
246
|
-
if (seen.has(args)) return;
|
|
245
|
+
const add = (args, why, what) => {
|
|
246
|
+
if (args === void 0 || args === null || args === "" || seen.has(args)) return;
|
|
247
247
|
seen.add(args);
|
|
248
|
-
subs.push({ args, why });
|
|
248
|
+
subs.push({ args, why, what });
|
|
249
249
|
};
|
|
250
|
+
const next = info.next;
|
|
251
|
+
if (next !== void 0 && next !== null) add(next.args, next.why, next.what);
|
|
250
252
|
const ahead = info.ahead || 0;
|
|
251
253
|
const behind = info.behind || 0;
|
|
252
254
|
const staged = info.stagedFiles || 0;
|
|
253
255
|
const unstaged = info.unstagedFiles || 0;
|
|
254
256
|
const untracked = info.untrackedFiles || 0;
|
|
255
|
-
if (behind > 0
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
257
|
+
if (behind > 0 && ahead > 0) {
|
|
258
|
+
add("sync", ahead + " ahead, " + behind + " behind — diverged", "rebase your commits onto upstream and push (asks before any force)");
|
|
259
|
+
} else {
|
|
260
|
+
if (behind > 0) add("pull", behind + " behind " + (info.upstream ?? "upstream"), "update this branch from upstream");
|
|
261
|
+
if (ahead > 0) add("push", ahead + " ahead of " + (info.upstream ?? "upstream"), "push local commits to the remote");
|
|
262
|
+
}
|
|
263
|
+
if (staged > 0) add("commit", staged + " staged", "commit the staged changes");
|
|
264
|
+
if (unstaged > 0) add("commit", unstaged + " unstaged", "stage and commit the working changes");
|
|
265
|
+
if (unstaged === 0 && untracked > 0) add("commit", untracked + " untracked", "stage the new files and commit them");
|
|
260
266
|
const pr = info.pr;
|
|
261
267
|
if (pr !== void 0 && pr !== null && pr.number !== void 0) {
|
|
262
|
-
if (pr.state === "failing") add("checks " + pr.number, "checks failing on #" + pr.number);
|
|
263
|
-
add("pr view " + pr.number, "open pull request #" + pr.number);
|
|
268
|
+
if (pr.state === "failing") add("checks " + pr.number, "checks failing on #" + pr.number, "watch the CI checks on pull request " + pr.number);
|
|
269
|
+
add("pr view " + pr.number, "open pull request #" + pr.number, "show pull request " + pr.number + " on GitHub");
|
|
264
270
|
} else if (ahead === 0 && behind === 0 && info.upstream !== void 0 && staged + unstaged + untracked === 0) {
|
|
265
|
-
add("pr", "branch is pushed and has no pull request");
|
|
266
|
-
}
|
|
267
|
-
const next = info.next;
|
|
268
|
-
if (next !== void 0 && next !== null && typeof next.command === "string"
|
|
269
|
-
&& !subs.some((entry) => entry.why === next.why)) {
|
|
270
|
-
add("next", next.why);
|
|
271
|
+
add("pr", "branch is pushed and has no pull request", "open a pull request for this branch");
|
|
271
272
|
}
|
|
272
273
|
return subs.slice(0, 6);
|
|
273
274
|
}
|
|
@@ -435,9 +436,9 @@ window.__ModuleLoader__.load({
|
|
|
435
436
|
"aria-label":
|
|
436
437
|
(isWorktree ? "git worktree: " : "git: ") +
|
|
437
438
|
STATUS_LABEL[status] +
|
|
438
|
-
//
|
|
439
|
-
// accessible name says
|
|
440
|
-
(info.
|
|
439
|
+
// a followed checkout is not where the conversation lives, and the
|
|
440
|
+
// accessible name says so — the session registered it explicitly
|
|
441
|
+
(info.worktreeFollowed === true ? " (registered for this session)" : ""),
|
|
441
442
|
focusable: "false",
|
|
442
443
|
style: { flex: "none", display: "block", color: META_STYLE.color }
|
|
443
444
|
};
|
|
@@ -584,7 +585,7 @@ window.__ModuleLoader__.load({
|
|
|
584
585
|
const checkout = [name === "" ? String(info.branch) : name + " on " + info.branch];
|
|
585
586
|
// the node half followed this tree because it is the one whose branch
|
|
586
587
|
// has the open PR — say so, because nothing on the row can
|
|
587
|
-
|
|
588
|
+
|
|
588
589
|
bits.push("checkout: " + checkout.join(" \u00B7 "));
|
|
589
590
|
}
|
|
590
591
|
const pr = formatPrDetail(info.pr);
|
|
@@ -622,7 +623,7 @@ window.__ModuleLoader__.load({
|
|
|
622
623
|
|
|
623
624
|
/**
|
|
624
625
|
* The `checkout` row: the directory the CONVERSATION itself names, present
|
|
625
|
-
* only when the badge is describing
|
|
626
|
+
* only when the badge is describing a FOLLOWED worktree instead. The chip's
|
|
626
627
|
* branch and counts are the tree's in that case, so without this row the
|
|
627
628
|
* checkout's own state is nowhere on screen. Branch, then files (or "clean"),
|
|
628
629
|
* then sync counts only when they are nonzero — the chip's own rule.
|
|
@@ -641,15 +642,13 @@ window.__ModuleLoader__.load({
|
|
|
641
642
|
* The `worktree` row: WHICH linked worktree the badge is describing. The chip
|
|
642
643
|
* names only the branch (see the note above formatOperationToken), so the
|
|
643
644
|
* card is the one place a tree is identified — the mark's shape can only say
|
|
644
|
-
* *that* the checkout is a worktree, never which.
|
|
645
|
-
* why, because "which tree" and "why this tree" are different questions and
|
|
646
|
-
* the second is the one a reader will ask.
|
|
645
|
+
* *that* the checkout is a worktree, never which.
|
|
647
646
|
*/
|
|
648
647
|
function worktreeDetail(info) {
|
|
649
648
|
if (info === void 0 || info === null || info.isWorktree !== true) return void 0;
|
|
650
649
|
const name = typeof info.worktreeName === "string" ? info.worktreeName : "";
|
|
651
650
|
if (name === "") return void 0;
|
|
652
|
-
return
|
|
651
|
+
return name;
|
|
653
652
|
}
|
|
654
653
|
|
|
655
654
|
const CARD_CONTAINER = {
|
|
@@ -658,12 +657,223 @@ window.__ModuleLoader__.load({
|
|
|
658
657
|
gap: "4px",
|
|
659
658
|
fontSize: "12px",
|
|
660
659
|
lineHeight: "18px",
|
|
661
|
-
|
|
660
|
+
// Auto-width: the card fits its widest row instead of a guessed
|
|
661
|
+
// constant, capped at 600px. The Tooltip's maxWidth must stay >= the
|
|
662
|
+
// cap or IT becomes the clamp.
|
|
663
|
+
width: "max-content",
|
|
664
|
+
maxWidth: "600px"
|
|
662
665
|
};
|
|
666
|
+
/**
|
|
667
|
+
* The ✎n count's own hover body — the changed-file lists and NOTHING
|
|
668
|
+
* else: the full card stays on the status mark, and this tooltip answers
|
|
669
|
+
* the one question the count poses ("what are these?"). One stacked
|
|
670
|
+
* column with sub-headers: UNSTAGED (tracked, edited — valid regardless
|
|
671
|
+
* of the collapsed untracked retry) then UNTRACKED. Reads the same lazy
|
|
672
|
+
* detail payload the mark's card uses; no tooltip at all when neither
|
|
673
|
+
* list has names (the count itself is absent on a clean tree, so an
|
|
674
|
+
* empty list is rare — a mid-fetch hover shows it plain until the
|
|
675
|
+
* response lands).
|
|
676
|
+
*/
|
|
677
|
+
function CountNames({ info }) {
|
|
678
|
+
const sections = [
|
|
679
|
+
{
|
|
680
|
+
label: "staged",
|
|
681
|
+
names: info?.stagedNames,
|
|
682
|
+
total: info?.stagedNamesTotal
|
|
683
|
+
},
|
|
684
|
+
{
|
|
685
|
+
label: "unstaged",
|
|
686
|
+
names: info?.unstagedNames,
|
|
687
|
+
total: info?.unstagedNamesTotal
|
|
688
|
+
},
|
|
689
|
+
{
|
|
690
|
+
label: "untracked",
|
|
691
|
+
names: info?.untrackedNames,
|
|
692
|
+
total: info?.untrackedNamesTotal
|
|
693
|
+
}
|
|
694
|
+
].filter((section) => Array.isArray(section.names) && section.names.length > 0);
|
|
695
|
+
if (sections.length === 0) {
|
|
696
|
+
return react_jsx_runtime.jsx("span", { style: CARD_CONTAINER, children: "no changed files" });
|
|
697
|
+
}
|
|
698
|
+
return react_jsx_runtime.jsxs("div", { style: CARD_CONTAINER, children:
|
|
699
|
+
sections.map((section) => react_jsx_runtime.jsxs("div", { style: CARD_ROW, children: [
|
|
700
|
+
react_jsx_runtime.jsx("span", { style: CARD_LABEL, children: section.label }),
|
|
701
|
+
react_jsx_runtime.jsx("span", {
|
|
702
|
+
style: { ...CARD_VALUE, display: "flex", flexDirection: "column", gap: "2px" },
|
|
703
|
+
children: [
|
|
704
|
+
...section.names.map((name) => react_jsx_runtime.jsx("span", { children: name }, section.label + name)),
|
|
705
|
+
(typeof section.total === "number" && section.total > section.names.length
|
|
706
|
+
? "\u2026 and " + (section.total - section.names.length) + " more"
|
|
707
|
+
: null)
|
|
708
|
+
]
|
|
709
|
+
})
|
|
710
|
+
] }, section.label))
|
|
711
|
+
});
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
/**
|
|
715
|
+
* Compress git's relative age to its shortest honest form: 43 seconds
|
|
716
|
+
* ago -> 43s ago, 2 hours ago -> 2h ago, weeks -> w, months -> mo (to
|
|
717
|
+
* stay distinct from minutes), years -> y.
|
|
718
|
+
*/
|
|
719
|
+
function shortWhen(when) {
|
|
720
|
+
return String(when)
|
|
721
|
+
.replace(/(\d+) seconds? ago/u, "$1s ago")
|
|
722
|
+
.replace(/(\d+) minutes? ago/u, "$1m ago")
|
|
723
|
+
.replace(/(\d+) hours? ago/u, "$1h ago")
|
|
724
|
+
.replace(/(\d+) days? ago/u, "$1d ago")
|
|
725
|
+
.replace(/(\d+) weeks? ago/u, "$1w ago")
|
|
726
|
+
.replace(/(\d+) months? ago/u, "$1mo ago")
|
|
727
|
+
.replace(/(\d+) years? ago/u, "$1y ago");
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
/**
|
|
731
|
+
* The branch name's own hover body — the branch's LINEAGE, and it ALWAYS
|
|
732
|
+
* renders once the lazy detail payload has landed. Rows: upstream, sync
|
|
733
|
+
* in words, the branch's last 10 commits (WHAT THIS BRANCH HAS, full
|
|
734
|
+
* stop — no base-relative filtering, which empties on every push or
|
|
735
|
+
* merge). The pull request has no row here — the chip's own PR token
|
|
736
|
+
* already carries number, CI state and the link.
|
|
737
|
+
*/
|
|
738
|
+
function BranchLineage({ info }) {
|
|
739
|
+
const rows = [];
|
|
740
|
+
const add = (label, value) => {
|
|
741
|
+
if (value === void 0 || value === null || value === "") return;
|
|
742
|
+
rows.push(cardRow(label, value));
|
|
743
|
+
};
|
|
744
|
+
add("upstream", info?.upstream === void 0 ? "none configured" : info.upstream);
|
|
745
|
+
const ahead = info?.ahead || 0;
|
|
746
|
+
const behind = info?.behind || 0;
|
|
747
|
+
// The sync row speaks ONLY about the upstream it names: with no
|
|
748
|
+
// upstream configured there are no ahead/behind counts at all (no
|
|
749
|
+
// `# branch.ab` header), and falling back to 0/0 would claim "in
|
|
750
|
+
// sync" — a lie the upstream row directly contradicts. Omit it; the
|
|
751
|
+
// "none configured" row above carries the fact.
|
|
752
|
+
if (info?.upstream !== void 0) {
|
|
753
|
+
add("sync", ahead === 0 && behind === 0
|
|
754
|
+
? "in sync with upstream"
|
|
755
|
+
: "\u2191" + ahead + " \u2193" + behind);
|
|
756
|
+
}
|
|
757
|
+
// The merge-state row, in WORDS (deliberately not arrows — the sync
|
|
758
|
+
// row above already uses ↑↓ for the push axis, and two arrow pairs
|
|
759
|
+
// with different references would be read as one). "merge" is its
|
|
760
|
+
// label because it is the merge axis — the counterpart of "sync",
|
|
761
|
+
// the push axis — and it stays correct whatever the default branch
|
|
762
|
+
// is named. A BLOCKED verdict becomes the reason (review required,
|
|
763
|
+
// checks failing, draft) so a dead-end word names the next action;
|
|
764
|
+
// CLEAN stays silent — the action row already offers /gh merge.
|
|
765
|
+
const pr = info?.pr;
|
|
766
|
+
const mergeParts = [];
|
|
767
|
+
if (pr !== void 0 && pr !== null && pr.number !== void 0) {
|
|
768
|
+
if (pr.draft === true) mergeParts.push("blocked: draft");
|
|
769
|
+
else if (pr.mergeState === "BLOCKED") {
|
|
770
|
+
mergeParts.push(pr.review === "REVIEW_REQUIRED" ? "blocked: review required"
|
|
771
|
+
: pr.state === "failing" ? "blocked: checks failing"
|
|
772
|
+
: "blocked");
|
|
773
|
+
} else if (pr.state === "failing") mergeParts.push("blocked: checks failing");
|
|
774
|
+
else if (pr.mergeState !== "CLEAN" && pr.review === "REVIEW_REQUIRED") mergeParts.push("blocked: review required");
|
|
775
|
+
}
|
|
776
|
+
if (info?.mainAhead !== void 0 || info?.mainBehind !== void 0) {
|
|
777
|
+
mergeParts.push((info.mainAhead || 0) + " ahead, " + (info.mainBehind || 0) + " behind main");
|
|
778
|
+
}
|
|
779
|
+
if (mergeParts.length > 0) add("merge", mergeParts.join(" \u00B7 "));
|
|
780
|
+
const commits = info?.branchCommits;
|
|
781
|
+
if (Array.isArray(commits) && commits.length > 0) {
|
|
782
|
+
// Full-width block, not the label/value two-column row: commit lines
|
|
783
|
+
// use the card's whole width, and each line is nowrap with CSS
|
|
784
|
+
// ellipsis so long subjects clip at the edge instead of pushing the
|
|
785
|
+
// card out. `when` is compressed the way git log users shorten it:
|
|
786
|
+
// 43 seconds ago -> 43s ago.
|
|
787
|
+
rows.push(
|
|
788
|
+
react_jsx_runtime.jsxs("div", {
|
|
789
|
+
style: { display: "flex", flexDirection: "column", gap: "2px", minWidth: 0 },
|
|
790
|
+
children:
|
|
791
|
+
commits.map((commit) => react_jsx_runtime.jsxs("span", {
|
|
792
|
+
style: { whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" },
|
|
793
|
+
children: [
|
|
794
|
+
// fixed-width monospace hash cell: every description starts
|
|
795
|
+
// on the same column, so the list reads as a table
|
|
796
|
+
react_jsx_runtime.jsx("span", {
|
|
797
|
+
style: {
|
|
798
|
+
color: "var(--dsw-alias-label-tertiary, #9ea7ad)",
|
|
799
|
+
fontFamily: "monospace",
|
|
800
|
+
display: "inline-block",
|
|
801
|
+
minWidth: "68px",
|
|
802
|
+
flex: "none"
|
|
803
|
+
},
|
|
804
|
+
children: (commit.sign === "+" ? "+" : commit.sign === "\u2212" ? "\u2212" : "") + commit.hash + ":"
|
|
805
|
+
}),
|
|
806
|
+
commit.subject + (commit.when === "" ? "" : " \u00B7 " + shortWhen(commit.when))
|
|
807
|
+
]
|
|
808
|
+
}, commit.hash))
|
|
809
|
+
}, "branch-commits")
|
|
810
|
+
);
|
|
811
|
+
}
|
|
812
|
+
// NO pull-request row: the chip's own PR token (number, CI state, link)
|
|
813
|
+
// carries it — a second rendering two pixels away is pure duplication.
|
|
814
|
+
return react_jsx_runtime.jsx("div", { style: CARD_CONTAINER, children: rows });
|
|
815
|
+
}
|
|
816
|
+
|
|
663
817
|
const CARD_ROW = { display: "flex", gap: "8px", alignItems: "baseline" };
|
|
664
818
|
const CARD_LABEL = { color: "var(--dsw-alias-label-tertiary, #9ea7ad)", flex: "none", minWidth: "62px" };
|
|
665
819
|
const CARD_VALUE = { minWidth: 0, overflowWrap: "anywhere" };
|
|
666
820
|
|
|
821
|
+
// The host's plugin-card chrome, reproduced inline: its CSS module is not
|
|
822
|
+
// exported to plugins, so these values mirror PluginCard.module.css so our
|
|
823
|
+
// card sits in the same list as its siblings.
|
|
824
|
+
const CARD_BASE = {
|
|
825
|
+
border: ".5px solid var(--dsw-alias-border-l4, #e4e8eb)",
|
|
826
|
+
borderRadius: "16px",
|
|
827
|
+
listStyle: "none",
|
|
828
|
+
transition: "border-color .16s, background .16s"
|
|
829
|
+
};
|
|
830
|
+
const CARD_CLOSED = { ...CARD_BASE, background: "var(--dsw-alias-bg-layer-3, #ffffff)" };
|
|
831
|
+
const CARD_OPEN = {
|
|
832
|
+
...CARD_BASE,
|
|
833
|
+
background: "var(--dsw-alias-bg-layer-2, #f7f8f9)",
|
|
834
|
+
borderColor: "var(--dsw-alias-label-dimmed, #b8bfc4)"
|
|
835
|
+
};
|
|
836
|
+
const CARD_HEADER = {
|
|
837
|
+
appearance: "none",
|
|
838
|
+
width: "100%",
|
|
839
|
+
font: "inherit",
|
|
840
|
+
color: "inherit",
|
|
841
|
+
textAlign: "left",
|
|
842
|
+
cursor: "pointer",
|
|
843
|
+
background: "none",
|
|
844
|
+
border: 0,
|
|
845
|
+
borderRadius: "12px",
|
|
846
|
+
display: "flex",
|
|
847
|
+
alignItems: "center",
|
|
848
|
+
gap: "12px",
|
|
849
|
+
padding: "14px 16px"
|
|
850
|
+
};
|
|
851
|
+
const CARD_NAME = { color: "var(--dsw-alias-label-primary, #1f2328)", fontSize: "15px", fontWeight: "600", lineHeight: "1.4" };
|
|
852
|
+
const CARD_DESCRIPTION = { color: "var(--dsw-alias-label-tertiary, #9ea7ad)", fontSize: "13px", lineHeight: "1.5" };
|
|
853
|
+
|
|
854
|
+
const CARD_BUTTON = {
|
|
855
|
+
cursor: "pointer",
|
|
856
|
+
color: "inherit",
|
|
857
|
+
background: "none",
|
|
858
|
+
border: "1px solid var(--dsw-alias-border-secondary, #d0d7de)",
|
|
859
|
+
borderRadius: "4px",
|
|
860
|
+
fontSize: "11px",
|
|
861
|
+
lineHeight: "18px",
|
|
862
|
+
padding: "0 6px",
|
|
863
|
+
flex: "none"
|
|
864
|
+
};
|
|
865
|
+
|
|
866
|
+
/** One label/value row — the shape every hover body shares. */
|
|
867
|
+
function cardRow(label, value) {
|
|
868
|
+
return react_jsx_runtime.jsxs("div", {
|
|
869
|
+
style: CARD_ROW,
|
|
870
|
+
children: [
|
|
871
|
+
react_jsx_runtime.jsx("span", { style: CARD_LABEL, children: label }),
|
|
872
|
+
react_jsx_runtime.jsx("span", { style: CARD_VALUE, children: value })
|
|
873
|
+
]
|
|
874
|
+
}, label);
|
|
875
|
+
}
|
|
876
|
+
|
|
667
877
|
|
|
668
878
|
/**
|
|
669
879
|
* Hover card body for the input chip — INPUT CHIP ONLY. Pure presentation
|
|
@@ -678,15 +888,7 @@ window.__ModuleLoader__.load({
|
|
|
678
888
|
const rows = [];
|
|
679
889
|
const add = (label, value) => {
|
|
680
890
|
if (value === void 0 || value === null || value === "") return;
|
|
681
|
-
rows.push(
|
|
682
|
-
react_jsx_runtime.jsxs("div", {
|
|
683
|
-
style: CARD_ROW,
|
|
684
|
-
children: [
|
|
685
|
-
react_jsx_runtime.jsx("span", { style: CARD_LABEL, children: label }),
|
|
686
|
-
react_jsx_runtime.jsx("span", { style: CARD_VALUE, children: value })
|
|
687
|
-
]
|
|
688
|
-
}, label)
|
|
689
|
-
);
|
|
891
|
+
rows.push(cardRow(label, value));
|
|
690
892
|
};
|
|
691
893
|
// The "action" row — the top line, in the card's ordinary row layout:
|
|
692
894
|
// the gh skill invocation the checkout justifies, derived from the same
|
|
@@ -695,45 +897,24 @@ window.__ModuleLoader__.load({
|
|
|
695
897
|
// this row is the read-only pointer to it.
|
|
696
898
|
const topAction = ghSkillActions(data)[0];
|
|
697
899
|
if (topAction !== void 0) {
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
add("upstream", info.upstream === void 0 ? "none configured" : info.upstream);
|
|
702
|
-
if (info.ahead !== void 0 || info.behind !== void 0) {
|
|
703
|
-
add("sync", "\u2191" + (info.ahead || 0) + " \u2193" + (info.behind || 0));
|
|
900
|
+
// git-comment convention: the invocation, two spaces, then "# "
|
|
901
|
+
// plus what it does
|
|
902
|
+
add("action", "/gh " + topAction.args + " # " + topAction.what);
|
|
704
903
|
}
|
|
904
|
+
// The branch lineage rows (branch, upstream, sync, pull request) moved
|
|
905
|
+
// to the branch NAME's hover — the card is the checkout's verdict, not
|
|
906
|
+
// the branch's CV. The branch name is also the chip's own text, so a
|
|
907
|
+
// card row for it was pure repetition.
|
|
705
908
|
add("files", formatFileBreakdown(info));
|
|
706
909
|
add("operation", info.operation === void 0 || info.operation === null ? void 0 : String(info.operation));
|
|
707
|
-
add("pull request", formatPrDetail(data.pr));
|
|
708
910
|
// WHICH checkout the badge describes, when it is a worktree. The chip shows
|
|
709
911
|
// the branch alone, so this row is the only place the tree is named.
|
|
710
912
|
add("worktree", worktreeDetail(data));
|
|
711
913
|
// The conversation's OWN directory, present only when the badge is
|
|
712
|
-
// describing
|
|
914
|
+
// describing a followed worktree — the chip's branch and counts are the
|
|
713
915
|
// tree's in that case, so this is where the checkout's own state stays
|
|
714
916
|
// visible. Absent until the `detail=1` response lands, like commits/stash.
|
|
715
|
-
if (data.
|
|
716
|
-
if (Array.isArray(data.lastCommits) && data.lastCommits.length > 0) {
|
|
717
|
-
rows.push(
|
|
718
|
-
react_jsx_runtime.jsxs("div", {
|
|
719
|
-
style: CARD_ROW,
|
|
720
|
-
children: [
|
|
721
|
-
react_jsx_runtime.jsx("span", { style: CARD_LABEL, children: "commits" }),
|
|
722
|
-
react_jsx_runtime.jsx("span", {
|
|
723
|
-
style: { ...CARD_VALUE, display: "flex", flexDirection: "column", gap: "2px" },
|
|
724
|
-
children: data.lastCommits.map((commit) =>
|
|
725
|
-
react_jsx_runtime.jsxs("span", {
|
|
726
|
-
children: [
|
|
727
|
-
react_jsx_runtime.jsx("span", { style: CARD_LABEL, children: commit.hash }),
|
|
728
|
-
" " + commit.subject + (commit.when === "" ? "" : " \u00B7 " + commit.when)
|
|
729
|
-
]
|
|
730
|
-
}, commit.hash)
|
|
731
|
-
)
|
|
732
|
-
})
|
|
733
|
-
]
|
|
734
|
-
}, "commits")
|
|
735
|
-
);
|
|
736
|
-
}
|
|
917
|
+
if (data.worktreeFollowed === true) add("checkout", formatCheckoutDetail(data.checkout));
|
|
737
918
|
add("stash", data.stashCount === void 0 ? void 0 : data.stashCount + " stashed");
|
|
738
919
|
return react_jsx_runtime.jsx("div", { style: CARD_CONTAINER, children: rows });
|
|
739
920
|
}
|
|
@@ -786,6 +967,11 @@ window.__ModuleLoader__.load({
|
|
|
786
967
|
// re-render that no fetch will schedule, so the × bumps a counter here.
|
|
787
968
|
const [, bumpNotice] = react.useState(0);
|
|
788
969
|
const seamNotice = seamHint !== null && !seamNoticeDismissed ? seamHint : null;
|
|
970
|
+
// the branch link's hover underline — the PR token's pattern: the
|
|
971
|
+
// affordance appears on hover/focus, never at rest. Its OWN state, not
|
|
972
|
+
// the PR token's linkHover: two links sharing one flag would
|
|
973
|
+
// underline both when either is hovered.
|
|
974
|
+
const [branchLinkHover, setBranchLinkHover] = react.useState(false);
|
|
789
975
|
const detail = useGitStatus(target, { pr: true, detail: true, enabled: hovered });
|
|
790
976
|
if (info === void 0 || info.git !== true) return null;
|
|
791
977
|
|
|
@@ -803,7 +989,8 @@ window.__ModuleLoader__.load({
|
|
|
803
989
|
if (Tooltip === void 0) return mark;
|
|
804
990
|
return react_jsx_runtime.jsx(Tooltip, {
|
|
805
991
|
side: "top",
|
|
806
|
-
|
|
992
|
+
// matches the body cap: the tooltip must never be the clamp
|
|
993
|
+
maxWidth: 600,
|
|
807
994
|
// a function label keeps the card's element tree out of every render
|
|
808
995
|
// until the tooltip actually opens
|
|
809
996
|
label: () => react_jsx_runtime.jsx(HoverCard, { info, detail }),
|
|
@@ -813,7 +1000,74 @@ window.__ModuleLoader__.load({
|
|
|
813
1000
|
// the mark is an element now rather than a leading glyph in the string, so
|
|
814
1001
|
// the SAME StatusMark the sidebar row draws carries the status here too;
|
|
815
1002
|
// the container's 4px gap supplies the space the emoji's own did
|
|
816
|
-
|
|
1003
|
+
// The branch NAME is its own hover surface: the commits this branch adds
|
|
1004
|
+
// (BranchCommitsList), sharing the mark's lazy detail fetch. The rest of
|
|
1005
|
+
// the leading text (operation token) and the count keep their own
|
|
1006
|
+
// surfaces. Degrades to plain text without the Tooltip primitive or
|
|
1007
|
+
// when the branch adds nothing beyond upstream.
|
|
1008
|
+
const branchHover = (() => {
|
|
1009
|
+
// The branch name links to the compare view (base...branch) — the web
|
|
1010
|
+
// page of exactly what the lineage tooltip lists. Vouched http(s)
|
|
1011
|
+
// only (the node half rebuilt the URL from parsed parts); new tab,
|
|
1012
|
+
// never navigating the conversation away.
|
|
1013
|
+
const isLink = typeof detail?.compareUrl === "string" && detail.compareUrl.startsWith("https://");
|
|
1014
|
+
const branch = isLink
|
|
1015
|
+
? react_jsx_runtime.jsx("a", {
|
|
1016
|
+
href: detail.compareUrl,
|
|
1017
|
+
target: "_blank",
|
|
1018
|
+
rel: "noopener noreferrer",
|
|
1019
|
+
"aria-label": "Compare " + info.branch + " with main on GitHub",
|
|
1020
|
+
onPointerEnter: () => { setHovered(true); setBranchLinkHover(true); },
|
|
1021
|
+
onPointerLeave: () => setBranchLinkHover(false),
|
|
1022
|
+
onFocus: () => { setHovered(true); setBranchLinkHover(true); },
|
|
1023
|
+
onBlur: () => setBranchLinkHover(false),
|
|
1024
|
+
style: {
|
|
1025
|
+
color: "inherit",
|
|
1026
|
+
textDecoration: branchLinkHover ? "underline" : "none",
|
|
1027
|
+
cursor: "pointer"
|
|
1028
|
+
},
|
|
1029
|
+
children: info.branch
|
|
1030
|
+
})
|
|
1031
|
+
: react_jsx_runtime.jsx("span", {
|
|
1032
|
+
onPointerEnter: () => setHovered(true),
|
|
1033
|
+
style: { cursor: "default" },
|
|
1034
|
+
children: info.branch
|
|
1035
|
+
});
|
|
1036
|
+
if (Tooltip === void 0 || detail === void 0 || detail === null) {
|
|
1037
|
+
// mid-fetch the branch shows plain; the lineage tooltip is
|
|
1038
|
+
// unconditional once the payload lands
|
|
1039
|
+
return branch;
|
|
1040
|
+
}
|
|
1041
|
+
return react_jsx_runtime.jsx(Tooltip, {
|
|
1042
|
+
side: "top",
|
|
1043
|
+
maxWidth: 480,
|
|
1044
|
+
label: () => react_jsx_runtime.jsx(BranchLineage, { info: detail }),
|
|
1045
|
+
children: branch
|
|
1046
|
+
});
|
|
1047
|
+
})();
|
|
1048
|
+
const text = formatOperationToken(info);
|
|
1049
|
+
// The ✎n count is its OWN hover surface: a names-only tooltip (see
|
|
1050
|
+
// UntrackedList). It shares the mark's lazy detail fetch — resting on
|
|
1051
|
+
// either triggers it once — and degrades to a plain count on a shell
|
|
1052
|
+
// without the Tooltip primitive.
|
|
1053
|
+
const countHover = (() => {
|
|
1054
|
+
const count = react_jsx_runtime.jsx("span", {
|
|
1055
|
+
onPointerEnter: () => setHovered(true),
|
|
1056
|
+
style: { cursor: "default" },
|
|
1057
|
+
children: formatGitSuffix(info)
|
|
1058
|
+
});
|
|
1059
|
+
const hasNames = (Array.isArray(detail?.untrackedNames) && detail.untrackedNames.length > 0)
|
|
1060
|
+
|| (Array.isArray(detail?.unstagedNames) && detail.unstagedNames.length > 0);
|
|
1061
|
+
if (Tooltip === void 0 || !hasNames) {
|
|
1062
|
+
return count;
|
|
1063
|
+
}
|
|
1064
|
+
return react_jsx_runtime.jsx(Tooltip, {
|
|
1065
|
+
side: "top",
|
|
1066
|
+
maxWidth: 480,
|
|
1067
|
+
label: () => react_jsx_runtime.jsx(CountNames, { info: detail }),
|
|
1068
|
+
children: count
|
|
1069
|
+
});
|
|
1070
|
+
})();
|
|
817
1071
|
const prToken = formatPrToken(info);
|
|
818
1072
|
const prUrl = prLinkUrl(info);
|
|
819
1073
|
const chip = react_jsx_runtime.jsxs("span", {
|
|
@@ -846,7 +1100,9 @@ window.__ModuleLoader__.load({
|
|
|
846
1100
|
// stays on the whole chip — it is about the badge, not the mark.
|
|
847
1101
|
hoverableMark,
|
|
848
1102
|
|
|
1103
|
+
branchHover,
|
|
849
1104
|
react_jsx_runtime.jsx("span", { key: "text", children: text }),
|
|
1105
|
+
countHover,
|
|
850
1106
|
prToken === ""
|
|
851
1107
|
? null
|
|
852
1108
|
: prUrl === void 0
|
|
@@ -959,6 +1215,162 @@ window.__ModuleLoader__.load({
|
|
|
959
1215
|
// `workspaces` is no longer required: both surfaces target an id and the
|
|
960
1216
|
// node half resolves the workspace, so the client never needs a service
|
|
961
1217
|
// lookup. Fewer declared services also means fewer ways to fail to load.
|
|
1218
|
+
/** The built-in ranking order — what "Reset to default" restores. */
|
|
1219
|
+
const DEFAULT_ORDER = ["operation", "unmerged", "sync", "publish", "merge", "commit", "checks"];
|
|
1220
|
+
|
|
1221
|
+
/**
|
|
1222
|
+
* Move one category within the ranking order — the card's only reorder
|
|
1223
|
+
* primitive, pure so the suite can drive it without a browser. Out-of-range
|
|
1224
|
+
* moves are no-ops (the buttons disable at the ends, this is the belt).
|
|
1225
|
+
*/
|
|
1226
|
+
function moveCategory(order, index, delta) {
|
|
1227
|
+
const next = [...order];
|
|
1228
|
+
const to = index + delta;
|
|
1229
|
+
if (index < 0 || index >= next.length || to < 0 || to >= next.length) return next;
|
|
1230
|
+
const [moved] = next.splice(index, 1);
|
|
1231
|
+
next.splice(to, 0, moved);
|
|
1232
|
+
return next;
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
/** Human labels for the ranking categories, in default order. */
|
|
1236
|
+
const CATEGORY_LABELS = {
|
|
1237
|
+
operation: ["operation", "resume a paused merge/rebase"],
|
|
1238
|
+
unmerged: ["unmerged", "resolve conflict markers"],
|
|
1239
|
+
sync: ["sync", "pull, or rebase + confirmed force-push"],
|
|
1240
|
+
publish: ["publish", "push local commits"],
|
|
1241
|
+
merge: ["merge", "a merge-ready pull request"],
|
|
1242
|
+
commit: ["commit", "stage and commit working changes"],
|
|
1243
|
+
checks: ["checks", "watch failing CI"]
|
|
1244
|
+
};
|
|
1245
|
+
|
|
1246
|
+
/**
|
|
1247
|
+
* The settings card's body — Settings → Plugins → Git Badge. Reads the
|
|
1248
|
+
* bound settings scope through the store the registration injects and
|
|
1249
|
+
* writes back one field ("order") via the scope's mutate path. Styled like
|
|
1250
|
+
* this plugin's other surfaces rather than the host's card primitives,
|
|
1251
|
+
* which are not exported to plugins.
|
|
1252
|
+
*/
|
|
1253
|
+
function GitBadgeOrderCard(props) {
|
|
1254
|
+
const state = props.useGitBadgeOrderCard((snapshot) => snapshot);
|
|
1255
|
+
const t = props.t;
|
|
1256
|
+
// Local expander state, mirroring the host's plugin cards: collapsed by
|
|
1257
|
+
// default, and auto-collapsed again once a save settles clean (so the
|
|
1258
|
+
// card confirms itself the way its siblings do).
|
|
1259
|
+
const [open, setOpen] = react.useState(false);
|
|
1260
|
+
const saveStarted = react.useRef(false);
|
|
1261
|
+
react.useEffect(() => {
|
|
1262
|
+
if (state.saving) {
|
|
1263
|
+
saveStarted.current = true;
|
|
1264
|
+
return;
|
|
1265
|
+
}
|
|
1266
|
+
if (!saveStarted.current) return;
|
|
1267
|
+
saveStarted.current = false;
|
|
1268
|
+
if (!state.dirty && !state.failed) setOpen(false);
|
|
1269
|
+
}, [state.dirty, state.failed, state.saving]);
|
|
1270
|
+
if (!state.available) return null;
|
|
1271
|
+
const rows = state.order.map((cat, index) => {
|
|
1272
|
+
const [label, hint] = CATEGORY_LABELS[cat] ?? [cat, ""];
|
|
1273
|
+
return react_jsx_runtime.jsxs("div", {
|
|
1274
|
+
style: { display: "flex", alignItems: "center", gap: "8px", padding: "2px 0" },
|
|
1275
|
+
children: [
|
|
1276
|
+
react_jsx_runtime.jsx("span", {
|
|
1277
|
+
style: { color: "var(--dsw-alias-label-tertiary, #9ea7ad)", fontFamily: "monospace", minWidth: "16px", textAlign: "right" },
|
|
1278
|
+
children: String(index + 1)
|
|
1279
|
+
}),
|
|
1280
|
+
react_jsx_runtime.jsx("span", { style: { minWidth: "84px" }, children: label }),
|
|
1281
|
+
react_jsx_runtime.jsx("span", { style: { color: "var(--dsw-alias-label-tertiary, #9ea7ad)", flex: "1", minWidth: 0 }, children: hint }),
|
|
1282
|
+
react_jsx_runtime.jsx("button", {
|
|
1283
|
+
type: "button",
|
|
1284
|
+
"aria-label": t("moveUp") + " " + label,
|
|
1285
|
+
disabled: state.saving || !state.writable || index === 0,
|
|
1286
|
+
onClick: () => props.move(index, -1),
|
|
1287
|
+
style: CARD_BUTTON,
|
|
1288
|
+
children: "\u2191"
|
|
1289
|
+
}),
|
|
1290
|
+
react_jsx_runtime.jsx("button", {
|
|
1291
|
+
type: "button",
|
|
1292
|
+
"aria-label": t("moveDown") + " " + label,
|
|
1293
|
+
disabled: state.saving || !state.writable || index === state.order.length - 1,
|
|
1294
|
+
onClick: () => props.move(index, 1),
|
|
1295
|
+
style: CARD_BUTTON,
|
|
1296
|
+
children: "\u2193"
|
|
1297
|
+
})
|
|
1298
|
+
]
|
|
1299
|
+
}, cat);
|
|
1300
|
+
});
|
|
1301
|
+
// The card chrome matches the host's own plugin cards (its CSS module is
|
|
1302
|
+
// not exported to plugins, so the values are reproduced inline): an <li>
|
|
1303
|
+
// with the bordered card, a header button that expands, and a body that
|
|
1304
|
+
// carries the list and the footer actions.
|
|
1305
|
+
return react_jsx_runtime.jsxs("li", {
|
|
1306
|
+
style: open ? CARD_OPEN : CARD_CLOSED,
|
|
1307
|
+
children: [
|
|
1308
|
+
react_jsx_runtime.jsxs("button", {
|
|
1309
|
+
type: "button",
|
|
1310
|
+
"aria-expanded": open,
|
|
1311
|
+
"aria-label": t(open ? "collapse" : "expand") + ": " + t("title"),
|
|
1312
|
+
onClick: () => setOpen(!open),
|
|
1313
|
+
style: CARD_HEADER,
|
|
1314
|
+
children: [
|
|
1315
|
+
react_jsx_runtime.jsxs("span", {
|
|
1316
|
+
style: { display: "flex", flexDirection: "column", flex: "1", gap: "4px", minWidth: 0 },
|
|
1317
|
+
children: [
|
|
1318
|
+
react_jsx_runtime.jsx("span", { style: CARD_NAME, children: t("title") }),
|
|
1319
|
+
react_jsx_runtime.jsx("span", { style: CARD_DESCRIPTION, children: t("description") })
|
|
1320
|
+
]
|
|
1321
|
+
}),
|
|
1322
|
+
react_jsx_runtime.jsx("span", {
|
|
1323
|
+
"aria-hidden": true,
|
|
1324
|
+
style: open
|
|
1325
|
+
? { color: "var(--dsw-alias-label-tertiary, #9ea7ad)", flex: "none", transform: "rotate(180deg)", transition: "transform .16s" }
|
|
1326
|
+
: { color: "var(--dsw-alias-label-tertiary, #9ea7ad)", flex: "none", transition: "transform .16s" },
|
|
1327
|
+
children: (primitives !== null && primitives.IconChevronDownOutline14 !== void 0
|
|
1328
|
+
? react_jsx_runtime.jsx(primitives.IconChevronDownOutline14, {})
|
|
1329
|
+
: "\u25BE")
|
|
1330
|
+
})
|
|
1331
|
+
]
|
|
1332
|
+
}),
|
|
1333
|
+
open
|
|
1334
|
+
? react_jsx_runtime.jsxs("div", {
|
|
1335
|
+
style: { borderTop: ".5px solid var(--dsw-alias-border-l2, #e4e8eb)", margin: "0 16px", paddingBottom: "8px" },
|
|
1336
|
+
children: [
|
|
1337
|
+
react_jsx_runtime.jsx("div", { style: { padding: "10px 0 6px" }, children: t("intro") }),
|
|
1338
|
+
react_jsx_runtime.jsx("div", { style: { display: "flex", flexDirection: "column" }, children: rows }),
|
|
1339
|
+
react_jsx_runtime.jsxs("div", {
|
|
1340
|
+
style: { display: "flex", gap: "8px", alignItems: "center", justifyContent: "flex-end", padding: "12px 0 4px" },
|
|
1341
|
+
children: [
|
|
1342
|
+
state.failed ? react_jsx_runtime.jsx("span", { style: { color: "var(--dsw-alias-label-error, #d13438)", flex: "1", fontSize: "12px" }, children: t("failed") }) : null,
|
|
1343
|
+
state.writable ? null : react_jsx_runtime.jsx("span", { style: { color: "var(--dsw-alias-label-tertiary, #9ea7ad)", flex: "1", fontSize: "12px" }, children: t("readOnly") }),
|
|
1344
|
+
react_jsx_runtime.jsx("button", {
|
|
1345
|
+
type: "button",
|
|
1346
|
+
disabled: !state.dirty || state.saving,
|
|
1347
|
+
onClick: props.discard,
|
|
1348
|
+
style: CARD_BUTTON,
|
|
1349
|
+
children: t("discard")
|
|
1350
|
+
}),
|
|
1351
|
+
react_jsx_runtime.jsx("button", {
|
|
1352
|
+
type: "button",
|
|
1353
|
+
disabled: !state.dirty || state.saving || !state.writable,
|
|
1354
|
+
onClick: props.save,
|
|
1355
|
+
style: CARD_BUTTON,
|
|
1356
|
+
children: state.saving ? t("saving") : t("save")
|
|
1357
|
+
}),
|
|
1358
|
+
react_jsx_runtime.jsx("button", {
|
|
1359
|
+
type: "button",
|
|
1360
|
+
disabled: state.saving || !state.writable,
|
|
1361
|
+
onClick: props.reset,
|
|
1362
|
+
style: CARD_BUTTON,
|
|
1363
|
+
children: t("reset")
|
|
1364
|
+
})
|
|
1365
|
+
]
|
|
1366
|
+
})
|
|
1367
|
+
]
|
|
1368
|
+
})
|
|
1369
|
+
: null
|
|
1370
|
+
]
|
|
1371
|
+
});
|
|
1372
|
+
}
|
|
1373
|
+
|
|
962
1374
|
const inject = ["slots", "inputTriggers"];
|
|
963
1375
|
|
|
964
1376
|
/**
|
|
@@ -1054,6 +1466,126 @@ window.__ModuleLoader__.load({
|
|
|
1054
1466
|
});
|
|
1055
1467
|
}
|
|
1056
1468
|
|
|
1469
|
+
// The settings card (Settings → Plugins → Git Badge): the order lives
|
|
1470
|
+
// in this plugin's `git-badge` settings namespace, which the node half
|
|
1471
|
+
// registers; this half renders the card that edits it through the bound
|
|
1472
|
+
// scope — reads from the scope snapshot, writes one field via mutate.
|
|
1473
|
+
// Guarded: a host without settingsScope loses the card, never the
|
|
1474
|
+
// badges.
|
|
1475
|
+
if (typeof ctx.inject === "function") {
|
|
1476
|
+
try {
|
|
1477
|
+
ctx.inject(["settingsScope", "slots", "locale"], (scopeCtx) => {
|
|
1478
|
+
const NS = "dsh-git-badge";
|
|
1479
|
+
scopeCtx.effect(() => scopeCtx.locale.register(NS, {
|
|
1480
|
+
en: {
|
|
1481
|
+
title: "Git Badge",
|
|
1482
|
+
description: "Which action class the badge suggests first.",
|
|
1483
|
+
expand: "Expand",
|
|
1484
|
+
collapse: "Collapse",
|
|
1485
|
+
intro: "The badge suggests one next action; this is the order it considers the classes in.",
|
|
1486
|
+
save: "Save",
|
|
1487
|
+
discard: "Discard",
|
|
1488
|
+
reset: "Reset to default",
|
|
1489
|
+
saving: "Saving\u2026",
|
|
1490
|
+
failed: "save failed \u2014 try again",
|
|
1491
|
+
readOnly: "read-only on this host",
|
|
1492
|
+
moveUp: "Move up",
|
|
1493
|
+
moveDown: "Move down"
|
|
1494
|
+
}
|
|
1495
|
+
}), "dsh-git-badge: settings locale");
|
|
1496
|
+
const scope = scopeCtx.settingsScope.bind({ namespace: "git-badge" });
|
|
1497
|
+
// draft === null means "mirror the stored order"; any reorder
|
|
1498
|
+
// stages a copy, and only a confirmed write clears it — a
|
|
1499
|
+
// rejected write leaves the draft for a retry rather than
|
|
1500
|
+
// pretending it saved.
|
|
1501
|
+
let draft = null;
|
|
1502
|
+
let saving = false;
|
|
1503
|
+
let failed = false;
|
|
1504
|
+
const subscribers = new Set();
|
|
1505
|
+
let snapshot = null;
|
|
1506
|
+
const storedOrder = () => {
|
|
1507
|
+
const view = scope.getSnapshot();
|
|
1508
|
+
return Array.isArray(view?.value?.order) ? view.value.order : DEFAULT_ORDER;
|
|
1509
|
+
};
|
|
1510
|
+
const build = () => {
|
|
1511
|
+
const view = scope.getSnapshot();
|
|
1512
|
+
const stored = storedOrder();
|
|
1513
|
+
const order = draft ?? stored;
|
|
1514
|
+
return {
|
|
1515
|
+
available: view?.status === "ready",
|
|
1516
|
+
writable: view?.writable === true,
|
|
1517
|
+
order,
|
|
1518
|
+
dirty: draft !== null && draft.join() !== stored.join(),
|
|
1519
|
+
saving,
|
|
1520
|
+
failed
|
|
1521
|
+
};
|
|
1522
|
+
};
|
|
1523
|
+
const publish = () => {
|
|
1524
|
+
snapshot = build();
|
|
1525
|
+
for (const listener of subscribers) listener();
|
|
1526
|
+
};
|
|
1527
|
+
scopeCtx.effect(() => {
|
|
1528
|
+
const off = scope.subscribe(publish);
|
|
1529
|
+
return () => {
|
|
1530
|
+
if (typeof off === "function") off();
|
|
1531
|
+
};
|
|
1532
|
+
}, "dsh-git-badge: settings scope");
|
|
1533
|
+
publish();
|
|
1534
|
+
const store = {
|
|
1535
|
+
getSnapshot: () => snapshot ?? build(),
|
|
1536
|
+
subscribe: (listener) => {
|
|
1537
|
+
subscribers.add(listener);
|
|
1538
|
+
return () => subscribers.delete(listener);
|
|
1539
|
+
}
|
|
1540
|
+
};
|
|
1541
|
+
const save = async () => {
|
|
1542
|
+
if (draft === null || saving) return;
|
|
1543
|
+
const wanted = [...draft];
|
|
1544
|
+
saving = true;
|
|
1545
|
+
failed = false;
|
|
1546
|
+
publish();
|
|
1547
|
+
try {
|
|
1548
|
+
await scope.mutate([{ op: "set", path: ["order"], value: wanted }]);
|
|
1549
|
+
} catch (error) {
|
|
1550
|
+
console.error("[dsh-git-badge] order save failed:", error);
|
|
1551
|
+
}
|
|
1552
|
+
saving = false;
|
|
1553
|
+
if (storedOrder().join() === wanted.join()) draft = null;
|
|
1554
|
+
else failed = true;
|
|
1555
|
+
publish();
|
|
1556
|
+
};
|
|
1557
|
+
scopeCtx.slots.inject("settings.plugin.item", function* () {
|
|
1558
|
+
yield scopeCtx.slots.register({
|
|
1559
|
+
name: "settings.plugin.item",
|
|
1560
|
+
key: "git-badge",
|
|
1561
|
+
locale: NS,
|
|
1562
|
+
inject: () => ({
|
|
1563
|
+
hooks: { gitBadgeOrderCard: store },
|
|
1564
|
+
move: (index, delta) => {
|
|
1565
|
+
draft = moveCategory(draft ?? [...storedOrder()], index, delta);
|
|
1566
|
+
failed = false;
|
|
1567
|
+
publish();
|
|
1568
|
+
},
|
|
1569
|
+
save: () => void save(),
|
|
1570
|
+
discard: () => {
|
|
1571
|
+
draft = null;
|
|
1572
|
+
failed = false;
|
|
1573
|
+
publish();
|
|
1574
|
+
},
|
|
1575
|
+
reset: () => {
|
|
1576
|
+
draft = [...DEFAULT_ORDER];
|
|
1577
|
+
failed = false;
|
|
1578
|
+
publish();
|
|
1579
|
+
}
|
|
1580
|
+
})
|
|
1581
|
+
}, GitBadgeOrderCard);
|
|
1582
|
+
});
|
|
1583
|
+
});
|
|
1584
|
+
} catch (error) {
|
|
1585
|
+
console.error("[dsh-git-badge] settings card unavailable:", error);
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1057
1589
|
// inject() re-evaluates when a seam's declaration appears, so boot
|
|
1058
1590
|
// order relative to the workspace browser does not matter. On an
|
|
1059
1591
|
// unpatched install the seam is never declared, so this callback never
|
|
@@ -1135,7 +1667,7 @@ window.__ModuleLoader__.load({
|
|
|
1135
1667
|
// Additive; the host reads apply/inject and ignores the rest. The suite
|
|
1136
1668
|
// drives these to assert the REQUEST contract — which surface asks for the
|
|
1137
1669
|
// expensive extras — without a browser, a fetch or a network.
|
|
1138
|
-
exports.__internals = { targetQuery, formatPrToken, formatFileBreakdown, formatPrDetail, formatCheckoutDetail, worktreeDetail, actionToken, ghSkillActions };
|
|
1670
|
+
exports.__internals = { targetQuery, formatPrToken, formatFileBreakdown, formatPrDetail, formatCheckoutDetail, worktreeDetail, actionToken, ghSkillActions, moveCategory };
|
|
1139
1671
|
return module.exports;
|
|
1140
1672
|
}
|
|
1141
1673
|
});
|