dsh-git-badge 0.8.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 +89 -1
- package/README.md +9 -3
- package/lib/client.js +220 -100
- package/lib/index.js +395 -7
- 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/CHANGELOG.md
CHANGED
|
@@ -8,7 +8,95 @@ this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
|
|
|
8
8
|
Versions before 0.6.0 predate this file; their history is in `git log` and on
|
|
9
9
|
npm. 0.6.0 is the first release recorded here.
|
|
10
10
|
|
|
11
|
-
## [
|
|
11
|
+
## [0.9.0] - 2026-09-14
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- **The seam patcher ships in the package** as the `dsh-git-badge-seam` bin, so a
|
|
16
|
+
market install can add the sidebar session-row badges without cloning the
|
|
17
|
+
repository: `npx dsh-git-badge-seam apply`, then restart. Pure Node (no
|
|
18
|
+
python3), anchor-based like the repo tooling it replaces, with the same
|
|
19
|
+
refusal semantics: it writes nothing unless every anchor resolves exactly
|
|
20
|
+
once, upgrades its own older artifacts by marker revision, and reverts to the
|
|
21
|
+
bytes as found before patching. When the seam is absent the plugin's boot log
|
|
22
|
+
now says so and names the command.
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
|
|
26
|
+
- **A session's badge follows the worktree its work is in.** A conversation's
|
|
27
|
+
directory is fixed when it is created, and DSH records no session→worktree link
|
|
28
|
+
at all (`attachSession` requires the stored cwd to equal the workspace path), so
|
|
29
|
+
a conversation started in the main checkout reported `main` — including "no pull
|
|
30
|
+
request" — while its work and its PR lived in a linked worktree.
|
|
31
|
+
Now, when a **session**-targeted badge resolves to a main checkout whose
|
|
32
|
+
repository has exactly one linked worktree whose branch has an **open** pull
|
|
33
|
+
request, the badge describes that worktree instead: its branch, dirty counts,
|
|
34
|
+
ahead/behind, tree-shaped mark, and PR/CI token. The `workspace` id in the
|
|
35
|
+
response is unchanged, so SSE attribution is unaffected.
|
|
36
|
+
The selection is deliberately the only one that cannot be ambiguous — one open
|
|
37
|
+
PR is a candidate, two are a reason to say nothing — the worktree is **named**
|
|
38
|
+
on the chip even when the branch would make the name look redundant, the mark's
|
|
39
|
+
accessible name states that the checkout was inferred, and the hover card gains
|
|
40
|
+
a `checkout` row carrying the conversation's OWN directory (branch · files ·
|
|
41
|
+
sync) so the main checkout's state stays visible. A workspace-targeted sidebar
|
|
42
|
+
row never follows a worktree: a row surveys the checkout the registry owns.
|
|
43
|
+
- **An inferred worktree is watched.** Its working tree and git dir get the same
|
|
44
|
+
event-driven treatment as a registered workspace's, and its events carry the
|
|
45
|
+
**owning workspace id**, so the badge refreshes within ~1s of a stage, commit or
|
|
46
|
+
checkout in the tree — including a worktree that is not itself a registered
|
|
47
|
+
workspace, which the main checkout's recursive watch would never see.
|
|
48
|
+
- **Identical concurrent status reads are collapsed** into one `git status` per
|
|
49
|
+
directory, which is the shape a burst of badges mounting together produces.
|
|
50
|
+
`statusCacheMs` (default 0) optionally extends that to serial bursts; the 0
|
|
51
|
+
default keeps a read taken after a mutation honest. New config: `worktreeStatus`
|
|
52
|
+
(`"auto"` | `"off"`), `prListLimit`, `statusCacheMs`.
|
|
53
|
+
- The repository-wide open-PR read (`gh pr list`) happens at most once per TTL per
|
|
54
|
+
repository, and never at all for a repository with no linked worktree.
|
|
55
|
+
|
|
56
|
+
### Changed
|
|
57
|
+
|
|
58
|
+
- **The sidebar session row is an ACTION token, not a status badge.** It shows
|
|
59
|
+
the one thing that conversation needs — `merge`, `fix CI`, `review`, `resolve`,
|
|
60
|
+
`pull`, `push` — and nothing when there is nothing to do. `merge` keys on
|
|
61
|
+
GitHub's own `mergeStateStatus: CLEAN` rather than a verdict assembled here; a
|
|
62
|
+
draft, blocked, behind, unstable or unknown PR stays silent, because waiting is
|
|
63
|
+
not an action. This is deliberately a trade: the row no longer shows local
|
|
64
|
+
status or the worktree's tree shape, and the hover line — which now names the
|
|
65
|
+
pull request as well as the checkout — is where that detail lives. The word floats right so a column of actions lines
|
|
66
|
+
up down the sidebar, sits heavier than the timestamp beside it, and is coloured
|
|
67
|
+
by SEVERITY with the app's own state tokens — error for `resolve` and `fix CI`,
|
|
68
|
+
warn for `review`, success for `merge`, quiet secondary for `pull` and `push` —
|
|
69
|
+
so the loud rows keep meaning something. The chip is unchanged. Revert this
|
|
70
|
+
commit to go back to the mark-plus-token row.
|
|
71
|
+
- **The sidebar badge moved from the workspace row to the session row.** A
|
|
72
|
+
workspace row cannot know which worktree its conversations are using, so a
|
|
73
|
+
per-workspace badge could only guess or report the main checkout. Each session
|
|
74
|
+
row now carries the status mark plus the PR/CI token when the branch has one
|
|
75
|
+
(`🌳 PR#391 ✓`), and never a branch — hovering the row adds a line naming the
|
|
76
|
+
checkout that badge describes (`checkout: hotfix-tree on feat/x`, and for an
|
|
77
|
+
inferred worktree, why it was followed). Rows still issue no request of their
|
|
78
|
+
own beyond the badge's: the hover line resolves to the same cache key, so a card
|
|
79
|
+
that mounts for every row cannot multiply git or forge work.
|
|
80
|
+
- The `seam/` patch now declares `sidebar.workspaces.sessionRow` and
|
|
81
|
+
`sidebar.workspaces.sessionRow.detail` instead of the old
|
|
82
|
+
`sidebar.workspaces.row` pair, and the row is rendered from the **tree** call
|
|
83
|
+
site only, which keeps the flat and search lists badge-free with no guard of
|
|
84
|
+
their own. `seam/apply.sh` recognises its own older artifacts by a marker
|
|
85
|
+
comment (and the previous hash), so rebuilding the patch installs it instead of
|
|
86
|
+
being skipped as "upstream landed" — and `revert` now restores the upstream
|
|
87
|
+
baseline rather than an older patch.
|
|
88
|
+
|
|
89
|
+
### Fixed
|
|
90
|
+
|
|
91
|
+
- **A failing seam occupant could blank the whole sidebar.** The host guards each
|
|
92
|
+
registered *entry* with an error boundary, but not the outlet a row renders
|
|
93
|
+
itself, so a throw in an occupant's render path reached the workspace browser —
|
|
94
|
+
and the shell abdicates that entry, removing the entire sidebar region about a
|
|
95
|
+
second after boot. The seam patch now wraps both session-row renders in its own
|
|
96
|
+
boundary: a broken occupant loses its badge and logs the error instead.
|
|
97
|
+
- **The PR cache served the wrong branch.** `prStatusFor` was keyed by toplevel
|
|
98
|
+
only, so a checkout that changed branch inside the TTL window was served the
|
|
99
|
+
previous branch's pull request. It is now keyed by toplevel **and** branch.
|
|
12
100
|
|
|
13
101
|
## [0.8.0] - 2026-09-12
|
|
14
102
|
|
package/README.md
CHANGED
|
@@ -5,11 +5,17 @@ Git status badges for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek
|
|
|
5
5
|
- **Input-row chip** — `● main ↑0 ↓2 ✎3` next to the access picker, showing the git state of the workspace the current conversation is attached to. Works on every install.
|
|
6
6
|
- **Hover card** — rest a pointer on the chip for what won't fit: the file breakdown (`✎3` split into staged / unstaged / untracked), the last three commits, and the stash. Fetched only on hover, so the everyday badge pays nothing for it. Works on every install.
|
|
7
7
|
- **PR / CI token** — `● main ↑0 ↓2 PR#142 ✗` when the branch has a GitHub pull request, read through your own `gh` CLI (the plugin never handles a credential). The token **links to the PR** — it opens in a new tab, and underlines when you hover or focus it. Invisible when `gh` cannot answer: not installed, not logged in, no GitHub remote, offline or rate-limited all render the unchanged chip, and a token with no URL to link to stays plain text.
|
|
8
|
-
- **
|
|
8
|
+
- **Worktree-aware** — a conversation's directory is fixed when it is created, so a conversation that does its work in a linked `git worktree` would otherwise keep reporting the main checkout. When exactly one linked worktree of that repository has an open pull request, the badge describes **that worktree** instead — its branch, counts, tree mark and PR/CI token (`🌳 chore/global-skills-tiering ↑0 ↓2 PR#391 ✓` rather than `● main`). The hover card names the tree in a `worktree` row, says it was inferred, and adds a `checkout` row with the conversation's own directory, so the main checkout's branch and dirty state stay one hover away. One candidate only — two open PRs across worktrees is a reason to say nothing rather than guess — and a session row always reports the checkout that session is working in.
|
|
9
|
+
- **Sidebar session-row action token** — each session row shows the ONE thing that conversation needs from you, or nothing at all: `merge`, `fix CI`, `review`, `resolve`, `pull`, `push`. It is triage rather than status: `merge` appears only when GitHub itself reports `mergeStateStatus: CLEAN`, and every waiting state (draft, blocked, behind, checks still running) stays silent. Hovering the row adds a line naming the checkout and the pull request the action refers to. The token floats to the right of the row, is set heavier than the timestamp beside it, and is coloured by how much it needs you — red for a conflict or a failing build, amber for a requested review, green for `merge`, quiet grey for routine sync. Requires the `sidebar.workspaces.sessionRow` seam; without it the plugin degrades to chip-only and logs what's active to the browser console.
|
|
10
|
+
- **Nothing on workspace rows.** A workspace cannot know which worktree its conversations are using, so a per-workspace badge could only guess or report the main checkout. The seam is on the session row for that reason.
|
|
9
11
|
|
|
10
|
-
The leading **mark**
|
|
12
|
+
The leading **mark** belongs to the input chip: a circle filled with the status colour, or a **tree** when the checkout is a linked `git worktree`. The sidebar row deliberately carries no mark — see the row bullet above, where one word does the job a colour cannot. (`●`/`🌳` stand in for it here — it is a drawn SVG so that it can take the theme's status colours.)
|
|
11
13
|
|
|
12
|
-
Event-driven freshness: working trees are watched and updates push over SSE — badges flip within ~1s of any commit, stage, or edit. One `git status --porcelain=v2`
|
|
14
|
+
Event-driven freshness: working trees are watched and updates push over SSE — badges flip within ~1s of any commit, stage, or edit, including inside a linked worktree that is only inferred. One `git status --porcelain=v2` per sample, shared by every surface pointing at the same checkout, plus at most one `gh` read per repository-and-branch per ~90s and one repository-wide open-PR list per ~90s. The status API takes **no path**: the caller passes a session id (chip and sidebar rows) or a workspace id, and the server resolves the directory itself.
|
|
15
|
+
|
|
16
|
+
Diagrams, the hover card's full field list and a walkthrough for git newcomers live
|
|
17
|
+
in the [repo README](https://github.com/Kevin-McIsaac/dsh-workspace-git-badge) —
|
|
18
|
+
this file is the npm-facing summary.
|
|
13
19
|
|
|
14
20
|
## Install
|
|
15
21
|
|
package/lib/client.js
CHANGED
|
@@ -6,19 +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
17
|
* worktree's name + in-progress operation token + sync/dirty suffix + the
|
|
14
18
|
* PR/CI token, with a HOVER CARD carrying the breakdown the chip has no room
|
|
15
|
-
* for (file detail, recent commits, stash, PR state
|
|
19
|
+
* for (file detail, recent commits, stash, PR state, and the conversation's own
|
|
20
|
+
* checkout when the badge followed a worktree).
|
|
16
21
|
*
|
|
17
|
-
* The hover card
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* badge pays for none of them.
|
|
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.
|
|
22
26
|
*/
|
|
23
27
|
window.__ModuleLoader__.load({
|
|
24
28
|
id: "dsh-git-badge",
|
|
@@ -340,43 +344,12 @@ window.__ModuleLoader__.load({
|
|
|
340
344
|
}
|
|
341
345
|
|
|
342
346
|
/**
|
|
343
|
-
*
|
|
344
|
-
*
|
|
345
|
-
*
|
|
346
|
-
*
|
|
347
|
-
*
|
|
348
|
-
* reports whether this is a linked worktree plus its directory NAME (never a
|
|
349
|
-
* path).
|
|
350
|
-
*
|
|
351
|
-
* The name is appended only when it says something the branch does not: the
|
|
352
|
-
* usual `repo-feat-x` directory sitting on branch `feat-x` would otherwise
|
|
353
|
-
* read as stutter. (The ROW needs no such rule — it shows the name instead
|
|
354
|
-
* of the branch, so there is nothing to stutter against.)
|
|
355
|
-
*/
|
|
356
|
-
|
|
357
|
-
/**
|
|
358
|
-
* True when the worktree name is already implied by the branch name, so
|
|
359
|
-
* showing both is pure repetition. Compared on a normalized form —
|
|
360
|
-
* case-folded, with `/` and `_` folded to `-` — because directory and
|
|
361
|
-
* branch conventions differ without changing the meaning (`feat/x` in a
|
|
362
|
-
* `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.)
|
|
363
352
|
*/
|
|
364
|
-
function worktreeNameIsRedundant(name, branch) {
|
|
365
|
-
if (typeof name !== "string" || name === "" || typeof branch !== "string") return true;
|
|
366
|
-
const normalize = (value) => value.toLowerCase().replace(/[/_]/g, "-");
|
|
367
|
-
const normalizedName = normalize(name);
|
|
368
|
-
const normalizedBranch = normalize(branch);
|
|
369
|
-
return normalizedName === normalizedBranch
|
|
370
|
-
|| normalizedName.endsWith("-" + normalizedBranch)
|
|
371
|
-
|| normalizedBranch.endsWith("-" + normalizedName);
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
/** ` name` for a worktree whose name the branch does not already say, else "". */
|
|
375
|
-
function formatWorktreeToken(info) {
|
|
376
|
-
if (info.isWorktree !== true) return "";
|
|
377
|
-
const name = typeof info.worktreeName === "string" ? info.worktreeName : "";
|
|
378
|
-
return worktreeNameIsRedundant(name, info.branch) ? "" : " " + name;
|
|
379
|
-
}
|
|
380
353
|
|
|
381
354
|
const META_STYLE = {
|
|
382
355
|
color: "var(--dsw-alias-label-tertiary, #9ea7ad)",
|
|
@@ -412,7 +385,12 @@ window.__ModuleLoader__.load({
|
|
|
412
385
|
height: 14,
|
|
413
386
|
viewBox: "0 0 12 12",
|
|
414
387
|
role: "img",
|
|
415
|
-
"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)" : ""),
|
|
416
394
|
focusable: "false",
|
|
417
395
|
style: { flex: "none", display: "block", color: META_STYLE.color }
|
|
418
396
|
};
|
|
@@ -436,52 +414,135 @@ window.__ModuleLoader__.load({
|
|
|
436
414
|
}
|
|
437
415
|
|
|
438
416
|
/**
|
|
439
|
-
*
|
|
440
|
-
*
|
|
441
|
-
*
|
|
442
|
-
*
|
|
443
|
-
*
|
|
444
|
-
*
|
|
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.
|
|
445
430
|
*/
|
|
446
|
-
function
|
|
447
|
-
if (info.
|
|
448
|
-
|
|
449
|
-
|
|
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
|
+
});
|
|
450
510
|
}
|
|
451
511
|
|
|
452
512
|
/**
|
|
453
|
-
*
|
|
454
|
-
*
|
|
455
|
-
*
|
|
456
|
-
*
|
|
457
|
-
* out-of-sync, red conflict or dirty-and-behind. Shape via the node half's
|
|
458
|
-
* isWorktree: circle = main checkout, tree = linked worktree.
|
|
459
|
-
* Always renders the workspace name (so the row keeps its identity) and
|
|
460
|
-
* appends the right-hand mark only for git workspaces. No branch, and no
|
|
461
|
-
* `|` separator.
|
|
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.
|
|
462
517
|
*
|
|
463
|
-
*
|
|
464
|
-
*
|
|
465
|
-
*
|
|
466
|
-
*
|
|
467
|
-
*
|
|
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`.
|
|
526
|
+
*
|
|
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.
|
|
468
530
|
*/
|
|
469
|
-
function
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
const
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
//
|
|
477
|
-
//
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
if (name !== null) meta.push(react_jsx_runtime.jsx("span", { key: "name", style: META_STYLE, children: name }));
|
|
481
|
-
meta.push(react_jsx_runtime.jsx(StatusMark, { key: "mark", info }));
|
|
482
|
-
children.push(react_jsx_runtime.jsx("span", { style: { display: "inline-flex", alignItems: "center", gap: "6px", flex: "none", marginLeft: "auto", paddingLeft: "8px" }, children: meta }));
|
|
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 "));
|
|
483
542
|
}
|
|
484
|
-
|
|
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 ") });
|
|
485
546
|
}
|
|
486
547
|
|
|
487
548
|
/**
|
|
@@ -512,6 +573,38 @@ window.__ModuleLoader__.load({
|
|
|
512
573
|
return bits.join(" \u00B7 ");
|
|
513
574
|
}
|
|
514
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.
|
|
582
|
+
*/
|
|
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));
|
|
589
|
+
}
|
|
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
|
+
|
|
515
608
|
const CARD_CONTAINER = {
|
|
516
609
|
display: "flex",
|
|
517
610
|
flexDirection: "column",
|
|
@@ -555,6 +648,14 @@ window.__ModuleLoader__.load({
|
|
|
555
648
|
add("files", formatFileBreakdown(info));
|
|
556
649
|
add("operation", info.operation === void 0 || info.operation === null ? void 0 : String(info.operation));
|
|
557
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));
|
|
558
659
|
if (Array.isArray(data.lastCommits) && data.lastCommits.length > 0) {
|
|
559
660
|
rows.push(
|
|
560
661
|
react_jsx_runtime.jsxs("div", {
|
|
@@ -611,7 +712,7 @@ window.__ModuleLoader__.load({
|
|
|
611
712
|
// the mark is an element now rather than a leading glyph in the string, so
|
|
612
713
|
// the SAME StatusMark the sidebar row draws carries the status here too;
|
|
613
714
|
// the container's 4px gap supplies the space the emoji's own did
|
|
614
|
-
const text = info.branch +
|
|
715
|
+
const text = info.branch + formatOperationToken(info) + formatGitSuffix(info);
|
|
615
716
|
const prToken = formatPrToken(info);
|
|
616
717
|
const prUrl = prLinkUrl(info);
|
|
617
718
|
const chip = react_jsx_runtime.jsxs("span", {
|
|
@@ -619,7 +720,10 @@ window.__ModuleLoader__.load({
|
|
|
619
720
|
display: "inline-flex",
|
|
620
721
|
alignItems: "center",
|
|
621
722
|
flex: "none",
|
|
622
|
-
|
|
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",
|
|
623
727
|
color: "var(--dsw-alias-label-secondary, #5b6570)",
|
|
624
728
|
fontSize: "12px",
|
|
625
729
|
lineHeight: "24px",
|
|
@@ -692,15 +796,15 @@ window.__ModuleLoader__.load({
|
|
|
692
796
|
const inject = ["slots"];
|
|
693
797
|
|
|
694
798
|
/**
|
|
695
|
-
* Register the badge into
|
|
696
|
-
*
|
|
697
|
-
*
|
|
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.
|
|
698
802
|
*/
|
|
699
803
|
function apply(ctx) {
|
|
700
804
|
// inject() re-evaluates when a seam's declaration appears, so boot
|
|
701
805
|
// order relative to the workspace browser does not matter. On an
|
|
702
|
-
// unpatched install the
|
|
703
|
-
//
|
|
806
|
+
// unpatched install the seam is never declared, so this callback never
|
|
807
|
+
// fires and the row badge simply never renders.
|
|
704
808
|
//
|
|
705
809
|
// The registration is reported from INSIDE the callback on purpose: a
|
|
706
810
|
// `spec()` check here at apply() time runs before the workspace browser
|
|
@@ -708,16 +812,24 @@ window.__ModuleLoader__.load({
|
|
|
708
812
|
// outcome. That mistake made the old one-shot line claim "sidebar rows
|
|
709
813
|
// = off" while five row badges were rendering.
|
|
710
814
|
let rowsReported = false;
|
|
711
|
-
ctx.slots.inject("sidebar.workspaces.
|
|
815
|
+
ctx.slots.inject("sidebar.workspaces.sessionRow", () => {
|
|
712
816
|
if (!rowsReported) {
|
|
713
817
|
rowsReported = true;
|
|
714
|
-
console.info("[dsh-git-badge] surfaces: sidebar rows = on (seam present).");
|
|
818
|
+
console.info("[dsh-git-badge] surfaces: sidebar session rows = on (seam present).");
|
|
715
819
|
}
|
|
716
820
|
return ctx.slots.register({
|
|
717
|
-
name: "sidebar.workspaces.
|
|
718
|
-
id: "git-badge"
|
|
719
|
-
},
|
|
821
|
+
name: "sidebar.workspaces.sessionRow",
|
|
822
|
+
id: "git-badge-row"
|
|
823
|
+
}, SessionGitBadge);
|
|
720
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));
|
|
721
833
|
// Input-row chip: upstream additive slot rendered in the input bar's
|
|
722
834
|
// leading cluster, right after the access picker — the git state sits
|
|
723
835
|
// with the controls that govern the conversation. Present on every
|
|
@@ -729,8 +841,16 @@ window.__ModuleLoader__.load({
|
|
|
729
841
|
id: "git-badge-chip",
|
|
730
842
|
inject: (sessionId) => ({ sessionId })
|
|
731
843
|
}, ComposerGitChip));
|
|
732
|
-
const seamDeclared = ctx.slots.spec("sidebar.workspaces.
|
|
733
|
-
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
|
+
);
|
|
734
854
|
// The hover card depends on a primitive the SHELL seeds, not on anything
|
|
735
855
|
// this plugin declares. Report the outcome rather than letting a missing
|
|
736
856
|
// seed look like a missing feature: the try/catch above deliberately keeps
|
|
@@ -749,7 +869,7 @@ window.__ModuleLoader__.load({
|
|
|
749
869
|
// Additive; the host reads apply/inject and ignores the rest. The suite
|
|
750
870
|
// drives these to assert the REQUEST contract — which surface asks for the
|
|
751
871
|
// expensive extras — without a browser, a fetch or a network.
|
|
752
|
-
exports.__internals = { targetQuery, formatPrToken, formatFileBreakdown, formatPrDetail };
|
|
872
|
+
exports.__internals = { targetQuery, formatPrToken, formatFileBreakdown, formatPrDetail, formatCheckoutDetail, worktreeDetail, actionToken };
|
|
753
873
|
return module.exports;
|
|
754
874
|
}
|
|
755
875
|
});
|