dsh-git-badge 0.7.0 → 0.9.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 +148 -3
- package/README.md +12 -4
- package/lib/client.js +499 -107
- package/lib/index.js +623 -18
- package/package.json +8 -2
- package/seam/anchors.js +138 -0
- package/seam/apply.js +304 -0
- package/seam/stub-index.js +11 -0
package/lib/client.js
CHANGED
|
@@ -6,12 +6,23 @@
|
|
|
6
6
|
* identically wherever they appear.
|
|
7
7
|
*
|
|
8
8
|
* Surfaces:
|
|
9
|
-
* - sidebar.workspaces.
|
|
10
|
-
*
|
|
11
|
-
* branch
|
|
9
|
+
* - sidebar.workspaces.sessionRow (seam): session-row badge — the status mark
|
|
10
|
+
* and the PR/CI token, immediately after the session title. Deliberately shows
|
|
11
|
+
* NO branch and no worktree name: the row lists conversations, and which
|
|
12
|
+
* checkout the badge describes is the hover card's business.
|
|
13
|
+
* - sidebar.workspaces.sessionRow.detail (seam): the hover-card line that names
|
|
14
|
+
* that checkout, rendered inside the card upstream ALREADY shows for a session
|
|
15
|
+
* row rather than in a tooltip nested inside it.
|
|
12
16
|
* - conversation.input.left (upstream): chip — status mark + branch + a
|
|
13
|
-
* worktree's name + in-progress operation token + sync/dirty suffix
|
|
14
|
-
*
|
|
17
|
+
* worktree's name + in-progress operation token + sync/dirty suffix + the
|
|
18
|
+
* PR/CI token, with a HOVER CARD carrying the breakdown the chip has no room
|
|
19
|
+
* for (file detail, recent commits, stash, PR state, and the conversation's own
|
|
20
|
+
* checkout when the badge followed a worktree).
|
|
21
|
+
*
|
|
22
|
+
* The chip's hover card needs nothing but the upstream slot: the Tooltip
|
|
23
|
+
* primitive is seeded by the shell itself, so the card works on a PRISTINE
|
|
24
|
+
* install with no seam patch. Its extra fields are fetched only once a pointer
|
|
25
|
+
* rests on the chip, so the everyday badge pays for none of them.
|
|
15
26
|
*/
|
|
16
27
|
window.__ModuleLoader__.load({
|
|
17
28
|
id: "dsh-git-badge",
|
|
@@ -22,6 +33,24 @@ window.__ModuleLoader__.load({
|
|
|
22
33
|
let react_jsx_runtime = require("react/jsx-runtime");
|
|
23
34
|
let react = require("react");
|
|
24
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Shell-seeded UI primitives. `Tooltip` is handed to EVERY client bundle
|
|
38
|
+
* by the web shell itself — it needs no `dsh.client.inject` entry and no
|
|
39
|
+
* npm dependency (dsh-client-ui-open-in-app requires it the same way).
|
|
40
|
+
*
|
|
41
|
+
* Guarded, because the seed table belongs to the shell and not to this
|
|
42
|
+
* plugin: on a shell that does not provide it the chip must still render,
|
|
43
|
+
* just without the hover card. A missing card is a smaller failure than a
|
|
44
|
+
* missing badge.
|
|
45
|
+
*/
|
|
46
|
+
let primitives = null;
|
|
47
|
+
try {
|
|
48
|
+
primitives = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
49
|
+
} catch {
|
|
50
|
+
primitives = null;
|
|
51
|
+
}
|
|
52
|
+
const Tooltip = primitives === null ? void 0 : primitives.Tooltip;
|
|
53
|
+
|
|
25
54
|
/** Module-level git-status cache. Entries are invalidated by SSE events, never by age. */
|
|
26
55
|
const GIT_CACHE = new Map();
|
|
27
56
|
/** In-flight fetches per cacheKey: bursts collapse into one request. */
|
|
@@ -35,11 +64,21 @@ window.__ModuleLoader__.load({
|
|
|
35
64
|
* one. The chip targets its conversation (`session`); a sidebar row
|
|
36
65
|
* targets the workspace id the seam hands it. The query string doubles as
|
|
37
66
|
* the cache key.
|
|
67
|
+
*
|
|
68
|
+
* The two optional additions are the INPUT CHIP's alone, and each is an
|
|
69
|
+
* explicit request rather than something every badge pays for:
|
|
70
|
+
* - `detail=1` — recent commits and stash for the hover card;
|
|
71
|
+
* - `pr=1` — the branch's GitHub PR/CI state.
|
|
72
|
+
* A sidebar row asks for neither, so surveying N workspaces never costs N
|
|
73
|
+
* forge calls (see the node half's PR region).
|
|
38
74
|
*/
|
|
39
|
-
function targetQuery(target) {
|
|
75
|
+
function targetQuery(target, options) {
|
|
40
76
|
if (target === void 0 || target.id === void 0) return void 0;
|
|
41
77
|
const param = target.kind === "workspace" ? "workspace" : "session";
|
|
42
|
-
|
|
78
|
+
let query = param + "=" + encodeURIComponent(target.id);
|
|
79
|
+
if (options?.pr === true) query += "&pr=1";
|
|
80
|
+
if (options?.detail === true) query += "&detail=1";
|
|
81
|
+
return query;
|
|
43
82
|
}
|
|
44
83
|
|
|
45
84
|
//#region SSE change feed (one EventSource per page, ref-counted)
|
|
@@ -90,16 +129,24 @@ window.__ModuleLoader__.load({
|
|
|
90
129
|
* node half's watcher reports a change for this target (SSE), plus a slow
|
|
91
130
|
* safety-net poll in case the stream dies silently. Returns undefined
|
|
92
131
|
* while loading and for non-git workspaces.
|
|
132
|
+
*
|
|
133
|
+
* `options.detail` / `options.pr` ask the node half for the chip's extras
|
|
134
|
+
* (see targetQuery). `options.enabled: false` holds the fetch back
|
|
135
|
+
* entirely — that is what makes the hover card's extra fields LAZY: the
|
|
136
|
+
* chip renders without them and only starts paying for `log -3` plus a
|
|
137
|
+
* stash list once a pointer actually rests on it. The hook is still called
|
|
138
|
+
* unconditionally (hooks may not be conditional); the GATE is inside.
|
|
93
139
|
*/
|
|
94
|
-
function useGitStatus(target) {
|
|
95
|
-
const
|
|
140
|
+
function useGitStatus(target, options) {
|
|
141
|
+
const enabled = options?.enabled !== false;
|
|
142
|
+
const cacheKey = targetQuery(target, options);
|
|
96
143
|
const hit = cacheKey === void 0 ? void 0 : GIT_CACHE.get(cacheKey);
|
|
97
144
|
// state carries the key it belongs to: switching conversations changes
|
|
98
145
|
// the key, and showing the previous session's badge until the new fetch
|
|
99
146
|
// lands would be wrong
|
|
100
147
|
const [state, setState] = react.useState({ key: cacheKey, data: hit === void 0 ? void 0 : hit.data });
|
|
101
148
|
react.useEffect(() => {
|
|
102
|
-
if (cacheKey === void 0) return;
|
|
149
|
+
if (cacheKey === void 0 || !enabled) return;
|
|
103
150
|
let alive = true;
|
|
104
151
|
// The workspace this target resolved to, taken from the response (the
|
|
105
152
|
// server echoes it). THIS is what events are matched against: an event
|
|
@@ -149,7 +196,7 @@ window.__ModuleLoader__.load({
|
|
|
149
196
|
unsubscribe();
|
|
150
197
|
clearInterval(fallback);
|
|
151
198
|
};
|
|
152
|
-
}, [cacheKey]);
|
|
199
|
+
}, [cacheKey, enabled]);
|
|
153
200
|
return state.key === cacheKey ? state.data : void 0;
|
|
154
201
|
}
|
|
155
202
|
|
|
@@ -222,6 +269,57 @@ window.__ModuleLoader__.load({
|
|
|
222
269
|
return parts.length > 0 ? " " + parts.join(" ") : "";
|
|
223
270
|
}
|
|
224
271
|
|
|
272
|
+
/**
|
|
273
|
+
* PR / CI token — INPUT CHIP ONLY, and absent entirely when the node half
|
|
274
|
+
* has nothing to say (no `gh`, no GitHub remote, no PR for the branch), so
|
|
275
|
+
* a machine without `gh` sees precisely the chip it saw before.
|
|
276
|
+
*
|
|
277
|
+
* The state glyph is not the only channel: the token carries an accessible
|
|
278
|
+
* name spelling the state out, the same rule the status mark follows.
|
|
279
|
+
*/
|
|
280
|
+
const PR_STATE_GLYPH = { passing: "\u2713", pending: "\u2026", failing: "\u2717", none: "" };
|
|
281
|
+
const PR_STATE_LABEL = { passing: "checks passing", pending: "checks running", failing: "checks failing", none: "no checks" };
|
|
282
|
+
|
|
283
|
+
/** ` PR#142 ✗` (plus ` draft`), or "" when there is no PR to report. */
|
|
284
|
+
function formatPrToken(info) {
|
|
285
|
+
const pr = info.pr;
|
|
286
|
+
if (pr === void 0 || pr === null || pr.number === void 0) return "";
|
|
287
|
+
const state = PR_STATE_GLYPH[pr.state] === void 0 ? "none" : pr.state;
|
|
288
|
+
const glyph = PR_STATE_GLYPH[state];
|
|
289
|
+
return " PR#" + pr.number + (glyph === "" ? "" : " " + glyph) + (pr.draft === true ? " draft" : "");
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/** The token's accessible name: "pull request 142, checks failing, draft". */
|
|
293
|
+
function prTokenLabel(info) {
|
|
294
|
+
const pr = info.pr;
|
|
295
|
+
if (pr === void 0 || pr === null || pr.number === void 0) return void 0;
|
|
296
|
+
const state = PR_STATE_LABEL[pr.state] === void 0 ? PR_STATE_LABEL.none : PR_STATE_LABEL[pr.state];
|
|
297
|
+
return "pull request " + pr.number + ", " + state + (pr.draft === true ? ", draft" : "");
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* The PR's web URL, or undefined — the token is a LINK only when the node
|
|
302
|
+
* half vouched for a URL, and is plain text otherwise (no `gh`, no GitHub
|
|
303
|
+
* remote, an unauthenticated `gh`: the token still appears, it just does not
|
|
304
|
+
* pretend to be clickable, so there is no dead link to discover).
|
|
305
|
+
*
|
|
306
|
+
* The protocol is re-checked HERE even though the node half already dropped
|
|
307
|
+
* a non-http(s) value: an `href` is the one place a payload string becomes
|
|
308
|
+
* executable, so the element that creates it verifies its own input rather
|
|
309
|
+
* than trusting an upstream guard to stay in place. The shell's own markdown
|
|
310
|
+
* renderer guards its links the same way.
|
|
311
|
+
*/
|
|
312
|
+
function prLinkUrl(info) {
|
|
313
|
+
const pr = info.pr;
|
|
314
|
+
if (pr === void 0 || pr === null || typeof pr.url !== "string") return void 0;
|
|
315
|
+
try {
|
|
316
|
+
const { protocol } = new URL(pr.url);
|
|
317
|
+
return protocol === "http:" || protocol === "https:" ? pr.url : void 0;
|
|
318
|
+
} catch {
|
|
319
|
+
return void 0;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
225
323
|
/**
|
|
226
324
|
* In-progress operation token — INPUT CHIP ONLY (the row shows the status
|
|
227
325
|
* tree alone). A paused rebase or cherry-pick whose conflicts are all
|
|
@@ -246,43 +344,12 @@ window.__ModuleLoader__.load({
|
|
|
246
344
|
}
|
|
247
345
|
|
|
248
346
|
/**
|
|
249
|
-
*
|
|
250
|
-
*
|
|
251
|
-
*
|
|
252
|
-
*
|
|
253
|
-
*
|
|
254
|
-
* reports whether this is a linked worktree plus its directory NAME (never a
|
|
255
|
-
* path).
|
|
256
|
-
*
|
|
257
|
-
* The name is appended only when it says something the branch does not: the
|
|
258
|
-
* usual `repo-feat-x` directory sitting on branch `feat-x` would otherwise
|
|
259
|
-
* read as stutter. (The ROW needs no such rule — it shows the name instead
|
|
260
|
-
* of the branch, so there is nothing to stutter against.)
|
|
261
|
-
*/
|
|
262
|
-
|
|
263
|
-
/**
|
|
264
|
-
* True when the worktree name is already implied by the branch name, so
|
|
265
|
-
* showing both is pure repetition. Compared on a normalized form —
|
|
266
|
-
* case-folded, with `/` and `_` folded to `-` — because directory and
|
|
267
|
-
* branch conventions differ without changing the meaning (`feat/x` in a
|
|
268
|
-
* `repo-feat-x` directory).
|
|
347
|
+
* NOTE: a worktree's NAME is deliberately NOT part of the chip's text. It is
|
|
348
|
+
* long, it competes with the branch for the same glance, and the chip already
|
|
349
|
+
* says THAT the checkout is a worktree through the mark's shape. WHICH tree it
|
|
350
|
+
* is belongs to the hover card's `worktree` row — see worktreeDetail. (The
|
|
351
|
+
* old append-unless-redundant rule lived here; it is gone with the name.)
|
|
269
352
|
*/
|
|
270
|
-
function worktreeNameIsRedundant(name, branch) {
|
|
271
|
-
if (typeof name !== "string" || name === "" || typeof branch !== "string") return true;
|
|
272
|
-
const normalize = (value) => value.toLowerCase().replace(/[/_]/g, "-");
|
|
273
|
-
const normalizedName = normalize(name);
|
|
274
|
-
const normalizedBranch = normalize(branch);
|
|
275
|
-
return normalizedName === normalizedBranch
|
|
276
|
-
|| normalizedName.endsWith("-" + normalizedBranch)
|
|
277
|
-
|| normalizedBranch.endsWith("-" + normalizedName);
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
/** ` name` for a worktree whose name the branch does not already say, else "". */
|
|
281
|
-
function formatWorktreeToken(info) {
|
|
282
|
-
if (info.isWorktree !== true) return "";
|
|
283
|
-
const name = typeof info.worktreeName === "string" ? info.worktreeName : "";
|
|
284
|
-
return worktreeNameIsRedundant(name, info.branch) ? "" : " " + name;
|
|
285
|
-
}
|
|
286
353
|
|
|
287
354
|
const META_STYLE = {
|
|
288
355
|
color: "var(--dsw-alias-label-tertiary, #9ea7ad)",
|
|
@@ -293,7 +360,7 @@ window.__ModuleLoader__.load({
|
|
|
293
360
|
};
|
|
294
361
|
|
|
295
362
|
/**
|
|
296
|
-
* Status mark, SHARED BY BOTH SURFACES: the same
|
|
363
|
+
* Status mark, SHARED BY BOTH SURFACES: the same shape in the same fill
|
|
297
364
|
* wherever it appears, so the sidebar row and the input chip read
|
|
298
365
|
* identically. The FILL is the status colour; the SHAPE carries
|
|
299
366
|
* worktree-ness — a circle for a main checkout, a tree for a linked
|
|
@@ -318,7 +385,12 @@ window.__ModuleLoader__.load({
|
|
|
318
385
|
height: 14,
|
|
319
386
|
viewBox: "0 0 12 12",
|
|
320
387
|
role: "img",
|
|
321
|
-
"aria-label":
|
|
388
|
+
"aria-label":
|
|
389
|
+
(isWorktree ? "git worktree: " : "git: ") +
|
|
390
|
+
STATUS_LABEL[status] +
|
|
391
|
+
// an inferred checkout is not where the conversation lives, so the
|
|
392
|
+
// accessible name says where the fact came from
|
|
393
|
+
(info.worktreeInferred === true ? " (inferred from an open pull request in this repository)" : ""),
|
|
322
394
|
focusable: "false",
|
|
323
395
|
style: { flex: "none", display: "block", color: META_STYLE.color }
|
|
324
396
|
};
|
|
@@ -342,52 +414,271 @@ window.__ModuleLoader__.load({
|
|
|
342
414
|
}
|
|
343
415
|
|
|
344
416
|
/**
|
|
345
|
-
*
|
|
346
|
-
*
|
|
347
|
-
*
|
|
348
|
-
*
|
|
349
|
-
*
|
|
350
|
-
*
|
|
417
|
+
* The ONE thing this conversation needs from you, or "" when it needs
|
|
418
|
+
* nothing.
|
|
419
|
+
*
|
|
420
|
+
* A session row's job is TRIAGE — "does this need me?" — so this is not the
|
|
421
|
+
* chip's status restated: it is a short imperative, present only when an
|
|
422
|
+
* action is genuinely warranted, first match wins. Silence is the common and
|
|
423
|
+
* correct answer: uncommitted work is a state rather than a chore, and DRAFT,
|
|
424
|
+
* BLOCKED, BEHIND, UNSTABLE and UNKNOWN are things to wait for, not to do.
|
|
425
|
+
*
|
|
426
|
+
* `merge` keys on GitHub's OWN verdict (`mergeStateStatus === "CLEAN"`) rather
|
|
427
|
+
* than a checks-plus-reviews judgement assembled here: mergeability depends on
|
|
428
|
+
* branch protection and required reviews, which is the forge's business. A PR
|
|
429
|
+
* read that could not answer has no `mergeState`, so it stays silent.
|
|
351
430
|
*/
|
|
352
|
-
function
|
|
353
|
-
if (info.
|
|
354
|
-
|
|
355
|
-
|
|
431
|
+
function actionToken(info) {
|
|
432
|
+
if (info === void 0 || info === null || info.git !== true) return "";
|
|
433
|
+
if ((info.unmergedFiles || 0) > 0) return "resolve";
|
|
434
|
+
const pr = info.pr === void 0 || info.pr === null ? void 0 : info.pr;
|
|
435
|
+
if (pr !== void 0) {
|
|
436
|
+
if (pr.state === "failing") return "fix CI";
|
|
437
|
+
if (pr.review === "CHANGES_REQUESTED") return "review";
|
|
438
|
+
if (pr.mergeState === "CLEAN") return "merge";
|
|
439
|
+
}
|
|
440
|
+
// diverged counts as behind: reconciling is the action either way
|
|
441
|
+
if ((info.behind || 0) > 0) return "pull";
|
|
442
|
+
if ((info.ahead || 0) > 0) return "push";
|
|
443
|
+
return "";
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Session-row badge — the ACTION, and nothing else:
|
|
448
|
+
* Session title… merge
|
|
449
|
+
*
|
|
450
|
+
* An experiment in what a sidebar row is FOR. It used to carry the chip's mark
|
|
451
|
+
* plus a PR token; it now answers exactly one question — does this conversation
|
|
452
|
+
* need me? — in one word, and says nothing otherwise. The chip keeps the mark,
|
|
453
|
+
* the branch, the counts and the PR/CI token, because that is the surface for
|
|
454
|
+
* "where am I, and what is the state".
|
|
455
|
+
*
|
|
456
|
+
* Consequences, all deliberate: the row no longer shows local status (clean /
|
|
457
|
+
* dirty / ahead / behind) at a glance, no longer carries the worktree's tree
|
|
458
|
+
* SHAPE, and a silent row cannot distinguish "nothing to do" from "no PR" or
|
|
459
|
+
* "no gh". Those answers live in the hover line below, which also names the
|
|
460
|
+
* pull request the action refers to.
|
|
461
|
+
*
|
|
462
|
+
* Targets the row's `sessionId`: the node half resolves the session to the
|
|
463
|
+
* checkout it is working in. `workspaceId` arrives in the owner share and is
|
|
464
|
+
* deliberately unused — the flat and search lists render without one, which is
|
|
465
|
+
* what keeps badges off them.
|
|
466
|
+
*
|
|
467
|
+
* `pr: true` is what makes an action possible at all (`merge` especially). It
|
|
468
|
+
* costs one forge read per (repository, branch) per TTL, and the detail line
|
|
469
|
+
* below reuses THIS request rather than issuing its own.
|
|
470
|
+
*/
|
|
471
|
+
/**
|
|
472
|
+
* Action -> colour. Colour here means SEVERITY — how much this needs you —
|
|
473
|
+
* rather than identity: a broken build and a conflict are the loud ones, a
|
|
474
|
+
* requested review is a nudge, `merge` is the all-clear, and routine sync
|
|
475
|
+
* stays quiet so the loud rows keep meaning something. The WORD remains the
|
|
476
|
+
* channel (colour only reinforces it), and the tokens are the app's own, so
|
|
477
|
+
* both themes work.
|
|
478
|
+
*/
|
|
479
|
+
const ACTION_COLOUR = {
|
|
480
|
+
resolve: "var(--dsw-alias-state-error-primary, #e5484d)",
|
|
481
|
+
"fix CI": "var(--dsw-alias-state-error-primary, #e5484d)",
|
|
482
|
+
review: "var(--dsw-alias-state-warn-primary, #d29922)",
|
|
483
|
+
merge: "var(--dsw-alias-state-success-primary, #30a46c)",
|
|
484
|
+
pull: "var(--dsw-alias-label-secondary, #5b6570)",
|
|
485
|
+
push: "var(--dsw-alias-label-secondary, #5b6570)"
|
|
486
|
+
};
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* The action's own style, on top of META_STYLE:
|
|
490
|
+
* - `marginLeft: auto` floats it right, so a column of actions lines up down
|
|
491
|
+
* the sidebar — that alignment is what makes the list scannable;
|
|
492
|
+
* - `marginRight` keeps it clear of the relative time beside it (the first cut
|
|
493
|
+
* sat flush and read `merge11m`);
|
|
494
|
+
* - weight 500 rather than the timestamp's own treatment, because an action
|
|
495
|
+
* rendered in tertiary grey reads as metadata.
|
|
496
|
+
*/
|
|
497
|
+
const ACTION_STYLE = { flex: "none", marginLeft: "auto", marginRight: "8px", fontWeight: 500 };
|
|
498
|
+
|
|
499
|
+
/** The action badge: floated, weighted, and coloured by severity. */
|
|
500
|
+
function SessionGitBadge({ sessionId }) {
|
|
501
|
+
const info = useGitStatus(sessionId === void 0 ? void 0 : { kind: "session", id: sessionId }, { pr: true });
|
|
502
|
+
if (sessionId === void 0 || info === void 0 || info.git !== true) return null;
|
|
503
|
+
const action = actionToken(info);
|
|
504
|
+
if (action === "") return null;
|
|
505
|
+
const colour = ACTION_COLOUR[action];
|
|
506
|
+
return react_jsx_runtime.jsx("span", {
|
|
507
|
+
style: colour === void 0 ? { ...META_STYLE, ...ACTION_STYLE } : { ...META_STYLE, ...ACTION_STYLE, color: colour },
|
|
508
|
+
children: action
|
|
509
|
+
});
|
|
356
510
|
}
|
|
357
511
|
|
|
358
512
|
/**
|
|
359
|
-
*
|
|
360
|
-
*
|
|
361
|
-
*
|
|
362
|
-
*
|
|
363
|
-
*
|
|
364
|
-
*
|
|
365
|
-
*
|
|
366
|
-
*
|
|
367
|
-
*
|
|
513
|
+
* Session-row hover-card line — everything the action word cannot say: which
|
|
514
|
+
* checkout the row's status describes, and which pull request the action
|
|
515
|
+
* refers to. It renders through the sessionRow.detail seam, inside the card
|
|
516
|
+
* upstream already opens for a row.
|
|
517
|
+
*
|
|
518
|
+
* This matters MORE now that the row shows only an imperative: `merge` with no
|
|
519
|
+
* way to see merge WHAT would be a nag rather than a hint, so the line appears
|
|
520
|
+
* whenever there is something to name — a worktree, a pull request, or both.
|
|
521
|
+
*
|
|
522
|
+
* It makes no request of its own: the same `{ pr: true }` query as the badge
|
|
523
|
+
* above resolves to the same cache key, so a card that mounts for every row
|
|
524
|
+
* still costs one fetch per row, and a cold cache is served by the badge's
|
|
525
|
+
* own in-flight request. That is why the badge must keep asking for `pr=1`.
|
|
368
526
|
*
|
|
369
|
-
*
|
|
370
|
-
*
|
|
371
|
-
*
|
|
372
|
-
|
|
373
|
-
|
|
527
|
+
* Only a WORKTREE's checkout is spelled out. For a main checkout the branch is
|
|
528
|
+
* the ordinary case and repeating it on every session's card would be noise;
|
|
529
|
+
* a tree, by contrast, is exactly what the row's own title cannot tell you.
|
|
530
|
+
*/
|
|
531
|
+
function SessionGitDetail({ sessionId }) {
|
|
532
|
+
const info = useGitStatus(sessionId === void 0 ? void 0 : { kind: "session", id: sessionId }, { pr: true });
|
|
533
|
+
if (info === void 0 || info.git !== true) return null;
|
|
534
|
+
const bits = [];
|
|
535
|
+
if (info.isWorktree === true) {
|
|
536
|
+
const name = typeof info.worktreeName === "string" ? info.worktreeName : "";
|
|
537
|
+
const checkout = [name === "" ? String(info.branch) : name + " on " + info.branch];
|
|
538
|
+
// the node half followed this tree because it is the one whose branch
|
|
539
|
+
// has the open PR — say so, because nothing on the row can
|
|
540
|
+
if (info.worktreeInferred === true) checkout.push("its branch has the open pull request");
|
|
541
|
+
bits.push("checkout: " + checkout.join(" \u00B7 "));
|
|
542
|
+
}
|
|
543
|
+
const pr = formatPrDetail(info.pr);
|
|
544
|
+
if (pr !== void 0) bits.push("pull request " + pr);
|
|
545
|
+
return bits.length === 0 ? null : react_jsx_runtime.jsx("span", { style: META_STYLE, children: bits.join(" \u00B7 ") });
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
/**
|
|
549
|
+
* The chip's `✎n` is one number for four different things; the card splits
|
|
550
|
+
* it, so "ready to commit" is distinguishable from "not staged yet". The
|
|
551
|
+
* collapsed caveat is the node half's own honesty flag: when the untracked
|
|
552
|
+
* walk blew its budget the count is a DIRECTORY count, and saying so beats
|
|
553
|
+
* presenting an under-count as exact.
|
|
554
|
+
*/
|
|
555
|
+
function formatFileBreakdown(info) {
|
|
556
|
+
const parts = [];
|
|
557
|
+
if ((info.stagedFiles || 0) > 0) parts.push(info.stagedFiles + " staged");
|
|
558
|
+
if ((info.unstagedFiles || 0) > 0) parts.push(info.unstagedFiles + " unstaged");
|
|
559
|
+
if ((info.unmergedFiles || 0) > 0) parts.push(info.unmergedFiles + " unmerged");
|
|
560
|
+
const untracked = info.untrackedFiles || 0;
|
|
561
|
+
if (untracked > 0) parts.push(untracked + " untracked" + (info.untrackedMode === "collapsed" ? " (collapsed)" : ""));
|
|
562
|
+
return parts.length === 0 ? "clean" : parts.join(" \u00B7 ");
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/** The PR's full state as text, for the card: "#142 · checks failing · draft · review approved". */
|
|
566
|
+
function formatPrDetail(pr) {
|
|
567
|
+
if (pr === void 0 || pr === null || pr.number === void 0) return void 0;
|
|
568
|
+
const state = PR_STATE_LABEL[pr.state] === void 0 ? PR_STATE_LABEL.none : PR_STATE_LABEL[pr.state];
|
|
569
|
+
const bits = ["#" + pr.number, state];
|
|
570
|
+
if (pr.draft === true) bits.push("draft");
|
|
571
|
+
if (pr.review !== void 0) bits.push(String(pr.review).toLowerCase().replace(/_/g, " "));
|
|
572
|
+
if (pr.open === false) bits.push("not open");
|
|
573
|
+
return bits.join(" \u00B7 ");
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/**
|
|
577
|
+
* The `checkout` row: the directory the CONVERSATION itself names, present
|
|
578
|
+
* only when the badge is describing an inferred worktree instead. The chip's
|
|
579
|
+
* branch and counts are the tree's in that case, so without this row the
|
|
580
|
+
* checkout's own state is nowhere on screen. Branch, then files (or "clean"),
|
|
581
|
+
* then sync counts only when they are nonzero — the chip's own rule.
|
|
374
582
|
*/
|
|
375
|
-
function
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
const
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
// marginLeft:auto floats the mark to the right edge, so the status
|
|
382
|
-
// shape holds a fixed right-hand column and a worktree's name grows
|
|
383
|
-
// leftwards from it instead of shoving the shape around.
|
|
384
|
-
const meta = [];
|
|
385
|
-
const name = rowName(info);
|
|
386
|
-
if (name !== null) meta.push(react_jsx_runtime.jsx("span", { key: "name", style: META_STYLE, children: name }));
|
|
387
|
-
meta.push(react_jsx_runtime.jsx(StatusMark, { key: "mark", info }));
|
|
388
|
-
children.push(react_jsx_runtime.jsx("span", { style: { display: "inline-flex", alignItems: "center", gap: "6px", flex: "none", marginLeft: "auto", paddingLeft: "8px" }, children: meta }));
|
|
583
|
+
function formatCheckoutDetail(checkout) {
|
|
584
|
+
if (checkout === void 0 || checkout === null || typeof checkout.branch !== "string") return void 0;
|
|
585
|
+
const files = (checkout.changedFiles || 0) + (checkout.untrackedFiles || 0);
|
|
586
|
+
const bits = [checkout.branch, files > 0 ? "\u270E" + files : "clean"];
|
|
587
|
+
if ((checkout.ahead || 0) > 0 || (checkout.behind || 0) > 0) {
|
|
588
|
+
bits.push("\u2191" + (checkout.ahead || 0) + " \u2193" + (checkout.behind || 0));
|
|
389
589
|
}
|
|
390
|
-
return
|
|
590
|
+
return bits.join(" \u00B7 ");
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
/**
|
|
594
|
+
* The `worktree` row: WHICH linked worktree the badge is describing. The chip
|
|
595
|
+
* names only the branch (see the note above formatOperationToken), so the
|
|
596
|
+
* card is the one place a tree is identified — the mark's shape can only say
|
|
597
|
+
* *that* the checkout is a worktree, never which. An inferred follow also says
|
|
598
|
+
* why, because "which tree" and "why this tree" are different questions and
|
|
599
|
+
* the second is the one a reader will ask.
|
|
600
|
+
*/
|
|
601
|
+
function worktreeDetail(info) {
|
|
602
|
+
if (info === void 0 || info === null || info.isWorktree !== true) return void 0;
|
|
603
|
+
const name = typeof info.worktreeName === "string" ? info.worktreeName : "";
|
|
604
|
+
if (name === "") return void 0;
|
|
605
|
+
return info.worktreeInferred === true ? name + " (inferred from its open pull request)" : name;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
const CARD_CONTAINER = {
|
|
609
|
+
display: "flex",
|
|
610
|
+
flexDirection: "column",
|
|
611
|
+
gap: "4px",
|
|
612
|
+
fontSize: "12px",
|
|
613
|
+
lineHeight: "18px",
|
|
614
|
+
maxWidth: "340px"
|
|
615
|
+
};
|
|
616
|
+
const CARD_ROW = { display: "flex", gap: "8px", alignItems: "baseline" };
|
|
617
|
+
const CARD_LABEL = { color: "var(--dsw-alias-label-tertiary, #9ea7ad)", flex: "none", minWidth: "62px" };
|
|
618
|
+
const CARD_VALUE = { minWidth: 0, overflowWrap: "anywhere" };
|
|
619
|
+
|
|
620
|
+
/**
|
|
621
|
+
* Hover card body for the input chip — INPUT CHIP ONLY. Pure presentation
|
|
622
|
+
* over fields the status response already carries, plus the `detail=1`
|
|
623
|
+
* additions (recent commits, stash) when they have arrived. Everything is
|
|
624
|
+
* spelled out in text, so no fact here depends on a colour or a glyph.
|
|
625
|
+
*/
|
|
626
|
+
function HoverCard({ info, detail }) {
|
|
627
|
+
// the detail payload is a superset of the plain one, so it wins when it
|
|
628
|
+
// has landed; until then the card still shows the base facts
|
|
629
|
+
const data = detail === void 0 ? info : detail;
|
|
630
|
+
const rows = [];
|
|
631
|
+
const add = (label, value) => {
|
|
632
|
+
if (value === void 0 || value === null || value === "") return;
|
|
633
|
+
rows.push(
|
|
634
|
+
react_jsx_runtime.jsxs("div", {
|
|
635
|
+
style: CARD_ROW,
|
|
636
|
+
children: [
|
|
637
|
+
react_jsx_runtime.jsx("span", { style: CARD_LABEL, children: label }),
|
|
638
|
+
react_jsx_runtime.jsx("span", { style: CARD_VALUE, children: value })
|
|
639
|
+
]
|
|
640
|
+
}, label)
|
|
641
|
+
);
|
|
642
|
+
};
|
|
643
|
+
add("branch", info.branch);
|
|
644
|
+
add("upstream", info.upstream === void 0 ? "none configured" : info.upstream);
|
|
645
|
+
if (info.ahead !== void 0 || info.behind !== void 0) {
|
|
646
|
+
add("sync", "\u2191" + (info.ahead || 0) + " \u2193" + (info.behind || 0));
|
|
647
|
+
}
|
|
648
|
+
add("files", formatFileBreakdown(info));
|
|
649
|
+
add("operation", info.operation === void 0 || info.operation === null ? void 0 : String(info.operation));
|
|
650
|
+
add("pull request", formatPrDetail(data.pr));
|
|
651
|
+
// WHICH checkout the badge describes, when it is a worktree. The chip shows
|
|
652
|
+
// the branch alone, so this row is the only place the tree is named.
|
|
653
|
+
add("worktree", worktreeDetail(data));
|
|
654
|
+
// The conversation's OWN directory, present only when the badge is
|
|
655
|
+
// describing an inferred worktree — the chip's branch and counts are the
|
|
656
|
+
// tree's in that case, so this is where the checkout's own state stays
|
|
657
|
+
// visible. Absent until the `detail=1` response lands, like commits/stash.
|
|
658
|
+
if (data.worktreeInferred === true) add("checkout", formatCheckoutDetail(data.checkout));
|
|
659
|
+
if (Array.isArray(data.lastCommits) && data.lastCommits.length > 0) {
|
|
660
|
+
rows.push(
|
|
661
|
+
react_jsx_runtime.jsxs("div", {
|
|
662
|
+
style: CARD_ROW,
|
|
663
|
+
children: [
|
|
664
|
+
react_jsx_runtime.jsx("span", { style: CARD_LABEL, children: "commits" }),
|
|
665
|
+
react_jsx_runtime.jsx("span", {
|
|
666
|
+
style: { ...CARD_VALUE, display: "flex", flexDirection: "column", gap: "2px" },
|
|
667
|
+
children: data.lastCommits.map((commit) =>
|
|
668
|
+
react_jsx_runtime.jsxs("span", {
|
|
669
|
+
children: [
|
|
670
|
+
react_jsx_runtime.jsx("span", { style: CARD_LABEL, children: commit.hash }),
|
|
671
|
+
" " + commit.subject + (commit.when === "" ? "" : " \u00B7 " + commit.when)
|
|
672
|
+
]
|
|
673
|
+
}, commit.hash)
|
|
674
|
+
)
|
|
675
|
+
})
|
|
676
|
+
]
|
|
677
|
+
}, "commits")
|
|
678
|
+
);
|
|
679
|
+
}
|
|
680
|
+
add("stash", data.stashCount === void 0 ? void 0 : data.stashCount + " stashed");
|
|
681
|
+
return react_jsx_runtime.jsx("div", { style: CARD_CONTAINER, children: rows });
|
|
391
682
|
}
|
|
392
683
|
|
|
393
684
|
//#region composer chip (upstream additive surface: conversation.input.left)
|
|
@@ -399,33 +690,103 @@ window.__ModuleLoader__.load({
|
|
|
399
690
|
* (conversation.input.left is an upstream additive slot), and stays useful
|
|
400
691
|
* next to the sidebar rows because it is context-anchored ("where am I")
|
|
401
692
|
* rather than surveying.
|
|
693
|
+
*
|
|
694
|
+
* This is the ONLY surface that asks the node half for the extras (`pr=1`
|
|
695
|
+
* always, `detail=1` on hover), because it is the only one that renders
|
|
696
|
+
* them — see targetQuery.
|
|
402
697
|
*/
|
|
403
698
|
function ComposerGitChip({ sessionId }) {
|
|
404
|
-
|
|
405
|
-
//
|
|
406
|
-
const info = useGitStatus(
|
|
699
|
+
const target = sessionId === void 0 ? void 0 : { kind: "session", id: sessionId };
|
|
700
|
+
// the PR/CI token is always on the chip, so its fetch is not gated
|
|
701
|
+
const info = useGitStatus(target, { pr: true });
|
|
702
|
+
// The card's extras cost a `log -3` plus a stash list, so they are
|
|
703
|
+
// fetched only once a pointer actually RESTS on the chip, then kept
|
|
704
|
+
// fresh by the same SSE path. Eagerly asking would add both invocations
|
|
705
|
+
// to every refresh — and a refresh fires on every file edit.
|
|
706
|
+
const [hovered, setHovered] = react.useState(false);
|
|
707
|
+
// the PR token's own hover/focus state: it underlines on hover (and on
|
|
708
|
+
// focus) rather than at rest, and that underline is applied inline below
|
|
709
|
+
const [linkHover, setLinkHover] = react.useState(false);
|
|
710
|
+
const detail = useGitStatus(target, { pr: true, detail: true, enabled: hovered });
|
|
407
711
|
if (info === void 0 || info.git !== true) return null;
|
|
408
712
|
// the mark is an element now rather than a leading glyph in the string, so
|
|
409
713
|
// the SAME StatusMark the sidebar row draws carries the status here too;
|
|
410
714
|
// the container's 4px gap supplies the space the emoji's own did
|
|
411
|
-
const text = info.branch +
|
|
412
|
-
|
|
715
|
+
const text = info.branch + formatOperationToken(info) + formatGitSuffix(info);
|
|
716
|
+
const prToken = formatPrToken(info);
|
|
717
|
+
const prUrl = prLinkUrl(info);
|
|
718
|
+
const chip = react_jsx_runtime.jsxs("span", {
|
|
413
719
|
style: {
|
|
414
720
|
display: "inline-flex",
|
|
415
721
|
alignItems: "center",
|
|
416
722
|
flex: "none",
|
|
417
|
-
|
|
723
|
+
// 6px, not 4px: the PR token leads with a space, and CSS drops
|
|
724
|
+
// leading whitespace at the start of a flex item, so 4px read as no
|
|
725
|
+
// separation at all between the branch and `PR#…`
|
|
726
|
+
gap: "6px",
|
|
418
727
|
color: "var(--dsw-alias-label-secondary, #5b6570)",
|
|
419
728
|
fontSize: "12px",
|
|
420
729
|
lineHeight: "24px",
|
|
421
730
|
whiteSpace: "nowrap",
|
|
422
731
|
cursor: "default"
|
|
423
732
|
},
|
|
733
|
+
// the card is fetched for a pointer that RESTS here, not one that
|
|
734
|
+
// merely crosses the chip
|
|
735
|
+
onPointerEnter: () => setHovered(true),
|
|
424
736
|
children: [
|
|
425
737
|
react_jsx_runtime.jsx(StatusMark, { key: "mark", info }),
|
|
426
|
-
react_jsx_runtime.jsx("span", { key: "text", children: text })
|
|
738
|
+
react_jsx_runtime.jsx("span", { key: "text", children: text }),
|
|
739
|
+
prToken === ""
|
|
740
|
+
? null
|
|
741
|
+
: prUrl === void 0
|
|
742
|
+
? react_jsx_runtime.jsx("span", {
|
|
743
|
+
key: "pr",
|
|
744
|
+
// the glyph is not the only channel: the token says what the
|
|
745
|
+
// CI state IS, for anyone who cannot see it
|
|
746
|
+
"aria-label": prTokenLabel(info),
|
|
747
|
+
children: prToken
|
|
748
|
+
})
|
|
749
|
+
: react_jsx_runtime.jsx("a", {
|
|
750
|
+
key: "pr",
|
|
751
|
+
href: prUrl,
|
|
752
|
+
// a new tab, because navigating THIS one away from the app
|
|
753
|
+
// would lose the conversation; noopener/noreferrer keep the
|
|
754
|
+
// opened tab from getting a handle on it
|
|
755
|
+
target: "_blank",
|
|
756
|
+
rel: "noopener noreferrer",
|
|
757
|
+
"aria-label": prTokenLabel(info) + ", opens on GitHub in a new tab",
|
|
758
|
+
// the chip's own colour rather than the browser's link blue /
|
|
759
|
+
// visited purple, so the token still reads as one row. The
|
|
760
|
+
// affordance is the pointer cursor plus an underline on hover
|
|
761
|
+
// or focus, set HERE rather than left to the host stylesheet:
|
|
762
|
+
// a shell that resets anchors would otherwise drop it silently.
|
|
763
|
+
style: {
|
|
764
|
+
color: "inherit",
|
|
765
|
+
textDecoration: linkHover ? "underline" : "none",
|
|
766
|
+
cursor: "pointer"
|
|
767
|
+
},
|
|
768
|
+
// focus joins hover — the token is keyboard-reachable now, and a
|
|
769
|
+
// keyboard user needs the same "this is a link" signal
|
|
770
|
+
onPointerEnter: () => setLinkHover(true),
|
|
771
|
+
onPointerLeave: () => setLinkHover(false),
|
|
772
|
+
onFocus: () => setLinkHover(true),
|
|
773
|
+
onBlur: () => setLinkHover(false),
|
|
774
|
+
children: prToken
|
|
775
|
+
})
|
|
427
776
|
]
|
|
428
777
|
});
|
|
778
|
+
// No Tooltip primitive (a shell that does not seed it): render the chip
|
|
779
|
+
// itself rather than losing the badge. The card is an enhancement, the
|
|
780
|
+
// badge is the feature.
|
|
781
|
+
if (Tooltip === void 0) return chip;
|
|
782
|
+
return react_jsx_runtime.jsx(Tooltip, {
|
|
783
|
+
side: "top",
|
|
784
|
+
maxWidth: 360,
|
|
785
|
+
// a function label keeps the card's element tree out of every render
|
|
786
|
+
// until the tooltip actually opens
|
|
787
|
+
label: () => react_jsx_runtime.jsx(HoverCard, { info, detail }),
|
|
788
|
+
children: chip
|
|
789
|
+
});
|
|
429
790
|
}
|
|
430
791
|
//#endregion
|
|
431
792
|
|
|
@@ -435,15 +796,15 @@ window.__ModuleLoader__.load({
|
|
|
435
796
|
const inject = ["slots"];
|
|
436
797
|
|
|
437
798
|
/**
|
|
438
|
-
* Register the badge into
|
|
439
|
-
*
|
|
440
|
-
*
|
|
799
|
+
* Register the badge into the seams. The seam owner hands each entry the row
|
|
800
|
+
* owner share as props; the row badge destructures `{ sessionId }` and
|
|
801
|
+
* deliberately ignores the cwd it may also be given.
|
|
441
802
|
*/
|
|
442
803
|
function apply(ctx) {
|
|
443
804
|
// inject() re-evaluates when a seam's declaration appears, so boot
|
|
444
805
|
// order relative to the workspace browser does not matter. On an
|
|
445
|
-
// unpatched install the
|
|
446
|
-
//
|
|
806
|
+
// unpatched install the seam is never declared, so this callback never
|
|
807
|
+
// fires and the row badge simply never renders.
|
|
447
808
|
//
|
|
448
809
|
// The registration is reported from INSIDE the callback on purpose: a
|
|
449
810
|
// `spec()` check here at apply() time runs before the workspace browser
|
|
@@ -451,16 +812,24 @@ window.__ModuleLoader__.load({
|
|
|
451
812
|
// outcome. That mistake made the old one-shot line claim "sidebar rows
|
|
452
813
|
// = off" while five row badges were rendering.
|
|
453
814
|
let rowsReported = false;
|
|
454
|
-
ctx.slots.inject("sidebar.workspaces.
|
|
815
|
+
ctx.slots.inject("sidebar.workspaces.sessionRow", () => {
|
|
455
816
|
if (!rowsReported) {
|
|
456
817
|
rowsReported = true;
|
|
457
|
-
console.info("[dsh-git-badge] surfaces: sidebar rows = on (seam present).");
|
|
818
|
+
console.info("[dsh-git-badge] surfaces: sidebar session rows = on (seam present).");
|
|
458
819
|
}
|
|
459
820
|
return ctx.slots.register({
|
|
460
|
-
name: "sidebar.workspaces.
|
|
461
|
-
id: "git-badge"
|
|
462
|
-
},
|
|
821
|
+
name: "sidebar.workspaces.sessionRow",
|
|
822
|
+
id: "git-badge-row"
|
|
823
|
+
}, SessionGitBadge);
|
|
463
824
|
});
|
|
825
|
+
// The row is ALREADY a HoverCard anchor upstream, so a badge that opened
|
|
826
|
+
// its own tooltip would nest two cards. This additive slot renders the
|
|
827
|
+
// provenance line inside that card instead — the same pattern the
|
|
828
|
+
// workspace-row detail slot used. It issues no request of its own.
|
|
829
|
+
ctx.slots.inject("sidebar.workspaces.sessionRow.detail", () => ctx.slots.register({
|
|
830
|
+
name: "sidebar.workspaces.sessionRow.detail",
|
|
831
|
+
id: "git-badge-row-detail"
|
|
832
|
+
}, SessionGitDetail));
|
|
464
833
|
// Input-row chip: upstream additive slot rendered in the input bar's
|
|
465
834
|
// leading cluster, right after the access picker — the git state sits
|
|
466
835
|
// with the controls that govern the conversation. Present on every
|
|
@@ -472,12 +841,35 @@ window.__ModuleLoader__.load({
|
|
|
472
841
|
id: "git-badge-chip",
|
|
473
842
|
inject: (sessionId) => ({ sessionId })
|
|
474
843
|
}, ComposerGitChip));
|
|
475
|
-
const seamDeclared = ctx.slots.spec("sidebar.workspaces.
|
|
476
|
-
console.info(
|
|
844
|
+
const seamDeclared = ctx.slots.spec("sidebar.workspaces.sessionRow") !== void 0;
|
|
845
|
+
console.info(
|
|
846
|
+
"[dsh-git-badge] surfaces: input chip = on; sidebar session rows = " +
|
|
847
|
+
(seamDeclared
|
|
848
|
+
? "on (seam present)."
|
|
849
|
+
: // discoverability: the chip works everywhere, but the session rows
|
|
850
|
+
// need the seam patch, and on a market install nothing else says so.
|
|
851
|
+
// The patcher ships IN this package — no clone needed.
|
|
852
|
+
"off (seam absent) — run `npx dsh-git-badge-seam apply` to add them, then restart dsh web.")
|
|
853
|
+
);
|
|
854
|
+
// The hover card depends on a primitive the SHELL seeds, not on anything
|
|
855
|
+
// this plugin declares. Report the outcome rather than letting a missing
|
|
856
|
+
// seed look like a missing feature: the try/catch above deliberately keeps
|
|
857
|
+
// the badge working, which would otherwise make this failure silent.
|
|
858
|
+
console.info(
|
|
859
|
+
"[dsh-git-badge] hover card = " +
|
|
860
|
+
(Tooltip === void 0
|
|
861
|
+
? "off (this shell seeds no Tooltip primitive) — the chip renders uncarded."
|
|
862
|
+
: "on (shell Tooltip primitive).")
|
|
863
|
+
);
|
|
477
864
|
}
|
|
478
865
|
|
|
479
866
|
exports.apply = apply;
|
|
480
867
|
exports.inject = inject;
|
|
868
|
+
// ---------- test-only exports ----------
|
|
869
|
+
// Additive; the host reads apply/inject and ignores the rest. The suite
|
|
870
|
+
// drives these to assert the REQUEST contract — which surface asks for the
|
|
871
|
+
// expensive extras — without a browser, a fetch or a network.
|
|
872
|
+
exports.__internals = { targetQuery, formatPrToken, formatFileBreakdown, formatPrDetail, formatCheckoutDetail, worktreeDetail, actionToken };
|
|
481
873
|
return module.exports;
|
|
482
874
|
}
|
|
483
875
|
});
|