dsh-git-badge 0.16.1 → 0.17.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 +148 -7
- package/README.md +3 -3
- package/lib/client.js +329 -184
- package/lib/index.js +215 -106
- 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/skill/gh/SKILL.md +23 -1
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 = () => {
|
|
@@ -232,45 +244,37 @@ window.__ModuleLoader__.load({
|
|
|
232
244
|
|
|
233
245
|
|
|
234
246
|
/**
|
|
235
|
-
* The
|
|
236
|
-
*
|
|
237
|
-
*
|
|
238
|
-
*
|
|
239
|
-
*
|
|
247
|
+
* The actions BOTH surfaces render — the server's list, verbatim.
|
|
248
|
+
*
|
|
249
|
+
* This half used to re-derive pull/push/commit/pr from the raw counts,
|
|
250
|
+
* which meant two implementations of the same conditions drifting apart:
|
|
251
|
+
* the card once promoted a client-only extra into its action row, and
|
|
252
|
+
* `/gh pr` was offered on the default branch. The node half owns the
|
|
253
|
+
* rules and the ranking now; the client adds presentation only — the risk
|
|
254
|
+
* gate for `clean`, and the labels.
|
|
240
255
|
*/
|
|
241
256
|
function ghSkillActions(info) {
|
|
242
257
|
if (info === void 0 || info === null || info.git !== true) return [];
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
258
|
+
return Array.isArray(info.actions) ? info.actions : [];
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* The risk gate the menu shows for `/gh clean`, in the host's own
|
|
263
|
+
* confirmation shape (RiskConfirmation: title, description, an acknowledge
|
|
264
|
+
* checkbox, two button labels). Confirm stays disabled until the box is
|
|
265
|
+
* ticked — that is the "are you sure?", before the invocation is even sent.
|
|
266
|
+
* The agent then re-establishes the exact list with `git clean -n` and asks
|
|
267
|
+
* again: two gates, because this one names the risk and that one names the
|
|
268
|
+
* files.
|
|
269
|
+
*/
|
|
270
|
+
function cleanGate() {
|
|
271
|
+
return {
|
|
272
|
+
title: "Remove untracked files?",
|
|
273
|
+
description: "Untracked files have no reflog and no stash entry — once removed they cannot be recovered. The agent will show the exact list (`git clean -n`) and ask again before removing anything.",
|
|
274
|
+
acknowledgeLabel: "I understand untracked files cannot be recovered",
|
|
275
|
+
cancelLabel: "Cancel",
|
|
276
|
+
confirmLabel: "Continue"
|
|
249
277
|
};
|
|
250
|
-
const next = info.next;
|
|
251
|
-
if (next !== void 0 && next !== null) add(next.args, next.why, next.what);
|
|
252
|
-
const ahead = info.ahead || 0;
|
|
253
|
-
const behind = info.behind || 0;
|
|
254
|
-
const staged = info.stagedFiles || 0;
|
|
255
|
-
const unstaged = info.unstagedFiles || 0;
|
|
256
|
-
const untracked = info.untrackedFiles || 0;
|
|
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");
|
|
266
|
-
const pr = info.pr;
|
|
267
|
-
if (pr !== void 0 && pr !== null && pr.number !== void 0) {
|
|
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");
|
|
270
|
-
} else if (ahead === 0 && behind === 0 && info.upstream !== void 0 && staged + unstaged + untracked === 0) {
|
|
271
|
-
add("pr", "branch is pushed and has no pull request", "open a pull request for this branch");
|
|
272
|
-
}
|
|
273
|
-
return subs.slice(0, 6);
|
|
274
278
|
}
|
|
275
279
|
|
|
276
280
|
/**
|
|
@@ -305,16 +309,29 @@ window.__ModuleLoader__.load({
|
|
|
305
309
|
* - clean workspaces stay quiet: arrows only when nonzero, no ✎ at 0
|
|
306
310
|
* - no upstream: arrows omitted entirely
|
|
307
311
|
*/
|
|
308
|
-
|
|
309
|
-
|
|
312
|
+
/**
|
|
313
|
+
* The upstream sync arrows (" ↑1 ↓2"), or "". Split out from the file
|
|
314
|
+
* count so each rides the surface that OWNS it: the arrows belong to the
|
|
315
|
+
* branch (they are the branch's standing against its upstream, and the
|
|
316
|
+
* branch name's hover and compare link cover them), while ✎n belongs to the
|
|
317
|
+
* count's names tooltip.
|
|
318
|
+
*/
|
|
319
|
+
function formatSync(info) {
|
|
310
320
|
const hasUpstream = info.ahead !== void 0 || info.behind !== void 0;
|
|
311
321
|
const files = (info.changedFiles || 0) + (info.untrackedFiles || 0);
|
|
312
|
-
if (hasUpstream
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
322
|
+
if (!hasUpstream || (files === 0 && !(info.ahead > 0) && !(info.behind > 0))) return "";
|
|
323
|
+
return " \u2191" + (info.ahead || 0) + " \u2193" + (info.behind || 0);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/** The ✎n file count (" ✎3"), or "". */
|
|
327
|
+
function formatFileCount(info) {
|
|
328
|
+
const files = (info.changedFiles || 0) + (info.untrackedFiles || 0);
|
|
329
|
+
return files > 0 ? " \u270E" + files : "";
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/** Both halves, in chip order — kept for callers that want the whole suffix. */
|
|
333
|
+
function formatGitSuffix(info) {
|
|
334
|
+
return formatSync(info) + formatFileCount(info);
|
|
318
335
|
}
|
|
319
336
|
|
|
320
337
|
/**
|
|
@@ -544,9 +561,57 @@ window.__ModuleLoader__.load({
|
|
|
544
561
|
*/
|
|
545
562
|
const ACTION_STYLE = { flex: "none", marginLeft: "auto", marginRight: "8px", fontWeight: 500 };
|
|
546
563
|
|
|
547
|
-
/**
|
|
548
|
-
|
|
564
|
+
/**
|
|
565
|
+
* The row's own run state, from the shell's standard kit.
|
|
566
|
+
*
|
|
567
|
+
* The renderer that assembles slots hands EVERY entry the sessions store as
|
|
568
|
+
* a selector hook (`useSessions`) — the same kit that supplies
|
|
569
|
+
* `useWorkspaces` — which is how a row can tell an idle session from one
|
|
570
|
+
* mid-turn without the seam having to carry it. A shell that does not seed
|
|
571
|
+
* that kit leaves the prop undefined: then we cannot know, and the badge
|
|
572
|
+
* behaves exactly as it did before rather than guessing "idle".
|
|
573
|
+
*
|
|
574
|
+
* One helper and one call site, so the hook order is identical on every path
|
|
575
|
+
* (the fallback is a plain function, never a hook).
|
|
576
|
+
*/
|
|
577
|
+
function useSessionRunning(useSessions, sessionId) {
|
|
578
|
+
const select = typeof useSessions === "function" ? useSessions : () => void 0;
|
|
579
|
+
return select((state) => state?.byId?.[sessionId]?.running) === true;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* Report the idle gate ONCE, the way the boot diagnostics report the seam and
|
|
584
|
+
* the Tooltip primitive. Whether the shell actually handed a slot the
|
|
585
|
+
* sessions store is invisible from anywhere else, and a gate that silently
|
|
586
|
+
* never fires is indistinguishable from a gate that never needed to.
|
|
587
|
+
*/
|
|
588
|
+
let idleGateReported = false;
|
|
589
|
+
function reportIdleGate(present) {
|
|
590
|
+
if (idleGateReported) return;
|
|
591
|
+
idleGateReported = true;
|
|
592
|
+
console.info(
|
|
593
|
+
"[dsh-git-badge] session-row idle gate = " +
|
|
594
|
+
(present
|
|
595
|
+
? "on (the standard kit supplies useSessions)."
|
|
596
|
+
: "off (no useSessions prop) — the token cannot tell a running turn, so it shows as before.")
|
|
597
|
+
);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/**
|
|
601
|
+
* The action badge: floated, weighted, and coloured by severity — and shown
|
|
602
|
+
* only when the session is IDLE. A turn in flight is precisely when a git
|
|
603
|
+
* suggestion is noise: the checkout is still moving and the agent has not
|
|
604
|
+
* finished, so the action describes a state that is about to change. Nothing
|
|
605
|
+
* about the action itself changes; the gate is the whole feature.
|
|
606
|
+
*
|
|
607
|
+
* The filter is on RENDER, not on the fetch: hiding the row must not take the
|
|
608
|
+
* hover card's detail line down with it — both read the same cached status.
|
|
609
|
+
*/
|
|
610
|
+
function SessionGitBadge({ sessionId, useSessions }) {
|
|
549
611
|
const info = useGitStatus(sessionId === void 0 ? void 0 : { kind: "session", id: sessionId }, { pr: true });
|
|
612
|
+
const running = useSessionRunning(useSessions, sessionId);
|
|
613
|
+
reportIdleGate(typeof useSessions === "function");
|
|
614
|
+
if (running) return null;
|
|
550
615
|
if (sessionId === void 0 || info === void 0 || info.git !== true) return null;
|
|
551
616
|
const action = actionToken(info);
|
|
552
617
|
if (action === "") return null;
|
|
@@ -651,6 +716,70 @@ window.__ModuleLoader__.load({
|
|
|
651
716
|
return name;
|
|
652
717
|
}
|
|
653
718
|
|
|
719
|
+
/**
|
|
720
|
+
* ONE commit line, shared by the branch hover and the PR token's hover so
|
|
721
|
+
* the two cannot drift apart again: a fixed-width monospace hash cell, so
|
|
722
|
+
* every description starts on the same column and the list reads as a
|
|
723
|
+
* table, then `subject · age`. `sign` belongs to the branch hover — "+"
|
|
724
|
+
* (only on this branch) or "−" (only on main); the PR list passes none,
|
|
725
|
+
* because all of its commits are `base..HEAD`.
|
|
726
|
+
*
|
|
727
|
+
* The cell needs `display: inline-block` for `minWidth` to mean anything: an
|
|
728
|
+
* inline span ignores it, which is exactly how the PR list came to render a
|
|
729
|
+
* hash with NO gap at all — it reused the card's label style, whose
|
|
730
|
+
* `minWidth` and `flex` only do anything inside a flex row.
|
|
731
|
+
*/
|
|
732
|
+
function CommitLine({ commit, sign = "" }) {
|
|
733
|
+
return react_jsx_runtime.jsxs("span", {
|
|
734
|
+
style: { whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" },
|
|
735
|
+
children: [
|
|
736
|
+
react_jsx_runtime.jsx("span", {
|
|
737
|
+
style: {
|
|
738
|
+
color: "var(--dsw-alias-label-tertiary, #9ea7ad)",
|
|
739
|
+
fontFamily: "monospace",
|
|
740
|
+
display: "inline-block",
|
|
741
|
+
minWidth: "68px",
|
|
742
|
+
flex: "none"
|
|
743
|
+
},
|
|
744
|
+
children: sign + commit.hash
|
|
745
|
+
}),
|
|
746
|
+
commit.subject + (commit.when === "" ? "" : " \u00B7 " + shortWhen(commit.when))
|
|
747
|
+
]
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
/**
|
|
752
|
+
* The PR token's own hover body — the commits IN THIS PULL REQUEST
|
|
753
|
+
* (`base..HEAD`), and nothing else: the token already says the number and
|
|
754
|
+
* CI state, so the one question left is "what is in it?". Same line format
|
|
755
|
+
* as the branch hover's commit list (see CommitLine — one renderer, so the
|
|
756
|
+
* columns line up in both by construction), and the card's own width
|
|
757
|
+
* behaviour: full-width lines, nowrap, ellipsis.
|
|
758
|
+
*/
|
|
759
|
+
function PrCommitsList({ info, pending = false }) {
|
|
760
|
+
const commits = info?.prCommits;
|
|
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
|
+
}
|
|
768
|
+
return react_jsx_runtime.jsx("span", { style: CARD_CONTAINER, children: "no commits beyond the base branch" });
|
|
769
|
+
}
|
|
770
|
+
return react_jsx_runtime.jsxs("div", { style: CARD_CONTAINER, children: [
|
|
771
|
+
react_jsx_runtime.jsxs("div", {
|
|
772
|
+
style: { display: "flex", flexDirection: "column", gap: "2px", minWidth: 0 },
|
|
773
|
+
children: [
|
|
774
|
+
...commits.map((commit) => react_jsx_runtime.jsx(CommitLine, { commit }, commit.hash)),
|
|
775
|
+
(typeof info.prCommitsTotal === "number" && info.prCommitsTotal > commits.length
|
|
776
|
+
? "\u2026 and " + (info.prCommitsTotal - commits.length) + " more"
|
|
777
|
+
: null)
|
|
778
|
+
]
|
|
779
|
+
})
|
|
780
|
+
] });
|
|
781
|
+
}
|
|
782
|
+
|
|
654
783
|
const CARD_CONTAINER = {
|
|
655
784
|
display: "flex",
|
|
656
785
|
flexDirection: "column",
|
|
@@ -669,12 +798,13 @@ window.__ModuleLoader__.load({
|
|
|
669
798
|
* the one question the count poses ("what are these?"). One stacked
|
|
670
799
|
* column with sub-headers: UNSTAGED (tracked, edited — valid regardless
|
|
671
800
|
* of the collapsed untracked retry) then UNTRACKED. Reads the same lazy
|
|
672
|
-
* detail payload the mark's card uses
|
|
673
|
-
*
|
|
674
|
-
*
|
|
675
|
-
*
|
|
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.
|
|
676
806
|
*/
|
|
677
|
-
function CountNames({ info }) {
|
|
807
|
+
function CountNames({ info, pending = false }) {
|
|
678
808
|
const sections = [
|
|
679
809
|
{
|
|
680
810
|
label: "staged",
|
|
@@ -693,6 +823,11 @@ window.__ModuleLoader__.load({
|
|
|
693
823
|
}
|
|
694
824
|
].filter((section) => Array.isArray(section.names) && section.names.length > 0);
|
|
695
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
|
+
}
|
|
696
831
|
return react_jsx_runtime.jsx("span", { style: CARD_CONTAINER, children: "no changed files" });
|
|
697
832
|
}
|
|
698
833
|
return react_jsx_runtime.jsxs("div", { style: CARD_CONTAINER, children:
|
|
@@ -741,27 +876,11 @@ window.__ModuleLoader__.load({
|
|
|
741
876
|
if (value === void 0 || value === null || value === "") return;
|
|
742
877
|
rows.push(cardRow(label, value));
|
|
743
878
|
};
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
//
|
|
748
|
-
//
|
|
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.
|
|
879
|
+
// ONLY the merge axis, and it ALWAYS renders. The old upstream and sync
|
|
880
|
+
// rows spoke about the push axis (the branch's own remote), which the
|
|
881
|
+
// merge row subsumes: "1 ahead, 0 behind main" is the same fact stated
|
|
882
|
+
// against the branch that matters, in words rather than arrows. A row
|
|
883
|
+
// that can be absent is a row the reader has to reconstruct.
|
|
765
884
|
const pr = info?.pr;
|
|
766
885
|
const mergeParts = [];
|
|
767
886
|
if (pr !== void 0 && pr !== null && pr.number !== void 0) {
|
|
@@ -771,12 +890,15 @@ window.__ModuleLoader__.load({
|
|
|
771
890
|
: pr.state === "failing" ? "blocked: checks failing"
|
|
772
891
|
: "blocked");
|
|
773
892
|
} else if (pr.state === "failing") mergeParts.push("blocked: checks failing");
|
|
774
|
-
else if (pr.mergeState
|
|
893
|
+
else if (pr.mergeState === "CLEAN" || (pr.state === "passing" && pr.review === "APPROVED")) mergeParts.push("ready to merge");
|
|
894
|
+
else if (pr.review === "REVIEW_REQUIRED") mergeParts.push("blocked: review required");
|
|
895
|
+
} else {
|
|
896
|
+
mergeParts.push("no pull request");
|
|
775
897
|
}
|
|
776
898
|
if (info?.mainAhead !== void 0 || info?.mainBehind !== void 0) {
|
|
777
899
|
mergeParts.push((info.mainAhead || 0) + " ahead, " + (info.mainBehind || 0) + " behind main");
|
|
778
900
|
}
|
|
779
|
-
|
|
901
|
+
add("merge", mergeParts.join(" \u00B7 "));
|
|
780
902
|
const commits = info?.branchCommits;
|
|
781
903
|
if (Array.isArray(commits) && commits.length > 0) {
|
|
782
904
|
// Full-width block, not the label/value two-column row: commit lines
|
|
@@ -788,23 +910,9 @@ window.__ModuleLoader__.load({
|
|
|
788
910
|
react_jsx_runtime.jsxs("div", {
|
|
789
911
|
style: { display: "flex", flexDirection: "column", gap: "2px", minWidth: 0 },
|
|
790
912
|
children:
|
|
791
|
-
commits.map((commit) => react_jsx_runtime.
|
|
792
|
-
|
|
793
|
-
|
|
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
|
-
]
|
|
913
|
+
commits.map((commit) => react_jsx_runtime.jsx(CommitLine, {
|
|
914
|
+
commit,
|
|
915
|
+
sign: commit.sign === "+" ? "+" : commit.sign === "\u2212" ? "\u2212" : ""
|
|
808
916
|
}, commit.hash))
|
|
809
917
|
}, "branch-commits")
|
|
810
918
|
);
|
|
@@ -891,11 +999,13 @@ window.__ModuleLoader__.load({
|
|
|
891
999
|
rows.push(cardRow(label, value));
|
|
892
1000
|
};
|
|
893
1001
|
// The "action" row — the top line, in the card's ordinary row layout:
|
|
894
|
-
// the
|
|
895
|
-
//
|
|
896
|
-
//
|
|
897
|
-
//
|
|
898
|
-
|
|
1002
|
+
// the SERVER's ranked suggestion, and nothing else. The client's
|
|
1003
|
+
// extras (secondary picker entries) must never promote themselves into
|
|
1004
|
+
// this line: when the server says there is nothing to do — clean and
|
|
1005
|
+
// synced — the row is omitted, and "no suggestion" stays a suggestion.
|
|
1006
|
+
// Plain text, no affordance: the actionable surface is the (+) menu's
|
|
1007
|
+
// /gh picker, and this row is the read-only pointer to it.
|
|
1008
|
+
const topAction = data.next;
|
|
899
1009
|
if (topAction !== void 0) {
|
|
900
1010
|
// git-comment convention: the invocation, two spaces, then "# "
|
|
901
1011
|
// plus what it does
|
|
@@ -920,6 +1030,24 @@ window.__ModuleLoader__.load({
|
|
|
920
1030
|
}
|
|
921
1031
|
|
|
922
1032
|
//#region composer chip (upstream additive surface: conversation.input.left)
|
|
1033
|
+
/**
|
|
1034
|
+
* The ONE copy of the hover-card degrade rule: wrap a node in the shell's
|
|
1035
|
+
* Tooltip when the primitive exists, and render the node bare otherwise — a
|
|
1036
|
+
* missing card is a smaller failure than a missing badge. `label` is the
|
|
1037
|
+
* thunk the Tooltip calls only when it opens, so the card's element tree
|
|
1038
|
+
* stays out of closed tooltips.
|
|
1039
|
+
*
|
|
1040
|
+
* ALWAYS mount the wrapper — never conditionally, once its data arrives.
|
|
1041
|
+
* The primitive is uncontrolled (it has no `open` prop) and opens on the
|
|
1042
|
+
* CHILD's mouseenter, so a wrapper mounted after the pointer is already
|
|
1043
|
+
* inside can never open for that visit: the pointer has to leave and
|
|
1044
|
+
* return. Gate the LABEL's content instead.
|
|
1045
|
+
*/
|
|
1046
|
+
function withTooltip(node, label, maxWidth) {
|
|
1047
|
+
if (Tooltip === void 0) return node;
|
|
1048
|
+
return react_jsx_runtime.jsx(Tooltip, { side: "top", maxWidth, label, children: node });
|
|
1049
|
+
}
|
|
1050
|
+
|
|
923
1051
|
/**
|
|
924
1052
|
* Chip line in the input row: git state of the workspace the CURRENT
|
|
925
1053
|
* conversation is attached to. It targets the session id and lets the node
|
|
@@ -955,7 +1083,7 @@ window.__ModuleLoader__.load({
|
|
|
955
1083
|
const target = sessionId === void 0 ? void 0 : { kind: "session", id: sessionId };
|
|
956
1084
|
// the PR/CI token is always on the chip, so its fetch is not gated
|
|
957
1085
|
const info = useGitStatus(target, { pr: true });
|
|
958
|
-
// The card's extras cost a `log -
|
|
1086
|
+
// The card's extras cost a `log -10` plus a stash list, so they are
|
|
959
1087
|
// fetched only once a pointer actually RESTS on the chip, then kept
|
|
960
1088
|
// fresh by the same SSE path. Eagerly asking would add both invocations
|
|
961
1089
|
// to every refresh — and a refresh fires on every file edit.
|
|
@@ -980,45 +1108,52 @@ window.__ModuleLoader__.load({
|
|
|
980
1108
|
// so the extras are paid for exactly when the card that can render them
|
|
981
1109
|
// is about to open. Defined inside the component: it closes over the
|
|
982
1110
|
// hovered state that gates the detail fetch.
|
|
983
|
-
const hoverableMark = (
|
|
984
|
-
|
|
1111
|
+
const hoverableMark = withTooltip(
|
|
1112
|
+
react_jsx_runtime.jsx("span", {
|
|
985
1113
|
onPointerEnter: () => setHovered(true),
|
|
986
1114
|
style: { display: "inline-flex", alignItems: "center", flex: "none", cursor: "default" },
|
|
987
1115
|
children: react_jsx_runtime.jsx(StatusMark, { info })
|
|
988
|
-
})
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
// a function label keeps the card's element tree out of every render
|
|
995
|
-
// until the tooltip actually opens
|
|
996
|
-
label: () => react_jsx_runtime.jsx(HoverCard, { info, detail }),
|
|
997
|
-
children: mark
|
|
998
|
-
});
|
|
999
|
-
})();
|
|
1116
|
+
}),
|
|
1117
|
+
// a function label keeps the card's element tree out of every render
|
|
1118
|
+
// until the tooltip actually opens
|
|
1119
|
+
() => react_jsx_runtime.jsx(HoverCard, { info, detail }),
|
|
1120
|
+
600 // matches the body cap: the tooltip must never be the clamp
|
|
1121
|
+
);
|
|
1000
1122
|
// the mark is an element now rather than a leading glyph in the string, so
|
|
1001
1123
|
// the SAME StatusMark the sidebar row draws carries the status here too;
|
|
1002
1124
|
// the container's 4px gap supplies the space the emoji's own did
|
|
1003
1125
|
// The branch NAME is its own hover surface: the commits this branch adds
|
|
1004
1126
|
// (BranchCommitsList), sharing the mark's lazy detail fetch. The rest of
|
|
1005
1127
|
// the leading text (operation token) and the count keep their own
|
|
1006
|
-
// surfaces.
|
|
1007
|
-
//
|
|
1128
|
+
// surfaces. The wrapper is always mounted (see withTooltip); without the
|
|
1129
|
+
// Tooltip primitive the name renders plain.
|
|
1008
1130
|
const branchHover = (() => {
|
|
1009
1131
|
// The branch name links to the compare view (base...branch) — the web
|
|
1010
1132
|
// page of exactly what the lineage tooltip lists. Vouched http(s)
|
|
1011
1133
|
// only (the node half rebuilt the URL from parsed parts); new tab,
|
|
1012
1134
|
// never navigating the conversation away.
|
|
1013
1135
|
const isLink = typeof detail?.compareUrl === "string" && detail.compareUrl.startsWith("https://");
|
|
1014
|
-
|
|
1136
|
+
// The sync arrows ride WITH the branch: they are the branch's standing
|
|
1137
|
+
// against its upstream, so the same hover (lineage) and the same link
|
|
1138
|
+
// (compare view, where the arrows are the diff) cover them. The ✎n
|
|
1139
|
+
// count stays its own surface — it answers a different question.
|
|
1140
|
+
const sync = formatSync(info);
|
|
1141
|
+
const label = info.branch + sync;
|
|
1142
|
+
// The Tooltip's CHILD must not change element type once the detail
|
|
1143
|
+
// payload lands: the primitive opens on the child's mouseenter and has
|
|
1144
|
+
// no `open` prop, so a node replaced under the pointer may never be
|
|
1145
|
+
// re-entered. The outer span is the stable hover surface; only this
|
|
1146
|
+
// inner node swaps (plain text -> anchor) as the compare URL arrives.
|
|
1147
|
+
const inner = isLink
|
|
1015
1148
|
? react_jsx_runtime.jsx("a", {
|
|
1016
1149
|
href: detail.compareUrl,
|
|
1017
1150
|
target: "_blank",
|
|
1018
1151
|
rel: "noopener noreferrer",
|
|
1019
|
-
"aria-label": "Compare " + info.branch + " with main on GitHub",
|
|
1020
|
-
onPointerEnter: () =>
|
|
1152
|
+
"aria-label": "Compare " + info.branch + " with main on GitHub" + (sync === "" ? "" : ", " + (info.ahead || 0) + " ahead and " + (info.behind || 0) + " behind upstream"),
|
|
1153
|
+
onPointerEnter: () => setBranchLinkHover(true),
|
|
1021
1154
|
onPointerLeave: () => setBranchLinkHover(false),
|
|
1155
|
+
// focus joins hover: a keyboard user needs the same underline, and
|
|
1156
|
+
// focus (unlike a pointer) must also start the detail fetch
|
|
1022
1157
|
onFocus: () => { setHovered(true); setBranchLinkHover(true); },
|
|
1023
1158
|
onBlur: () => setBranchLinkHover(false),
|
|
1024
1159
|
style: {
|
|
@@ -1026,24 +1161,19 @@ window.__ModuleLoader__.load({
|
|
|
1026
1161
|
textDecoration: branchLinkHover ? "underline" : "none",
|
|
1027
1162
|
cursor: "pointer"
|
|
1028
1163
|
},
|
|
1029
|
-
children:
|
|
1164
|
+
children: label
|
|
1030
1165
|
})
|
|
1031
|
-
:
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
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
|
|
1166
|
+
: label;
|
|
1167
|
+
const branch = react_jsx_runtime.jsx("span", {
|
|
1168
|
+
onPointerEnter: () => setHovered(true),
|
|
1169
|
+
style: { cursor: isLink ? "pointer" : "default" },
|
|
1170
|
+
children: inner
|
|
1046
1171
|
});
|
|
1172
|
+
// ALWAYS wrapped. A wrapper mounted only once the lazy detail arrives
|
|
1173
|
+
// cannot open for the visit that fetched it (see the child-stability
|
|
1174
|
+
// note above); until the payload lands the lineage degrades to the base
|
|
1175
|
+
// response, the way the mark's own card already does.
|
|
1176
|
+
return withTooltip(branch, () => react_jsx_runtime.jsx(BranchLineage, { info: detail ?? info }), 480);
|
|
1047
1177
|
})();
|
|
1048
1178
|
const text = formatOperationToken(info);
|
|
1049
1179
|
// The ✎n count is its OWN hover surface: a names-only tooltip (see
|
|
@@ -1051,25 +1181,71 @@ window.__ModuleLoader__.load({
|
|
|
1051
1181
|
// either triggers it once — and degrades to a plain count on a shell
|
|
1052
1182
|
// without the Tooltip primitive.
|
|
1053
1183
|
const countHover = (() => {
|
|
1184
|
+
// ✎n only: the sync arrows moved to the branch's surface above
|
|
1185
|
+
const countText = formatFileCount(info);
|
|
1054
1186
|
const count = react_jsx_runtime.jsx("span", {
|
|
1055
1187
|
onPointerEnter: () => setHovered(true),
|
|
1056
1188
|
style: { cursor: "default" },
|
|
1057
|
-
children:
|
|
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
|
|
1189
|
+
children: countText
|
|
1069
1190
|
});
|
|
1191
|
+
// always wrapped, for the same reason as the branch: the primitive
|
|
1192
|
+
// opens on the child's mouseenter, so a wrapper added after the
|
|
1193
|
+
// pointer arrives never opens. While the names are in flight the label
|
|
1194
|
+
// restates the breakdown the base response already carries.
|
|
1195
|
+
const pending = detail === void 0 || detail === null;
|
|
1196
|
+
return withTooltip(count, () => react_jsx_runtime.jsx(CountNames, { info: detail ?? info, pending }), 480);
|
|
1070
1197
|
})();
|
|
1071
1198
|
const prToken = formatPrToken(info);
|
|
1072
1199
|
const prUrl = prLinkUrl(info);
|
|
1200
|
+
// The PR token, with its own hover: the commits IN THIS PR (see
|
|
1201
|
+
// PrCommitsList). Built here so the link/plain cases and the tooltip
|
|
1202
|
+
// compose once instead of nesting a third conditional in the children.
|
|
1203
|
+
// The token is its OWN detail gate: resting on it (or focusing it)
|
|
1204
|
+
// enables the same `detail=1` fetch the mark and the branch do. Without
|
|
1205
|
+
// that, a pointer going straight to the token had rested on nothing that
|
|
1206
|
+
// fetches, so `prCommits` was never requested and this hover could not
|
|
1207
|
+
// open at all — the data it renders is detail-only.
|
|
1208
|
+
const prTokenHover = (() => {
|
|
1209
|
+
const token = prUrl === void 0
|
|
1210
|
+
? react_jsx_runtime.jsx("span", {
|
|
1211
|
+
// the glyph is not the only channel: the token says what the
|
|
1212
|
+
// CI state IS, for anyone who cannot see it
|
|
1213
|
+
"aria-label": prTokenLabel(info),
|
|
1214
|
+
onPointerEnter: () => setHovered(true),
|
|
1215
|
+
style: { cursor: "default" },
|
|
1216
|
+
children: prToken
|
|
1217
|
+
})
|
|
1218
|
+
: react_jsx_runtime.jsx("a", {
|
|
1219
|
+
href: prUrl,
|
|
1220
|
+
// a new tab, because navigating THIS one away from the app
|
|
1221
|
+
// would lose the conversation; noopener/noreferrer keep the
|
|
1222
|
+
// opened tab from getting a handle on it
|
|
1223
|
+
target: "_blank",
|
|
1224
|
+
rel: "noopener noreferrer",
|
|
1225
|
+
"aria-label": prTokenLabel(info) + ", opens on GitHub in a new tab",
|
|
1226
|
+
// the chip's own colour rather than the browser's link blue /
|
|
1227
|
+
// visited purple, so the token still reads as one row. The
|
|
1228
|
+
// affordance is the pointer cursor plus an underline on hover
|
|
1229
|
+
// or focus, set HERE rather than left to the host stylesheet:
|
|
1230
|
+
// a shell that resets anchors would otherwise drop it silently.
|
|
1231
|
+
style: {
|
|
1232
|
+
color: "inherit",
|
|
1233
|
+
textDecoration: linkHover ? "underline" : "none",
|
|
1234
|
+
cursor: "pointer"
|
|
1235
|
+
},
|
|
1236
|
+
// focus joins hover — the token is keyboard-reachable now, and a
|
|
1237
|
+
// keyboard user needs the same "this is a link" signal
|
|
1238
|
+
onPointerEnter: () => { setHovered(true); setLinkHover(true); },
|
|
1239
|
+
onPointerLeave: () => setLinkHover(false),
|
|
1240
|
+
onFocus: () => { setHovered(true); setLinkHover(true); },
|
|
1241
|
+
onBlur: () => setLinkHover(false),
|
|
1242
|
+
children: prToken
|
|
1243
|
+
});
|
|
1244
|
+
// always wrapped (see the branch): while the commits are in flight the
|
|
1245
|
+
// label names the PR state the base response already carries
|
|
1246
|
+
const pending = detail === void 0 || detail === null;
|
|
1247
|
+
return withTooltip(token, () => react_jsx_runtime.jsx(PrCommitsList, { info: detail ?? info, pending }), 480);
|
|
1248
|
+
})();
|
|
1073
1249
|
const chip = react_jsx_runtime.jsxs("span", {
|
|
1074
1250
|
style: {
|
|
1075
1251
|
display: "inline-flex",
|
|
@@ -1103,43 +1279,7 @@ window.__ModuleLoader__.load({
|
|
|
1103
1279
|
branchHover,
|
|
1104
1280
|
react_jsx_runtime.jsx("span", { key: "text", children: text }),
|
|
1105
1281
|
countHover,
|
|
1106
|
-
|
|
1107
|
-
? null
|
|
1108
|
-
: prUrl === void 0
|
|
1109
|
-
? react_jsx_runtime.jsx("span", {
|
|
1110
|
-
key: "pr",
|
|
1111
|
-
// the glyph is not the only channel: the token says what the
|
|
1112
|
-
// CI state IS, for anyone who cannot see it
|
|
1113
|
-
"aria-label": prTokenLabel(info),
|
|
1114
|
-
children: prToken
|
|
1115
|
-
})
|
|
1116
|
-
: react_jsx_runtime.jsx("a", {
|
|
1117
|
-
key: "pr",
|
|
1118
|
-
href: prUrl,
|
|
1119
|
-
// a new tab, because navigating THIS one away from the app
|
|
1120
|
-
// would lose the conversation; noopener/noreferrer keep the
|
|
1121
|
-
// opened tab from getting a handle on it
|
|
1122
|
-
target: "_blank",
|
|
1123
|
-
rel: "noopener noreferrer",
|
|
1124
|
-
"aria-label": prTokenLabel(info) + ", opens on GitHub in a new tab",
|
|
1125
|
-
// the chip's own colour rather than the browser's link blue /
|
|
1126
|
-
// visited purple, so the token still reads as one row. The
|
|
1127
|
-
// affordance is the pointer cursor plus an underline on hover
|
|
1128
|
-
// or focus, set HERE rather than left to the host stylesheet:
|
|
1129
|
-
// a shell that resets anchors would otherwise drop it silently.
|
|
1130
|
-
style: {
|
|
1131
|
-
color: "inherit",
|
|
1132
|
-
textDecoration: linkHover ? "underline" : "none",
|
|
1133
|
-
cursor: "pointer"
|
|
1134
|
-
},
|
|
1135
|
-
// focus joins hover — the token is keyboard-reachable now, and a
|
|
1136
|
-
// keyboard user needs the same "this is a link" signal
|
|
1137
|
-
onPointerEnter: () => setLinkHover(true),
|
|
1138
|
-
onPointerLeave: () => setLinkHover(false),
|
|
1139
|
-
onFocus: () => setLinkHover(true),
|
|
1140
|
-
onBlur: () => setLinkHover(false),
|
|
1141
|
-
children: prToken
|
|
1142
|
-
}),
|
|
1282
|
+
prTokenHover,
|
|
1143
1283
|
/**
|
|
1144
1284
|
* The seam-absent notice: the market-install notification for the
|
|
1145
1285
|
* state a blocked postinstall leaves behind — chip working, sidebar
|
|
@@ -1424,7 +1564,12 @@ window.__ModuleLoader__.load({
|
|
|
1424
1564
|
info = await fetch("/api/git-badge?" + query).then((r) => r.json());
|
|
1425
1565
|
}
|
|
1426
1566
|
const actions = ghSkillActions(info);
|
|
1427
|
-
return actions.map((action) => ({
|
|
1567
|
+
return actions.map((action) => ({
|
|
1568
|
+
label: "/gh " + action.args,
|
|
1569
|
+
detail: action.why,
|
|
1570
|
+
args: action.args,
|
|
1571
|
+
...action.danger === true ? { confirmation: cleanGate() } : {}
|
|
1572
|
+
}));
|
|
1428
1573
|
} catch (error) {
|
|
1429
1574
|
console.error("[dsh-git-badge] /gh options failed:", error);
|
|
1430
1575
|
return [];
|
|
@@ -1667,7 +1812,7 @@ window.__ModuleLoader__.load({
|
|
|
1667
1812
|
// Additive; the host reads apply/inject and ignores the rest. The suite
|
|
1668
1813
|
// drives these to assert the REQUEST contract — which surface asks for the
|
|
1669
1814
|
// expensive extras — without a browser, a fetch or a network.
|
|
1670
|
-
exports.__internals = { targetQuery, formatPrToken, formatFileBreakdown, formatPrDetail, formatCheckoutDetail, worktreeDetail, actionToken, ghSkillActions, moveCategory };
|
|
1815
|
+
exports.__internals = { targetQuery, formatPrToken, formatFileBreakdown, formatPrDetail, formatCheckoutDetail, worktreeDetail, actionToken, ghSkillActions, cleanGate, moveCategory };
|
|
1671
1816
|
return module.exports;
|
|
1672
1817
|
}
|
|
1673
1818
|
});
|