dsh-git-branch-manage 1.0.1 → 1.0.3

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/lib/client.js CHANGED
@@ -5,8 +5,8 @@ window.__ModuleLoader__.load({
5
5
  var exports = module.exports;
6
6
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
7
  let react = require("react");
8
- let react_dom = require("react-dom");
9
8
  let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
9
+ let react_dom = require("react-dom");
10
10
  let react_jsx_runtime = require("react/jsx-runtime");
11
11
  //#region src/normalize.ts
12
12
  /**
@@ -87,6 +87,23 @@ window.__ModuleLoader__.load({
87
87
  /** POST ROUTE_PREFIX/purge — delete a NON-git directory sitting DIRECTLY in
88
88
  * the storage root (an orphaned leftover whose .git is already gone). */
89
89
  const ROUTE_PURGE = `${ROUTE_PREFIX}/purge`;
90
+ /** POST ROUTE_PREFIX/changes — working-tree changes (staged/unstaged rows). */
91
+ const ROUTE_CHANGES = `${ROUTE_PREFIX}/changes`;
92
+ /** POST ROUTE_PREFIX/stage — batch stage (git add) or unstage paths. */
93
+ const ROUTE_STAGE = `${ROUTE_PREFIX}/stage`;
94
+ /** POST ROUTE_PREFIX/commit — commit the index with a message. */
95
+ const ROUTE_COMMIT = `${ROUTE_PREFIX}/commit`;
96
+ /** POST ROUTE_PREFIX/log — paginated commit history of the checkout. */
97
+ const ROUTE_LOG = `${ROUTE_PREFIX}/log`;
98
+ /** POST ROUTE_PREFIX/commit-files — the files one commit changed. */
99
+ const ROUTE_COMMIT_FILES = `${ROUTE_PREFIX}/commit-files`;
100
+ /** POST ROUTE_PREFIX/ai-commit-message — one-shot LLM commit message from
101
+ * the staged diff (mounted only where the host provides the llm service). */
102
+ const ROUTE_AI_COMMIT_MESSAGE = `${ROUTE_PREFIX}/ai-commit-message`;
103
+ /** Upper bound for one commit message (characters). The host rejects longer
104
+ * bodies and the client caps its input at the same value, so the two ends
105
+ * can never disagree. */
106
+ const COMMIT_MESSAGE_LIMIT = 2e3;
90
107
  //#endregion
91
108
  //#region src/client/api.ts
92
109
  /** POST one JSON body and parse the uniform envelope. */
@@ -307,6 +324,72 @@ window.__ModuleLoader__.load({
307
324
  function requestPurgeDirectory(path) {
308
325
  return post(ROUTE_PURGE, { path });
309
326
  }
327
+ /**
328
+ * Working-tree changes of one checkout (staged/unstaged/untracked rows).
329
+ * @param repoPath - absolute directory inside the repository.
330
+ */
331
+ function requestChanges(repoPath) {
332
+ return post(ROUTE_CHANGES, { repoPath });
333
+ }
334
+ /**
335
+ * Stage or unstage relative paths of one checkout.
336
+ * @param repoPath - absolute directory inside the repository.
337
+ * @param paths - relative paths from requestChanges output.
338
+ * @param stage - true stages (git add), false unstages (git reset).
339
+ */
340
+ function requestStage(repoPath, paths, stage) {
341
+ return post(ROUTE_STAGE, {
342
+ repoPath,
343
+ paths: [...paths],
344
+ stage
345
+ });
346
+ }
347
+ /**
348
+ * Commit the index with a message.
349
+ * @param repoPath - absolute directory inside the repository.
350
+ * @param message - the commit message, verbatim.
351
+ */
352
+ function requestCommit(repoPath, message) {
353
+ return post(ROUTE_COMMIT, {
354
+ repoPath,
355
+ message
356
+ });
357
+ }
358
+ /**
359
+ * One page of commit history of the checkout.
360
+ * @param repoPath - absolute directory inside the repository.
361
+ * @param limit - page size (server-capped at 200).
362
+ * @param skip - offset into the history.
363
+ * @param ref - optional starting rev (a branch name); absent = HEAD.
364
+ */
365
+ function requestLog(repoPath, limit, skip, ref) {
366
+ return post(ROUTE_LOG, {
367
+ repoPath,
368
+ ...limit === void 0 ? {} : { limit },
369
+ ...skip === void 0 ? {} : { skip },
370
+ ...ref === void 0 || ref === "" ? {} : { ref }
371
+ });
372
+ }
373
+ /**
374
+ * The files one commit changed (status letter + +/- line counts).
375
+ * @param repoPath - absolute directory inside the repository.
376
+ * @param hash - the commit hash.
377
+ */
378
+ function requestCommitFiles(repoPath, hash) {
379
+ return post(ROUTE_COMMIT_FILES, {
380
+ repoPath,
381
+ hash
382
+ });
383
+ }
384
+ /**
385
+ * Generate a commit message from the checkout's diff with the requesting
386
+ * session's own model (a one-shot host llm call).
387
+ * @param body - the repo path plus the session's model selection and an
388
+ * optional user hint.
389
+ */
390
+ function requestAiCommitMessage(body) {
391
+ return post(ROUTE_AI_COMMIT_MESSAGE, body);
392
+ }
310
393
  //#endregion
311
394
  //#region src/client/branch-tree.ts
312
395
  /** Whether a bare `git push` has a known upstream and local commits ahead. */
@@ -499,13 +582,13 @@ window.__ModuleLoader__.load({
499
582
  }
500
583
  //#endregion
501
584
  //#region \0git-worktree-css:/home/runner/work/dsh-git-branch/dsh-git-branch/src/client/BranchChip.module.css?inline
502
- const css$2 = "/* Branch chip row inside the composer tool row (conversation.input_958825969.left_958825969,\n * right of the mode chips). A single chip button: the branch picker (the\n * worktree toggle that used to share this dock is gone — worktree actions\n * live in the branch menu's right-click).\n *\n * Geometry mirrors the composer trigger chips in the DSH base (see\n * PermissionSelect / ModelSelect): 28px height, 13/20 medium-secondary\n * label, transparent fill, no outline — the dock stays at the same\n * visual weight as the surrounding chips (dsh-worktree select, standard\n * mode select, Workspace Write, MiniMax-M3 High). The corners stop short\n * of the base's full pill (24px) so the silhouette stays a chip rather\n * than a capsule, per the design brief. */\n\n/* Shared trigger geometry — copied 1:1 from the base composer triggers\n * (PermissionSelect .trigger_958825969 / ModelSelect .trigger_958825969). */\n.dock_958825969 {\n display: inline-flex;\n align-items: stretch;\n height: 28px;\n border-radius: 6px;\n background: transparent;\n color: var(--dsw-alias-label-secondary, #81858c);\n font-size: 13px;\n line-height: 20px;\n font-weight: 500;\n min-height: 28px;\n overflow: hidden;\n}\n\n.chip_958825969 {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n height: 100%;\n padding: 0 8px 0 8px;\n border: none;\n border-radius: 0;\n background: transparent;\n color: inherit;\n font: inherit;\n font-size: 13px;\n line-height: 20px;\n font-weight: 500;\n cursor: pointer;\n}\n\n.chip_958825969:hover {\n background: var(--dsw-alias-interactive-bg-hover, color-mix(in srgb, currentColor 8%, transparent));\n}\n\n.branch_958825969 {\n max-width: 16em;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n/* Confirm flyout: the second-level panel opening right of the branch\n * card (the base Menu's submenu posture: r20, elevation-prominent\n * hairline, --dsw-specific-menu with --dsw-menu-backdrop-filter — host 0.1.2 Menu card;\n * hosts without the token fall back to the old r12-era shadow-lv3). Width is content-driven — it follows\n * the longest line (for remote picks that is the subject line's branch\n * name) — floored at 168px (the Cancel/Confirm pair plus padding: short\n * branch names must not squeeze the buttons into wrapping \"切换中…\"\n * mid-word) and capped at 400px; BranchMenu also clamps it inline to the\n * room right of the card. Beyond the cap the lines wrap. border-box so\n * every width arm includes the card chrome. */\n.popCard_958825969 {\n box-sizing: border-box;\n position: fixed;\n z-index: 1000;\n width: max-content;\n min-width: 168px;\n max-width: min(400px, 80vw);\n padding: 4px;\n border: 0;\n border-radius: 20px;\n background: var(--dsw-specific-menu, var(--dsw-alias-surface-raised, #ffffff));\n backdrop-filter: var(--dsw-menu-backdrop-filter);\n --dsw-elevation-stroke-color: var(--dsw-alias-border-l1);\n /* Elevation-prominent carries the hairline stroke; the lv3 fallback\n * keeps a shadow where the token does not exist. */\n box-shadow: var(--dsw-elevation-prominent, var(--dsw-shadow-lv3));\n display: flex;\n flex-direction: column;\n gap: 2px;\n}\n\n.popAsk_958825969 {\n margin: 0;\n padding: 6px 8px;\n font-size: 13px;\n line-height: 20px;\n color: inherit;\n /* Branch names are long unbroken tokens — plain wrapping would let them\n * overflow the capped card instead of breaking. */\n overflow-wrap: anywhere;\n}\n\n/* Remote confirm's name line: the branch itself, weight 500 like the leaf\n * rows — the subject the ask line refers to (\"该远程分支\"). A dedicated\n * line keeps the long unbroken token away from the sentence, so wraps\n * happen inside the name instead of shredding the Chinese around it. */\n.popSubject_958825969 {\n margin: 0;\n padding: 0 8px 2px;\n font-size: 13px;\n line-height: 20px;\n font-weight: 500;\n color: inherit;\n overflow-wrap: anywhere;\n}\n\n/* The removal confirm's consequence lines (dirty/ahead/archived sessions):\n * subdued one-row facts under the ask, the dirty count alone in the warning\n * tone — mirroring the manager dialog's fact block. */\n.popDetails_958825969 {\n padding: 0 8px 2px;\n}\n\n.popDetail_958825969 {\n margin: 0;\n font-size: 12px;\n line-height: 18px;\n color: var(--dsw-alias-label-secondary, rgba(128, 128, 128, 0.9));\n overflow-wrap: anywhere;\n}\n\n.popDetailWarn_958825969 {\n color: var(--dsw-alias-status-danger, #d5304f);\n}\n\n.popActions_958825969 {\n display: flex;\n gap: 2px;\n padding: 2px;\n}\n\n/* Menu-row-like buttons: transparent fill, hover tint, same 13px type.\n * nowrap keeps labels like 切换中… from splitting mid-word when the flyout\n * is pinned to its minimum width by a short branch name. */\n.popActions_958825969 button {\n flex: 1 1 0;\n height: 28px;\n border: none;\n border-radius: 8px;\n background: transparent;\n color: inherit;\n font: inherit;\n font-size: 13px;\n white-space: nowrap;\n cursor: pointer;\n}\n\n.popActions_958825969 button:hover:not(:disabled) {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.popActions_958825969 button:disabled {\n opacity: 0.5;\n cursor: default;\n}\n\n.popActions_958825969 button:last-child {\n color: var(--dsw-alias-label-primary-bluish, #4186f0);\n font-weight: 500;\n}\n\n/* ── Branch picker popup (BranchMenu) ──────────────────────────────\n * Upward-opening card pinned above the chip, in the Menu's card chrome\n * (r20, elevation-prominent hairline, --dsw-specific-menu with\n * --dsw-menu-backdrop-filter — host Menu/PermissionSelect card;\n * hosts without the token fall back to shadow-lv3). Three owner requirements shape the geometry:\n *\n * 1. height cap: min(420px, 60vh) on the card, only the rows area\n * scrolls (.menuRows_958825969) — heading and search stay pinned;\n * 2. the search field sits pinned at the card's bottom edge, directly\n * above the chip, with the scrolling rows above it;\n * 3. the card is CSS-`bottom`-pinned ~6px above the chip's top and so\n * grows entirely upward — it can never cover the composer or fill\n * the viewport, whatever the branch count.\n *\n * The inline `left`/`bottom` come from BranchMenu's placement pass; the\n * width is fixed here (design 360px — 320 tree + 40 tool strip — and\n * viewport-capped) so horizontal clamping stays deterministic without\n * measuring the card. */\n\n/* The portal lands directly under document.body_958825969, outside the shell's\n * box-sizing reset — content-box default would add padding/border on top\n * of the declared width (360 became 370, and the width:100% children\n * padded 16px past the card's right clip, shearing off their rounded\n * corners). Every width-declared box in this popup opts back into\n * border-box: the card, the toolbar, the rows, and the search input. */\n.menuCard_958825969 {\n box-sizing: border-box;\n position: fixed;\n z-index: 1000;\n display: flex;\n flex-direction: row;\n width: min(360px, calc(100vw - 24px));\n max-height: min(420px, 60vh);\n overflow: hidden;\n padding: 4px;\n border: 0;\n border-radius: 20px;\n background: var(--dsw-specific-menu, var(--dsw-alias-surface-raised, #ffffff));\n backdrop-filter: var(--dsw-menu-backdrop-filter);\n --dsw-elevation-stroke-color: var(--dsw-alias-border-l1);\n /* Same elevation card as .popCard_958825969 (host 0.1.2 Menu); lv3 fallback\n * for hosts without the token. */\n box-shadow: var(--dsw-elevation-prominent, var(--dsw-shadow-lv3));\n --dsh-scrollbar-thumb: var(--dsw-alias-scrollbar-bg-l2);\n --dsh-scrollbar-thumb-hover: var(--dsw-alias-scrollbar-hover-l2);\n}\n\n/* Left tool strip (IDEA branch-panel posture at popup scale): a narrow\n * column of icon-only buttons for actions that do not belong in the tree\n * — locate the checked-out branch, expand/collapse the whole tree.\n * Buttons stack from the BOTTOM up: the card's height shrinks when the\n * repo has few branches, so a top-anchored strip would drift around and\n * the buttons would sit far from the search bar; anchoring to the bottom\n * pins them at a stable spot (beside the search row) whatever the height. */\n.menuToolbar_958825969 {\n box-sizing: border-box;\n display: flex;\n flex-direction: column;\n justify-content: flex-end;\n gap: 2px;\n flex-shrink: 0;\n width: 32px;\n padding: 0 2px;\n margin-right: 4px;\n border-right: 0.5px solid color-mix(in srgb, currentColor 12%, transparent);\n}\n\n.menuToolButton_958825969 {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 28px;\n height: 28px;\n margin: 0 auto;\n border: none;\n border-radius: 8px;\n background: transparent;\n color: inherit;\n cursor: pointer;\n}\n\n.menuToolButton_958825969:hover:not(:disabled) {\n background: var(--dsw-alias-interactive-bg-hover, color-mix(in srgb, currentColor 8%, transparent));\n}\n\n.menuToolButton_958825969:disabled {\n opacity: 0.4;\n cursor: default;\n}\n\n/* The sync tools while their action runs: a soft opacity pulse. The glyph\n * is a directional arrow, so the circular-refresh spin read as garbled\n * rotation — pulsing keeps the arrow legible while clearly \"live\", and the\n * button keeps full opacity (the usual disabled dim would read as broken\n * for an in-flight network operation). */\n.menuToolButtonRunning_958825969 svg {\n animation: menuToolPulse 700ms ease-in-out infinite alternate;\n}\n\n.menuToolButtonRunning_958825969:disabled {\n opacity: 1;\n cursor: progress;\n}\n\n@keyframes menuToolPulse {\n from { opacity: 1; }\n to { opacity: 0.25; }\n}\n\n/* The main column: heading + scrollable rows + pinned search. */\n.menuMain_958825969 {\n box-sizing: border-box;\n display: flex;\n flex-direction: column;\n flex: 1 1 auto;\n min-width: 0;\n}\n\n/* Non-interactive group heading — the Menu label row's posture. */\n.menuHeading_958825969 {\n padding: 6px 8px 4px;\n font-size: 13px;\n line-height: 20px;\n color: var(--dsw-alias-label-secondary, #81858c);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n flex-shrink: 0;\n}\n\n/* The only scrollable region: shrinks within the capped card so the\n * heading above and the search below stay visible while rows scroll. */\n.menuRows_958825969 {\n flex: 1 1 auto;\n min-height: 0;\n overflow-y: auto;\n}\n\n/* Menu-row-like buttons: transparent fill, hover tint, same 13px type.\n * Weight 500 mirrors the chip's branch label (the portal escapes the\n * composer's font context, so the match must be explicit — the inherited\n * body weight is 400 and reads visibly lighter than the chip beside it). */\n.menuRow_958825969 {\n box-sizing: border-box; /* width:100% + padding must stay inside .menuRows_958825969 */\n display: flex;\n align-items: center;\n gap: 8px;\n width: 100%;\n min-height: 28px;\n padding: 4px 8px;\n border: none;\n border-radius: 8px;\n background: transparent;\n color: inherit;\n font: inherit;\n font-size: 13px;\n font-weight: 500;\n line-height: 20px;\n text-align: left;\n cursor: pointer;\n}\n\n.menuRow_958825969:hover:not(:disabled) {\n background: var(--dsw-alias-interactive-bg-hover, color-mix(in srgb, currentColor 8%, transparent));\n}\n\n/* The checked-out branch: a persistent tint a step above the resting rows\n * so it reads as \"you are here\" the moment the list scrolls it into view\n * (BranchMenu centers it on open) — the trailing check alone is easy to\n * miss from a distance. */\n.menuRowSelected_958825969,\n.menuRowSelected_958825969:hover:not(:disabled) {\n background: color-mix(in srgb, currentColor 10%, transparent);\n}\n\n/* IDEA-style selection: the clicked row gets a bluish fill, layering over\n * the HEAD tint when the same row is both — selection communicates intent,\n * the HEAD tint communicates position, and they coexist (IDEA 4.4). */\n.menuRowPicked_958825969,\n.menuRowPicked_958825969:hover:not(:disabled) {\n background: color-mix(in srgb, var(--dsw-alias-label-primary-bluish, #4186f0) 18%, transparent);\n}\n\n.menuRowLabel_958825969 {\n flex: 1 1 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n/* Upstream divergence arrows (↑N/↓N) trailing a local branch row, before\n * the HEAD check: the count badge tone at a smaller size so a busy list\n * still reads the name first. Plain text arrows — no base-set icon pairs\n * a direction with a count. tabular-nums keeps ↑10 from nudging neighbors\n * as the digits tick. */\n.menuRowArrow_958825969 {\n flex-shrink: 0;\n font-size: 11px;\n line-height: 20px;\n font-weight: 400;\n font-variant-numeric: tabular-nums;\n color: color-mix(in srgb, currentColor 55%, transparent);\n}\n\n/* Search hit highlight (IDEA): the matched substring gets a warm tint\n * behind it — theme-agnostic, legible in both light and dark cards. */\n.menuSearchMark_958825969 {\n background: color-mix(in srgb, #b58900 38%, transparent);\n border-radius: 2px;\n}\n\n/* Tree group rows (folder headers) — BranchMenu's '/' prefix tree: the\n * same row geometry as .menuRow_958825969, but labeled in the secondary tone so the\n * leaf rows pop, with a count badge and a chevron that turns on expand. */\n.menuGroup_958825969 {\n box-sizing: border-box;\n display: flex;\n align-items: center;\n gap: 6px;\n width: 100%;\n min-height: 28px;\n padding: 4px 8px;\n border: none;\n border-radius: 8px;\n background: transparent;\n color: var(--dsw-alias-label-secondary, #81858c);\n font: inherit;\n font-size: 13px;\n font-weight: 500;\n line-height: 20px;\n text-align: left;\n cursor: pointer;\n}\n\n.menuGroup_958825969:hover {\n background: var(--dsw-alias-interactive-bg-hover, color-mix(in srgb, currentColor 8%, transparent));\n}\n\n/* The two top-level section headers (local/remote branches) step ABOVE the\n * folder headers they contain: primary label tone like the leaf rows plus\n * a heavier weight, so the nav hierarchy reads group > row > folder — the\n * folder headers keep the secondary tone that alone would leave the\n * sections as the faintest (and most important) rows on the list. */\n.menuGroupTop_958825969 {\n color: inherit;\n font-weight: 600;\n}\n\n.menuGroupLabel_958825969 {\n flex: 1 1 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.menuGroupChevron_958825969 {\n flex-shrink: 0;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 12px;\n height: 12px;\n transition: transform 120ms ease;\n}\n\n.menuGroupChevronOpen_958825969 {\n transform: rotate(90deg);\n}\n\n.menuGroupCount_958825969 {\n flex-shrink: 0;\n font-size: 12px;\n line-height: 20px;\n font-weight: 400;\n color: color-mix(in srgb, currentColor 55%, transparent);\n}\n\n/* Zero-search-hit state: centered secondary line inside the rows area. */\n.menuEmpty_958825969 {\n padding: 10px 8px;\n font-size: 13px;\n line-height: 20px;\n color: var(--dsw-alias-label-secondary, #81858c);\n text-align: center;\n}\n\n/* Bottom-pinned search field, separated from the rows by a hairline.\n * Borderless so it reads as part of the card, focus shown as the\n * standard hover tint rather than an outline. */\n.menuSearchWrap_958825969 {\n flex-shrink: 0;\n margin-top: 2px;\n padding-top: 4px;\n border-top: 0.5px solid color-mix(in srgb, currentColor 12%, transparent);\n}\n\n.menuSearch_958825969 {\n box-sizing: border-box; /* width:100% + padding must stay inside the wrap */\n width: 100%;\n height: 30px;\n padding: 0 8px;\n border: none;\n border-radius: 8px;\n background: transparent;\n color: inherit;\n font: inherit;\n font-size: 13px;\n /* Weight 500 mirrors the chip's branch label (see .menuRow_958825969) — the portal\n * escapes the composer's font context and would inherit body's 400. */\n font-weight: 500;\n line-height: 20px;\n}\n\n.menuSearch_958825969::placeholder {\n color: var(--dsw-alias-label-secondary, #81858c);\n}\n\n.menuSearch_958825969:focus {\n outline: none;\n background: var(--dsw-alias-interactive-bg-hover, color-mix(in srgb, currentColor 8%, transparent));\n}\n\n/* ── Create/rename flyouts (BranchMenu row context menu) ────────────\n * Open IN PLACE of the row menu (at its point, clamped) and hold the whole\n * flow in one panel: ask line, input, live hint, and the Cancel/confirm\n * pair — typing the name and pressing the button fires in one stroke, no\n * second confirm step. */\n\n/* The naming input inside the flyout: borderless, part of the card, focus\n * shown as the standard hover tint. Fixed width — the popCard is\n * max-content, so a percentage width would chase the input's own default\n * size instead of the design width. */\n.menuCreate_958825969 {\n box-sizing: border-box;\n width: 248px;\n height: 30px;\n padding: 0 8px;\n border: none;\n border-radius: 8px;\n background: transparent;\n color: inherit;\n font: inherit;\n font-size: 13px;\n /* Weight 500 mirrors the chip's branch label (see .menuSearch_958825969). */\n font-weight: 500;\n line-height: 20px;\n}\n\n.menuCreate_958825969::placeholder {\n color: var(--dsw-alias-label-secondary, #81858c);\n}\n\n.menuCreate_958825969:focus {\n outline: none;\n background: var(--dsw-alias-interactive-bg-hover, color-mix(in srgb, currentColor 8%, transparent));\n}\n\n.menuCreate_958825969:disabled {\n opacity: 0.5;\n}\n\n/* The error line under the input — only rendered while the draft is\n * unacceptable with a non-empty reason; the input itself stays neutral,\n * the line carries the state. */\n.menuCreateHintBad_958825969 {\n margin: 0;\n padding: 0 8px;\n font-size: 12px;\n line-height: 16px;\n color: var(--dsw-alias-label-error);\n}\n\n/* ── Row context menu (BranchMenu right-click) ─────────────────────\n * Cursor-anchored transient over the branch card: translucent menu chrome\n * (--dsw-specific-menu with --dsw-menu-backdrop-filter, elevation-prominent hairline,\n * lv3 fallback) at a smaller radius — r20 would round a launcher this\n * short into a pill, r12 keeps the corners proportionate to a handful of\n * rows (two on a worktree row, up to six plus a header on a branch row).\n * Width hugs the widest item\n * (shrink-to-fit, no design width) with a generous cap for safety —\n * a launcher reads better snug than stretched. Positioned once at open\n * and dismissed rather than re-placed when the page moves (see\n * BranchMenu's ctx placement pass); z-index one step above the popCard so\n * it never slides under the confirm flyout. */\n.ctxCard_958825969 {\n box-sizing: border-box;\n position: fixed;\n z-index: 1001;\n width: max-content;\n max-width: 280px;\n padding: 4px;\n border: 0;\n border-radius: 12px;\n background: var(--dsw-specific-menu, var(--dsw-alias-surface-raised, #ffffff));\n backdrop-filter: var(--dsw-menu-backdrop-filter);\n --dsw-elevation-stroke-color: var(--dsw-alias-border-l1);\n box-shadow: var(--dsw-elevation-prominent, var(--dsw-shadow-lv3));\n display: flex;\n flex-direction: column;\n gap: 2px;\n}\n\n/* The target row's name, above the verbs. Unlike the items this one holds\n * a branch name, so it is the one line allowed to hit the 280px cap and\n * ellipsize (the full name rides in a title). Secondary tone and no hover\n * affordance: it is a label, not a seventh item. */\n.ctxHeader_958825969 {\n padding: 4px 8px 2px;\n font-size: 12px;\n line-height: 16px;\n color: var(--dsw-alias-label-secondary);\n overflow: hidden;\n white-space: nowrap;\n text-overflow: ellipsis;\n}\n\n/* Menu-item rows: the .menuRow_958825969 geometry at a hair lighter weight — a\n * launcher's labels read as commands, not data. nowrap: the labels are\n * fixed copy (no branch names), the 280px cap is unreachable by design. */\n.ctxItem_958825969 {\n display: inline-flex;\n align-items: center;\n gap: 8px;\n width: 100%;\n min-height: 28px;\n padding: 4px 8px;\n border: none;\n border-radius: 8px;\n background: transparent;\n color: inherit;\n font: inherit;\n font-size: 13px;\n line-height: 20px;\n text-align: left;\n white-space: nowrap;\n cursor: pointer;\n}\n\n.ctxItem_958825969:hover,\n.ctxItem_958825969:focus-visible {\n background: var(--dsw-alias-interactive-bg-hover, color-mix(in srgb, currentColor 8%, transparent));\n}\n\n/* The running-session withhold (worktree removal): a disabled item stays\n * visible — the reason it is missing lives on its hover title, and a verb\n * that appears and disappears with session state reads as a bug, not a\n * rule. No hover tint, no pointer; the label dims instead of vanishing. */\n.ctxItem_958825969:disabled {\n cursor: default;\n opacity: 0.45;\n}\n\n.ctxItem_958825969:disabled:hover {\n background: transparent;\n}\n\n/* The arrow-key walk lands here, so the focused item must read as strongly\n * as a hovered one — the tint above carries it, the default ring would sit\n * outside the 8px radius. */\n.ctxItem_958825969:focus-visible {\n outline: none;\n}\n\n/* Leading glyph rides the label tone, a step quieter. */\n.ctxItem_958825969 svg {\n flex-shrink: 0;\n opacity: 0.75;\n}\n\n.ctxItemLabel_958825969 {\n flex: 1 1 0;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n";
503
- const tagId$2 = "dsh-git-branch-manage/BranchChip.module.css";
504
- if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=\"" + tagId$2 + "\"]") === null) {
585
+ const css$3 = "/* Branch chip row inside the composer tool row (conversation.input_958825969.left_958825969,\n * right of the mode chips). A single chip button: the branch picker (the\n * worktree toggle that used to share this dock is gone — worktree actions\n * live in the branch menu's right-click).\n *\n * Geometry mirrors the composer trigger chips in the DSH base (see\n * PermissionSelect / ModelSelect): 28px height, 13/20 medium-secondary\n * label, transparent fill, no outline — the dock stays at the same\n * visual weight as the surrounding chips (dsh-worktree select, standard\n * mode select, Workspace Write, MiniMax-M3 High). The corners stop short\n * of the base's full pill (24px) so the silhouette stays a chip rather\n * than a capsule, per the design brief. */\n\n/* Shared trigger geometry — copied 1:1 from the base composer triggers\n * (PermissionSelect .trigger_958825969 / ModelSelect .trigger_958825969). */\n.dock_958825969 {\n display: inline-flex;\n align-items: stretch;\n height: 28px;\n border-radius: 6px;\n background: transparent;\n color: var(--dsw-alias-label-secondary, #81858c);\n font-size: 13px;\n line-height: 20px;\n font-weight: 500;\n min-height: 28px;\n overflow: hidden;\n}\n\n.chip_958825969 {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n height: 100%;\n padding: 0 8px 0 8px;\n border: none;\n border-radius: 0;\n background: transparent;\n color: inherit;\n font: inherit;\n font-size: 13px;\n line-height: 20px;\n font-weight: 500;\n cursor: pointer;\n}\n\n.chip_958825969:hover {\n background: var(--dsw-alias-interactive-bg-hover, color-mix(in srgb, currentColor 8%, transparent));\n}\n\n.branch_958825969 {\n max-width: 16em;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n/* Confirm flyout: the second-level panel opening right of the branch\n * card (the base Menu's submenu posture: r20, elevation-prominent\n * hairline, --dsw-specific-menu with --dsw-menu-backdrop-filter — host 0.1.2 Menu card;\n * hosts without the token fall back to the old r12-era shadow-lv3). Width is content-driven — it follows\n * the longest line (for remote picks that is the subject line's branch\n * name) — floored at 168px (the Cancel/Confirm pair plus padding: short\n * branch names must not squeeze the buttons into wrapping \"切换中…\"\n * mid-word) and capped at 400px; BranchMenu also clamps it inline to the\n * room right of the card. Beyond the cap the lines wrap. border-box so\n * every width arm includes the card chrome. */\n.popCard_958825969 {\n box-sizing: border-box;\n position: fixed;\n z-index: 1000;\n width: max-content;\n min-width: 168px;\n max-width: min(400px, 80vw);\n padding: 4px;\n border: 0;\n border-radius: 20px;\n background: var(--dsw-specific-menu, var(--dsw-alias-surface-raised, #ffffff));\n backdrop-filter: var(--dsw-menu-backdrop-filter);\n --dsw-elevation-stroke-color: var(--dsw-alias-border-l1);\n /* Elevation-prominent carries the hairline stroke; the lv3 fallback\n * keeps a shadow where the token does not exist. */\n box-shadow: var(--dsw-elevation-prominent, var(--dsw-shadow-lv3));\n display: flex;\n flex-direction: column;\n gap: 2px;\n}\n\n.popAsk_958825969 {\n margin: 0;\n padding: 6px 8px;\n font-size: 13px;\n line-height: 20px;\n color: inherit;\n /* Branch names are long unbroken tokens — plain wrapping would let them\n * overflow the capped card instead of breaking. */\n overflow-wrap: anywhere;\n}\n\n/* Remote confirm's name line: the branch itself, weight 500 like the leaf\n * rows — the subject the ask line refers to (\"该远程分支\"). A dedicated\n * line keeps the long unbroken token away from the sentence, so wraps\n * happen inside the name instead of shredding the Chinese around it. */\n.popSubject_958825969 {\n margin: 0;\n padding: 0 8px 2px;\n font-size: 13px;\n line-height: 20px;\n font-weight: 500;\n color: inherit;\n overflow-wrap: anywhere;\n}\n\n/* The removal confirm's consequence lines (dirty/ahead/archived sessions):\n * subdued one-row facts under the ask, the dirty count alone in the warning\n * tone — mirroring the manager dialog's fact block. */\n.popDetails_958825969 {\n padding: 0 8px 2px;\n}\n\n.popDetail_958825969 {\n margin: 0;\n font-size: 12px;\n line-height: 18px;\n color: var(--dsw-alias-label-secondary, rgba(128, 128, 128, 0.9));\n overflow-wrap: anywhere;\n}\n\n.popDetailWarn_958825969 {\n color: var(--dsw-alias-status-danger, #d5304f);\n}\n\n.popActions_958825969 {\n display: flex;\n gap: 2px;\n padding: 2px;\n}\n\n/* Menu-row-like buttons: transparent fill, hover tint, same 13px type.\n * nowrap keeps labels like 切换中… from splitting mid-word when the flyout\n * is pinned to its minimum width by a short branch name. */\n.popActions_958825969 button {\n flex: 1 1 0;\n height: 28px;\n border: none;\n border-radius: 8px;\n background: transparent;\n color: inherit;\n font: inherit;\n font-size: 13px;\n white-space: nowrap;\n cursor: pointer;\n}\n\n.popActions_958825969 button:hover:not(:disabled) {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.popActions_958825969 button:disabled {\n opacity: 0.5;\n cursor: default;\n}\n\n.popActions_958825969 button:last-child {\n color: var(--dsw-alias-label-primary-bluish, #4186f0);\n font-weight: 500;\n}\n\n/* ── Branch picker popup (BranchMenu) ──────────────────────────────\n * Upward-opening card pinned above the chip, in the Menu's card chrome\n * (r20, elevation-prominent hairline, --dsw-specific-menu with\n * --dsw-menu-backdrop-filter — host Menu/PermissionSelect card;\n * hosts without the token fall back to shadow-lv3). Three owner requirements shape the geometry:\n *\n * 1. height cap: min(420px, 60vh) on the card, only the rows area\n * scrolls (.menuRows_958825969) — heading and search stay pinned;\n * 2. the search field sits pinned at the card's bottom edge, directly\n * above the chip, with the scrolling rows above it;\n * 3. the card is CSS-`bottom`-pinned ~6px above the chip's top and so\n * grows entirely upward — it can never cover the composer or fill\n * the viewport, whatever the branch count.\n *\n * The inline `left`/`bottom` come from BranchMenu's placement pass; the\n * width is fixed here (design 360px — 320 tree + 40 tool strip — and\n * viewport-capped) so horizontal clamping stays deterministic without\n * measuring the card. */\n\n/* The portal lands directly under document.body_958825969, outside the shell's\n * box-sizing reset — content-box default would add padding/border on top\n * of the declared width (360 became 370, and the width:100% children\n * padded 16px past the card's right clip, shearing off their rounded\n * corners). Every width-declared box in this popup opts back into\n * border-box: the card, the toolbar, the rows, and the search input. */\n.menuCard_958825969 {\n box-sizing: border-box;\n position: fixed;\n z-index: 1000;\n display: flex;\n flex-direction: row;\n width: min(360px, calc(100vw - 24px));\n max-height: min(420px, 60vh);\n overflow: hidden;\n padding: 4px;\n border: 0;\n border-radius: 20px;\n background: var(--dsw-specific-menu, var(--dsw-alias-surface-raised, #ffffff));\n backdrop-filter: var(--dsw-menu-backdrop-filter);\n --dsw-elevation-stroke-color: var(--dsw-alias-border-l1);\n /* Same elevation card as .popCard_958825969 (host 0.1.2 Menu); lv3 fallback\n * for hosts without the token. */\n box-shadow: var(--dsw-elevation-prominent, var(--dsw-shadow-lv3));\n --dsh-scrollbar-thumb: var(--dsw-alias-scrollbar-bg-l2);\n --dsh-scrollbar-thumb-hover: var(--dsw-alias-scrollbar-hover-l2);\n}\n\n/* Left tool strip (IDEA branch-panel posture at popup scale): a narrow\n * column of icon-only buttons for actions that do not belong in the tree\n * — locate the checked-out branch, expand/collapse the whole tree.\n * Buttons stack from the BOTTOM up: the card's height shrinks when the\n * repo has few branches, so a top-anchored strip would drift around and\n * the buttons would sit far from the search bar; anchoring to the bottom\n * pins them at a stable spot (beside the search row) whatever the height. */\n.menuToolbar_958825969 {\n box-sizing: border-box;\n display: flex;\n flex-direction: column;\n justify-content: flex-end;\n gap: 2px;\n flex-shrink: 0;\n width: 32px;\n padding: 0 2px;\n margin-right: 4px;\n border-right: 0.5px solid color-mix(in srgb, currentColor 12%, transparent);\n}\n\n.menuToolButton_958825969 {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 28px;\n height: 28px;\n margin: 0 auto;\n border: none;\n border-radius: 8px;\n background: transparent;\n color: inherit;\n cursor: pointer;\n}\n\n.menuToolButton_958825969:hover:not(:disabled) {\n background: var(--dsw-alias-interactive-bg-hover, color-mix(in srgb, currentColor 8%, transparent));\n}\n\n.menuToolButton_958825969:disabled {\n opacity: 0.4;\n cursor: default;\n}\n\n/* The sync tools while their action runs: a soft opacity pulse. The glyph\n * is a directional arrow, so the circular-refresh spin read as garbled\n * rotation — pulsing keeps the arrow legible while clearly \"live\", and the\n * button keeps full opacity (the usual disabled dim would read as broken\n * for an in-flight network operation). */\n.menuToolButtonRunning_958825969 svg {\n animation: menuToolPulse 700ms ease-in-out infinite alternate;\n}\n\n.menuToolButtonRunning_958825969:disabled {\n opacity: 1;\n cursor: progress;\n}\n\n@keyframes menuToolPulse {\n from { opacity: 1; }\n to { opacity: 0.25; }\n}\n\n/* The main column: heading + scrollable rows + pinned search. */\n.menuMain_958825969 {\n box-sizing: border-box;\n display: flex;\n flex-direction: column;\n flex: 1 1 auto;\n min-width: 0;\n}\n\n/* Non-interactive group heading — the Menu label row's posture. */\n.menuHeading_958825969 {\n padding: 6px 8px 4px;\n font-size: 13px;\n line-height: 20px;\n color: var(--dsw-alias-label-secondary, #81858c);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n flex-shrink: 0;\n}\n\n/* The only scrollable region: shrinks within the capped card so the\n * heading above and the search below stay visible while rows scroll. */\n.menuRows_958825969 {\n flex: 1 1 auto;\n min-height: 0;\n overflow-y: auto;\n}\n\n/* Menu-row-like buttons: transparent fill, hover tint, same 13px type.\n * Weight 500 mirrors the chip's branch label (the portal escapes the\n * composer's font context, so the match must be explicit — the inherited\n * body weight is 400 and reads visibly lighter than the chip beside it). */\n.menuRow_958825969 {\n box-sizing: border-box; /* width:100% + padding must stay inside .menuRows_958825969 */\n display: flex;\n align-items: center;\n gap: 8px;\n width: 100%;\n min-height: 28px;\n padding: 4px 8px;\n border: none;\n border-radius: 8px;\n background: transparent;\n color: inherit;\n font: inherit;\n font-size: 13px;\n font-weight: 500;\n line-height: 20px;\n text-align: left;\n cursor: pointer;\n}\n\n.menuRow_958825969:hover:not(:disabled) {\n background: var(--dsw-alias-interactive-bg-hover, color-mix(in srgb, currentColor 8%, transparent));\n}\n\n/* The checked-out branch: a persistent tint a step above the resting rows\n * so it reads as \"you are here\" the moment the list scrolls it into view\n * (BranchMenu centers it on open) — the trailing check alone is easy to\n * miss from a distance. */\n.menuRowSelected_958825969,\n.menuRowSelected_958825969:hover:not(:disabled) {\n background: color-mix(in srgb, currentColor 10%, transparent);\n}\n\n/* IDEA-style selection: the clicked row gets a bluish fill, layering over\n * the HEAD tint when the same row is both — selection communicates intent,\n * the HEAD tint communicates position, and they coexist (IDEA 4.4). */\n.menuRowPicked_958825969,\n.menuRowPicked_958825969:hover:not(:disabled) {\n background: color-mix(in srgb, var(--dsw-alias-label-primary-bluish, #4186f0) 18%, transparent);\n}\n\n.menuRowLabel_958825969 {\n flex: 1 1 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n/* Upstream divergence arrows (↑N/↓N) trailing a local branch row, before\n * the HEAD check: the count badge tone at a smaller size so a busy list\n * still reads the name first. Plain text arrows — no base-set icon pairs\n * a direction with a count. tabular-nums keeps ↑10 from nudging neighbors\n * as the digits tick. */\n.menuRowArrow_958825969 {\n flex-shrink: 0;\n font-size: 11px;\n line-height: 20px;\n font-weight: 400;\n font-variant-numeric: tabular-nums;\n color: color-mix(in srgb, currentColor 55%, transparent);\n}\n\n/* Search hit highlight (IDEA): the matched substring gets a warm tint\n * behind it — theme-agnostic, legible in both light and dark cards. */\n.menuSearchMark_958825969 {\n background: color-mix(in srgb, #b58900 38%, transparent);\n border-radius: 2px;\n}\n\n/* Tree group rows (folder headers) — BranchMenu's '/' prefix tree: the\n * same row geometry as .menuRow_958825969, but labeled in the secondary tone so the\n * leaf rows pop, with a count badge and a chevron that turns on expand. */\n.menuGroup_958825969 {\n box-sizing: border-box;\n display: flex;\n align-items: center;\n gap: 6px;\n width: 100%;\n min-height: 28px;\n padding: 4px 8px;\n border: none;\n border-radius: 8px;\n background: transparent;\n color: var(--dsw-alias-label-secondary, #81858c);\n font: inherit;\n font-size: 13px;\n font-weight: 500;\n line-height: 20px;\n text-align: left;\n cursor: pointer;\n}\n\n.menuGroup_958825969:hover {\n background: var(--dsw-alias-interactive-bg-hover, color-mix(in srgb, currentColor 8%, transparent));\n}\n\n/* The two top-level section headers (local/remote branches) step ABOVE the\n * folder headers they contain: primary label tone like the leaf rows plus\n * a heavier weight, so the nav hierarchy reads group > row > folder — the\n * folder headers keep the secondary tone that alone would leave the\n * sections as the faintest (and most important) rows on the list. */\n.menuGroupTop_958825969 {\n color: inherit;\n font-weight: 600;\n}\n\n.menuGroupLabel_958825969 {\n flex: 1 1 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.menuGroupChevron_958825969 {\n flex-shrink: 0;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 12px;\n height: 12px;\n transition: transform 120ms ease;\n}\n\n.menuGroupChevronOpen_958825969 {\n transform: rotate(90deg);\n}\n\n.menuGroupCount_958825969 {\n flex-shrink: 0;\n font-size: 12px;\n line-height: 20px;\n font-weight: 400;\n color: color-mix(in srgb, currentColor 55%, transparent);\n}\n\n/* Zero-search-hit state: centered secondary line inside the rows area. */\n.menuEmpty_958825969 {\n padding: 10px 8px;\n font-size: 13px;\n line-height: 20px;\n color: var(--dsw-alias-label-secondary, #81858c);\n text-align: center;\n}\n\n/* Bottom-pinned search field, separated from the rows by a hairline.\n * Borderless so it reads as part of the card, focus shown as the\n * standard hover tint rather than an outline. */\n.menuSearchWrap_958825969 {\n flex-shrink: 0;\n margin-top: 2px;\n padding-top: 4px;\n border-top: 0.5px solid color-mix(in srgb, currentColor 12%, transparent);\n}\n\n.menuSearch_958825969 {\n box-sizing: border-box; /* width:100% + padding must stay inside the wrap */\n width: 100%;\n height: 30px;\n padding: 0 8px;\n border: none;\n border-radius: 8px;\n background: transparent;\n color: inherit;\n font: inherit;\n font-size: 13px;\n /* Weight 500 mirrors the chip's branch label (see .menuRow_958825969) — the portal\n * escapes the composer's font context and would inherit body's 400. */\n font-weight: 500;\n line-height: 20px;\n}\n\n.menuSearch_958825969::placeholder {\n color: var(--dsw-alias-label-secondary, #81858c);\n}\n\n.menuSearch_958825969:focus {\n outline: none;\n background: var(--dsw-alias-interactive-bg-hover, color-mix(in srgb, currentColor 8%, transparent));\n}\n\n/* ── Create/rename flyouts (BranchMenu row context menu) ────────────\n * Open IN PLACE of the row menu (at its point, clamped) and hold the whole\n * flow in one panel: ask line, input, live hint, and the Cancel/confirm\n * pair — typing the name and pressing the button fires in one stroke, no\n * second confirm step. */\n\n/* The naming input inside the flyout: borderless, part of the card, focus\n * shown as the standard hover tint. Fixed width — the popCard is\n * max-content, so a percentage width would chase the input's own default\n * size instead of the design width. */\n.menuCreate_958825969 {\n box-sizing: border-box;\n width: 248px;\n height: 30px;\n padding: 0 8px;\n border: none;\n border-radius: 8px;\n background: transparent;\n color: inherit;\n font: inherit;\n font-size: 13px;\n /* Weight 500 mirrors the chip's branch label (see .menuSearch_958825969). */\n font-weight: 500;\n line-height: 20px;\n}\n\n.menuCreate_958825969::placeholder {\n color: var(--dsw-alias-label-secondary, #81858c);\n}\n\n.menuCreate_958825969:focus {\n outline: none;\n background: var(--dsw-alias-interactive-bg-hover, color-mix(in srgb, currentColor 8%, transparent));\n}\n\n.menuCreate_958825969:disabled {\n opacity: 0.5;\n}\n\n/* The error line under the input — only rendered while the draft is\n * unacceptable with a non-empty reason; the input itself stays neutral,\n * the line carries the state. */\n.menuCreateHintBad_958825969 {\n margin: 0;\n padding: 0 8px;\n font-size: 12px;\n line-height: 16px;\n color: var(--dsw-alias-label-error);\n}\n\n/* ── Row context menu (BranchMenu right-click) ─────────────────────\n * Cursor-anchored transient over the branch card: translucent menu chrome\n * (--dsw-specific-menu with --dsw-menu-backdrop-filter, elevation-prominent hairline,\n * lv3 fallback) at a smaller radius — r20 would round a launcher this\n * short into a pill, r12 keeps the corners proportionate to a handful of\n * rows (two on a worktree row, up to six plus a header on a branch row).\n * Width hugs the widest item\n * (shrink-to-fit, no design width) with a generous cap for safety —\n * a launcher reads better snug than stretched. Positioned once at open\n * and dismissed rather than re-placed when the page moves (see\n * BranchMenu's ctx placement pass); z-index one step above the popCard so\n * it never slides under the confirm flyout. */\n.ctxCard_958825969 {\n box-sizing: border-box;\n position: fixed;\n z-index: 1001;\n width: max-content;\n max-width: 280px;\n padding: 4px;\n border: 0;\n border-radius: 12px;\n background: var(--dsw-specific-menu, var(--dsw-alias-surface-raised, #ffffff));\n backdrop-filter: var(--dsw-menu-backdrop-filter);\n --dsw-elevation-stroke-color: var(--dsw-alias-border-l1);\n box-shadow: var(--dsw-elevation-prominent, var(--dsw-shadow-lv3));\n display: flex;\n flex-direction: column;\n gap: 2px;\n}\n\n/* The target row's name, above the verbs. Unlike the items this one holds\n * a branch name, so it is the one line allowed to hit the 280px cap and\n * ellipsize (the full name rides in a title). Secondary tone and no hover\n * affordance: it is a label, not a seventh item. */\n.ctxHeader_958825969 {\n padding: 4px 8px 2px;\n font-size: 12px;\n line-height: 16px;\n color: var(--dsw-alias-label-secondary);\n overflow: hidden;\n white-space: nowrap;\n text-overflow: ellipsis;\n}\n\n/* Menu-item rows: the .menuRow_958825969 geometry at a hair lighter weight — a\n * launcher's labels read as commands, not data. nowrap: the labels are\n * fixed copy (no branch names), the 280px cap is unreachable by design. */\n.ctxItem_958825969 {\n display: inline-flex;\n align-items: center;\n gap: 8px;\n width: 100%;\n min-height: 28px;\n padding: 4px 8px;\n border: none;\n border-radius: 8px;\n background: transparent;\n color: inherit;\n font: inherit;\n font-size: 13px;\n line-height: 20px;\n text-align: left;\n white-space: nowrap;\n cursor: pointer;\n}\n\n.ctxItem_958825969:hover,\n.ctxItem_958825969:focus-visible {\n background: var(--dsw-alias-interactive-bg-hover, color-mix(in srgb, currentColor 8%, transparent));\n}\n\n/* The running-session withhold (worktree removal): a disabled item stays\n * visible — the reason it is missing lives on its hover title, and a verb\n * that appears and disappears with session state reads as a bug, not a\n * rule. No hover tint, no pointer; the label dims instead of vanishing. */\n.ctxItem_958825969:disabled {\n cursor: default;\n opacity: 0.45;\n}\n\n.ctxItem_958825969:disabled:hover {\n background: transparent;\n}\n\n/* The arrow-key walk lands here, so the focused item must read as strongly\n * as a hovered one — the tint above carries it, the default ring would sit\n * outside the 8px radius. */\n.ctxItem_958825969:focus-visible {\n outline: none;\n}\n\n/* Leading glyph rides the label tone, a step quieter. */\n.ctxItem_958825969 svg {\n flex-shrink: 0;\n opacity: 0.75;\n}\n\n.ctxItemLabel_958825969 {\n flex: 1 1 0;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n";
586
+ const tagId$3 = "dsh-git-branch-manage/BranchChip.module.css";
587
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=\"" + tagId$3 + "\"]") === null) {
505
588
  const tag = document.createElement("style");
506
589
  tag.dataset.plugin = "dsh-git-branch-manage";
507
- tag.dataset.pluginCss = tagId$2;
508
- tag.textContent = css$2;
590
+ tag.dataset.pluginCss = tagId$3;
591
+ tag.textContent = css$3;
509
592
  document.head.appendChild(tag);
510
593
  }
511
594
  var BranchChip_module_default = {
@@ -2401,7 +2484,7 @@ window.__ModuleLoader__.load({
2401
2484
  * {@link buildLinkedWorktreeRows}), so nothing here ever has to express
2402
2485
  * "not usable in this directory".
2403
2486
  */
2404
- function buildBranchRows(branches, worktrees) {
2487
+ function buildBranchRows$1(branches, worktrees) {
2405
2488
  const held = new Set(worktrees.flatMap((w) => w.main || w.branch === void 0 ? [] : [w.branch]));
2406
2489
  const localMap = new Map(branches.filter((b) => b.kind === "local").map((b) => [b.name, b]));
2407
2490
  return [
@@ -2913,7 +2996,7 @@ window.__ModuleLoader__.load({
2913
2996
  const rows = (0, react.useMemo)(() => {
2914
2997
  if (facts === null) return [];
2915
2998
  if (inLinkedWorktree) return buildLinkedWorktreeRows(facts.branches, facts.worktrees, facts.currentBranch);
2916
- return buildBranchRows(facts.branches, facts.worktrees);
2999
+ return buildBranchRows$1(facts.branches, facts.worktrees);
2917
3000
  }, [facts, inLinkedWorktree]);
2918
3001
  const localNames = (0, react.useMemo)(() => rows.filter((row) => row.kind !== "remote").map((row) => row.name), [rows]);
2919
3002
  if (facts === null) return null;
@@ -3100,7 +3183,7 @@ window.__ModuleLoader__.load({
3100
3183
  const KEEP_FIELD = "keepWorktrees";
3101
3184
  const COPY_FILES_FIELD = "postCreateCopyFiles";
3102
3185
  /**
3103
- * Stages the settings edit over the `git-worktree` scope.
3186
+ * Stages settings edits over the `dsh-git-branch` Host entry.
3104
3187
  *
3105
3188
  * The form publishes through a snapshot store because the slot component
3106
3189
  * reads through a snapshot selector while both the scope and the local draft
@@ -3116,7 +3199,7 @@ window.__ModuleLoader__.load({
3116
3199
  failed = false;
3117
3200
  switchFailed = null;
3118
3201
  /**
3119
- * @param scope - the bound configuration form for the `git-worktree` namespace.
3202
+ * @param scope - the bound configuration form for the `dsh-git-branch` Host entry.
3120
3203
  */
3121
3204
  constructor(scope) {
3122
3205
  this.scope = scope;
@@ -3296,222 +3379,1798 @@ window.__ModuleLoader__.load({
3296
3379
  }
3297
3380
  };
3298
3381
  //#endregion
3299
- //#region src/client/prune-history.ts
3300
- const KEY = "git-worktree:prune-history";
3301
- /** The log is a recent-activity trail, not an archive: the oldest runs fall
3302
- * off once this many are kept. */
3303
- const MAX_RUNS = 20;
3304
- /** Append one finished run (newest first), trimming the tail past
3305
- * {@link MAX_RUNS}. Best-effort: a storage write that throws (private mode,
3306
- * quota) swallows — the toast already told the user what happened. */
3307
- function recordPruneRun(entry) {
3308
- try {
3309
- const next = [entry, ...readPruneHistory()].slice(0, MAX_RUNS);
3310
- localStorage.setItem(KEY, JSON.stringify(next));
3311
- } catch {}
3312
- }
3313
- /** Structural check of ONE stored run. The payload is browser-local JSON an
3314
- * older build (or anything else holding this key) may have written in a
3315
- * different shape, and the settings card reads `run.removed.length` straight
3316
- * into its render — a top-level `Array.isArray` alone would let `[1, 2]`
3317
- * through and take the whole card down with a TypeError. Only the fields the
3318
- * card actually touches are checked; their element shapes stay trusted (a
3319
- * malformed member degrades one line, not the page). */
3320
- function isPruneRunEntry(value) {
3321
- if (typeof value !== "object" || value === null) return false;
3322
- const entry = value;
3323
- return typeof entry.at === "number" && Array.isArray(entry.removed) && Array.isArray(entry.skippedDirty) && Array.isArray(entry.failed);
3324
- }
3325
- /** The recorded runs, newest first; an unreadable/foreign payload reads as
3326
- * "no history" rather than throwing into the settings render — entry by
3327
- * entry, so one bad record costs its own line and nothing else. */
3328
- function readPruneHistory() {
3329
- try {
3330
- const raw = localStorage.getItem(KEY);
3331
- if (raw === null) return [];
3332
- const parsed = JSON.parse(raw);
3333
- return Array.isArray(parsed) ? parsed.filter(isPruneRunEntry) : [];
3334
- } catch {
3335
- return [];
3336
- }
3337
- }
3338
- //#endregion
3339
- //#region src/client/relative-time.ts
3340
- /**
3341
- * Compact relative time for trailing labels, as a structured bucket the
3342
- * renderer localizes ("now"/"5min"/"3h"/"2d"/"4mo"/"1y" in en).
3343
- */
3344
- function relativeTime(updatedAt, now) {
3345
- const MIN = 6e4;
3346
- const HOUR = 36e5;
3347
- const DAY = 864e5;
3348
- const diff = Math.max(0, now - updatedAt);
3349
- if (diff < MIN) return {
3350
- unit: "now",
3351
- n: 0
3352
- };
3353
- if (diff < HOUR) return {
3354
- unit: "minutes",
3355
- n: Math.floor(diff / MIN)
3356
- };
3357
- if (diff < DAY) return {
3358
- unit: "hours",
3359
- n: Math.floor(diff / HOUR)
3360
- };
3361
- if (diff < 30 * DAY) return {
3362
- unit: "days",
3363
- n: Math.floor(diff / DAY)
3364
- };
3365
- if (diff < 365 * DAY) return {
3366
- unit: "months",
3367
- n: Math.floor(diff / (30 * DAY))
3368
- };
3369
- return {
3370
- unit: "years",
3371
- n: Math.floor(diff / (365 * DAY))
3372
- };
3373
- }
3374
- /** Localized compact relative time ("刚刚"/"5分钟" in zh, "now"/"5min" in en). */
3375
- function timeLabel(updatedAt, now, t) {
3376
- const { unit, n } = relativeTime(updatedAt, now);
3377
- if (unit === "now") return t("time.now");
3378
- if (unit === "minutes") return t("time.minutes", { n });
3379
- if (unit === "hours") return t("time.hours", { n });
3380
- if (unit === "days") return t("time.days", { n });
3381
- if (unit === "months") return t("time.months", { n });
3382
- return t("time.years", { n });
3383
- }
3384
- //#endregion
3385
- //#region src/client/concurrency.ts
3386
- /**
3387
- * Bounded-concurrency map for the browser half's fan-out calls.
3388
- *
3389
- * Every plugin route that answers a batch is a git process per item on the
3390
- * host side, and on Windows a process spawn is the expensive part — the
3391
- * `/group` route already caps its own fan-out for exactly this reason. The
3392
- * browser side has the same duty for the calls it makes ONE PER ITEM
3393
- * (the manager dialog inspects every scanned worktree): a bare
3394
- * `Promise.all(items.map(...))` hands the host as many concurrent spawns as
3395
- * the user happens to have directories.
3396
- *
3397
- * @module git-worktree/client/concurrency
3398
- */
3382
+ //#region src/client/git-panel-data.ts
3399
3383
  /**
3400
- * Map over `items` with at most `limit` calls in flight, preserving INPUT
3401
- * ORDER in the result (the caller zips results back onto its rows, so
3402
- * completion order must not leak). Rejections propagate like `Promise.all`:
3403
- * callers that want per-item tolerance catch inside `fn`.
3404
- *
3405
- * @param items - the inputs, mapped in order.
3406
- * @param limit - maximum concurrent calls; values under 1 are clamped to 1.
3407
- * @param fn - the per-item async work, given the item and its index.
3408
- * @returns the results, index-aligned with `items`.
3384
+ * Classify raw %D decoration segments: `HEAD -> main` (and bare `HEAD`) is
3385
+ * the head badge, `tag: x` a tag, anything containing `/` a remote branch,
3386
+ * the rest a local branch.
3409
3387
  */
3410
- async function mapLimit(items, limit, fn) {
3411
- const queue = items.map((item, index) => [item, index]);
3412
- const results = [];
3413
- const width = Math.max(1, Math.floor(limit));
3414
- let cursor = 0;
3415
- const worker = async () => {
3416
- for (;;) {
3417
- const entry = queue[cursor];
3418
- cursor += 1;
3419
- if (entry === void 0) return;
3420
- const [item, index] = entry;
3421
- results[index] = await fn(item, index);
3388
+ function classifyRefs(refs) {
3389
+ const badges = [];
3390
+ for (const ref of refs) {
3391
+ if (ref === "HEAD" || ref.startsWith("HEAD -> ")) {
3392
+ badges.push({
3393
+ kind: "head",
3394
+ text: ref
3395
+ });
3396
+ continue;
3422
3397
  }
3423
- };
3424
- await Promise.all(Array.from({ length: Math.min(width, queue.length) }, worker));
3425
- return results;
3426
- }
3427
- //#endregion
3428
- //#region src/client/scan-groups.ts
3429
- /** Group the scan for display: project groups before legacy groups (same
3430
- * repo can appear under both), repositories alphabetical within a source,
3431
- * rows within a group by branch name, all numeric-aware so `feat/v2` sorts
3432
- * past `feat/v10`. Grouping is display-order only — the removal flows
3433
- * address rows by path. */
3434
- function groupScanEntries(entries) {
3435
- const byKey = /* @__PURE__ */ new Map();
3436
- const orphans = [];
3437
- for (const entry of entries) {
3438
- if (entry.repoName === null) {
3439
- orphans.push(entry);
3398
+ if (ref.startsWith("tag: ")) {
3399
+ badges.push({
3400
+ kind: "tag",
3401
+ text: ref.slice(5)
3402
+ });
3440
3403
  continue;
3441
3404
  }
3442
- const key = `${entry.source}\u0000${entry.repoName}`;
3443
- const bucket = byKey.get(key);
3444
- if (bucket === void 0) byKey.set(key, {
3445
- source: entry.source,
3446
- repoName: entry.repoName,
3447
- list: [entry]
3405
+ if (ref.includes("/")) {
3406
+ badges.push({
3407
+ kind: "remote",
3408
+ text: ref
3409
+ });
3410
+ continue;
3411
+ }
3412
+ badges.push({
3413
+ kind: "local",
3414
+ text: ref
3448
3415
  });
3449
- else bucket.list.push(entry);
3450
3416
  }
3451
- const cmp = (a, b) => a.localeCompare(b, void 0, { numeric: true });
3452
- const groups = [...byKey.values()].sort((a, b) => a.source === b.source ? cmp(a.repoName, b.repoName) : a.source === "project" ? -1 : 1).map(({ source, repoName, list }) => ({
3453
- repoName,
3454
- source,
3455
- entries: list.slice().sort((a, b) => cmp(a.branch ?? "", b.branch ?? ""))
3456
- }));
3457
- return orphans.length > 0 ? [...groups, {
3458
- repoName: null,
3459
- source: "orphans",
3460
- entries: orphans
3461
- }] : groups;
3417
+ return badges;
3462
3418
  }
3463
- //#endregion
3464
- //#region src/client/worktree-remove-flow.ts
3465
3419
  /**
3466
- * Remove one worktree completely. A refused git removal rethrows UNLESS the
3467
- * directory is actually gone (the Windows half-removal shape: git already
3468
- * unregistered and emptied the tree, then failed the final rmdir — the git
3469
- * half is done, so the flow continues with the DSH half). Session archives
3470
- * never abort the flow (a rejected archive is logged and skipped); a failed
3471
- * workspace unregistration DOES reject — the caller's UI owns the retry.
3420
+ * Split the working-tree rows into the two panel groups. Untracked files
3421
+ * render in the unstaged group as `?` (git add still stages them); an
3422
+ * unmerged conflict renders in the unstaged group as `U` unchecked (git add
3423
+ * resolves it); a rename's checkbox toggles BOTH paths (unstage needs the
3424
+ * old path).
3472
3425
  */
3473
- async function removeWorktreeFully(deps, target) {
3474
- try {
3475
- await deps.removeWorktree(target.path, target.force);
3476
- } catch (reason) {
3477
- if ((await deps.probeDirectories([target.path]).catch(() => void 0))?.exists[target.path] !== false) throw reason;
3426
+ function groupChanges(changes) {
3427
+ const staged = [];
3428
+ const unstaged = [];
3429
+ for (const change of changes) {
3430
+ if (change.untracked === true) {
3431
+ unstaged.push({
3432
+ path: change.path,
3433
+ status: "?",
3434
+ staged: false
3435
+ });
3436
+ continue;
3437
+ }
3438
+ if (change.staged !== void 0) staged.push({
3439
+ path: change.path,
3440
+ ...change.oldPath !== void 0 ? { oldPath: change.oldPath } : {},
3441
+ status: change.staged,
3442
+ staged: true
3443
+ });
3444
+ if (change.unstaged !== void 0) unstaged.push({
3445
+ path: change.path,
3446
+ ...change.oldPath !== void 0 ? { oldPath: change.oldPath } : {},
3447
+ status: change.unstaged,
3448
+ staged: false
3449
+ });
3478
3450
  }
3479
- for (const sessionId of target.archiveSessionIds ?? []) await deps.archiveSession(sessionId).catch((reason) => {
3480
- console.warn("session archive rejected during worktree removal:", reason);
3481
- });
3482
- if (target.workspaceId !== void 0) await deps.deleteWorkspace(target.workspaceId);
3451
+ return {
3452
+ staged,
3453
+ unstaged
3454
+ };
3483
3455
  }
3484
- //#endregion
3485
- //#region src/client/worktree-prune.ts
3486
- /** Canonical-ish key for cross-source path comparison: both sides come from
3487
- * the same host, but the scan assembles `<rootDir>/<child>` while the
3488
- * workspace model stores its own canonical spelling — separators are the
3489
- * only drift worth papering over. */
3490
- function pathKey(path) {
3491
- return path.replace(/\\/g, "/").replace(/\/+$/, "");
3456
+ /** Subject-case-insensitive filter over the commit subject and author. */
3457
+ function filterCommits(commits, query) {
3458
+ const needle = query.trim().toLowerCase();
3459
+ if (needle === "") return [...commits];
3460
+ return commits.filter((commit) => commit.subject.toLowerCase().includes(needle) || commit.authorName.toLowerCase().includes(needle));
3492
3461
  }
3493
- /** Freshest `updatedAt` across QUALIFYING sessions (non-blank, non-subagent
3494
- * — the same qualification the archive set uses); 0 when nothing qualifies,
3495
- * which reads as "least active in the room". Shared by the lazy prune's
3496
- * plan input and the manager dialog's last-use column so the two activity
3497
- * figures can never drift apart. */
3498
- function freshestUpdatedAt(summaries) {
3499
- let latest = 0;
3500
- for (const summary of summaries) {
3501
- if (summary === void 0 || summary.blank || summary.origin === "subagent") continue;
3502
- if (summary.updatedAt > latest) latest = summary.updatedAt;
3462
+ /** Merge the next page into the accumulated list, dropping hash duplicates
3463
+ * (a ref moved between fetches must not duplicate rows). */
3464
+ function appendPage(current, page) {
3465
+ const seen = new Set(current.map((commit) => commit.hash));
3466
+ const merged = [...current];
3467
+ for (const commit of page) {
3468
+ if (seen.has(commit.hash)) continue;
3469
+ seen.add(commit.hash);
3470
+ merged.push(commit);
3503
3471
  }
3504
- return latest;
3472
+ return merged;
3473
+ }
3474
+ /** Build the branch rows from a RepoStatus: locals first (current first),
3475
+ * then remote-only branches, sorted by name. */
3476
+ function buildBranchRows(branches, currentBranch) {
3477
+ const locals = [];
3478
+ const remotes = [];
3479
+ for (const branch of branches) {
3480
+ const row = {
3481
+ name: branch.name,
3482
+ kind: branch.kind,
3483
+ current: branch.kind === "local" && branch.name === currentBranch,
3484
+ ...branch.ahead !== void 0 ? { ahead: branch.ahead } : {},
3485
+ ...branch.behind !== void 0 ? { behind: branch.behind } : {}
3486
+ };
3487
+ if (branch.kind === "local") locals.push(row);
3488
+ else remotes.push(row);
3489
+ }
3490
+ const byName = (left, right) => left.name.localeCompare(right.name);
3491
+ locals.sort((left, right) => Number(right.current) - Number(left.current) || byName(left, right));
3492
+ remotes.sort(byName);
3493
+ return {
3494
+ locals,
3495
+ remotes
3496
+ };
3505
3497
  }
3506
3498
  /**
3507
- * Which worktrees the lazy prune removes, least-recently-active first.
3508
- * The cap counts every valid worktree INCLUDING the excluded ones (the
3509
- * fresh creation counts against the cap it may have just overfilled), but
3510
- * excluded directories are never candidates: the plan deletes the
3511
- * overflow's worth of the least-active non-excluded paths, so a cap
3512
- * tighter than the exclusion set simply deletes nothing this round.
3513
- * Ties (two never-used directories) break alphabetically for a stable,
3514
- * testable order.
3499
+ * Nest remote-only branches under their remote (`origin/feat/x` lands in
3500
+ * the `origin` subtree as `feat/x`) — the IDEA-style two-level 远程 tree.
3501
+ * A bare ref directly under refs/remotes (no `/`) is skipped: the branch
3502
+ * list never offers it.
3503
+ */
3504
+ function groupRemotes(remotes) {
3505
+ const groups = /* @__PURE__ */ new Map();
3506
+ for (const row of remotes) {
3507
+ const slash = row.name.indexOf("/");
3508
+ if (slash <= 0 || slash === row.name.length - 1) continue;
3509
+ const remote = row.name.slice(0, slash);
3510
+ const short = row.name.slice(slash + 1);
3511
+ const list = groups.get(remote) ?? [];
3512
+ list.push({
3513
+ ...row,
3514
+ short
3515
+ });
3516
+ groups.set(remote, list);
3517
+ }
3518
+ return [...groups.entries()].map(([remote, branches]) => ({
3519
+ remote,
3520
+ branches
3521
+ })).sort((left, right) => left.remote.localeCompare(right.remote));
3522
+ }
3523
+ //#endregion
3524
+ //#region \0git-worktree-css:/home/runner/work/dsh-git-branch/dsh-git-branch/src/client/GitHistoryPanel.module.css?inline
3525
+ const css$2 = "/* Git panel (sidebar global panel). Three stacked sections over the main\n * column: uncommitted changes + commit box, branches, commit history. Host\n * tokens only; the few status colors fall back to the same literals the\n * settings card uses. */\n\n.page_427795137 {\n display: flex;\n flex-direction: column;\n min-height: 0;\n height: 100%;\n}\n\n.topbar_427795137 {\n display: flex;\n align-items: center;\n gap: 10px;\n padding: 10px 16px 8px;\n}\n\n.back_427795137 {\n flex: none;\n display: inline-flex;\n align-items: center;\n gap: 6px;\n border: 0;\n background: none;\n cursor: pointer;\n font-size: 13px;\n line-height: 1.5;\n color: var(--dsw-alias-label-secondary);\n padding: 4px 8px;\n border-radius: 8px;\n}\n\n.back_427795137:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n color: var(--dsw-alias-label-primary);\n}\n\n.title_427795137 {\n margin: 0;\n font-size: 16px;\n font-weight: 600;\n color: var(--dsw-alias-label-primary);\n}\n\n.repoPill_427795137 {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n min-width: 0;\n font-size: 12px;\n line-height: 1.4;\n color: var(--dsw-alias-label-secondary);\n border: 0.5px solid var(--dsw-alias-border-l4);\n border-radius: 999px;\n padding: 3px 10px;\n white-space: nowrap;\n overflow: hidden;\n}\n\n.repoName_427795137 {\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.branchName_427795137 {\n color: var(--dsw-alias-label-primary);\n font-weight: 500;\n}\n\n.track_427795137 {\n font-variant-numeric: tabular-nums;\n}\n\n.topbarActions_427795137 {\n margin-left: auto;\n display: flex;\n align-items: center;\n gap: 6px;\n flex: none;\n}\n\n.refreshBtn_427795137 {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 28px;\n height: 28px;\n border: 0;\n border-radius: 8px;\n background: none;\n color: var(--dsw-alias-label-secondary);\n cursor: pointer;\n}\n\n.refreshBtn_427795137:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n color: var(--dsw-alias-label-primary);\n}\n\n.refreshBtn_427795137[disabled] {\n opacity: 0.5;\n cursor: default;\n}\n\n.scroll_427795137 {\n flex: 1;\n min-height: 0;\n overflow-y: auto;\n padding: 0 16px 16px;\n display: flex;\n flex-direction: column;\n gap: 10px;\n}\n\n/* Sections NEVER flex-shrink: the scroll container is a definite-height\n * flex column, and a shrinkable child collapses to a fraction of its\n * content while the overflow paints over the following sections (the\n * branch section went empty and the commit box drifted into the history).\n * Natural heights; the container scrolls as a whole. */\n.section_427795137 {\n flex: none;\n border: 0.5px solid var(--dsw-alias-border-l2);\n border-radius: 10px;\n background: var(--dsw-alias-bg-layer-2);\n display: flex;\n flex-direction: column;\n}\n\n.sectionHead_427795137 {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 8px 12px;\n border-bottom: 0.5px solid var(--dsw-alias-border-l2);\n}\n\n.sectionTitle_427795137 {\n margin: 0;\n font-size: 13px;\n font-weight: 600;\n color: var(--dsw-alias-label-primary);\n}\n\n.countBadge_427795137 {\n font-size: 10px;\n line-height: 1;\n border-radius: 999px;\n padding: 3px 7px;\n background: var(--dsw-alias-bg-layer-3);\n color: var(--dsw-alias-label-secondary);\n font-variant-numeric: tabular-nums;\n white-space: nowrap;\n}\n\n.sectionTools_427795137 {\n margin-left: auto;\n display: flex;\n align-items: center;\n gap: 6px;\n}\n\n.sectionBody_427795137 {\n padding: 10px 12px;\n display: flex;\n flex-direction: column;\n gap: 8px;\n}\n\n/* Small action chip used across the section heads and the commit row. */\n.act_427795137 {\n display: inline-flex;\n align-items: center;\n gap: 5px;\n font: inherit;\n font-size: 12px;\n line-height: 1.4;\n cursor: pointer;\n background: var(--dsw-alias-bg-layer-3);\n color: var(--dsw-alias-label-primary);\n border: 0.5px solid var(--dsw-alias-border-l4);\n border-radius: 7px;\n padding: 4px 10px;\n white-space: nowrap;\n}\n\n.act_427795137:hover:not([disabled]) {\n border-color: var(--dsw-alias-brand-primary);\n}\n\n.act_427795137[disabled] {\n opacity: 0.45;\n cursor: default;\n}\n\n.actPrimary_427795137 {\n /* Hardcoded blue by request — the brand token does not resolve reliably\n * inside the better-sidebar panel. */\n background: #4c6bf5;\n border-color: transparent;\n color: #fff;\n}\n\n.actPrimary_427795137:hover:not([disabled]) {\n border-color: transparent;\n filter: brightness(1.06);\n}\n\n/* Uncommitted changes */\n\n.groupTitleRow_427795137 {\n display: flex;\n align-items: center;\n gap: 6px;\n font-size: 12px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.linkBtn_427795137 {\n margin-left: auto;\n background: none;\n border: 0;\n padding: 2px 4px;\n font: inherit;\n font-size: 12px;\n color: var(--dsw-alias-brand-primary);\n cursor: pointer;\n border-radius: 5px;\n}\n\n.linkBtn_427795137:hover:not([disabled]) {\n text-decoration: underline;\n}\n\n.linkBtn_427795137[disabled] {\n opacity: 0.5;\n cursor: default;\n}\n\n.changeRow_427795137 {\n display: grid;\n grid-template-columns: 16px 18px minmax(0, 1fr) auto;\n gap: 8px;\n align-items: center;\n padding: 3px 6px;\n border-radius: 6px;\n font-size: 12px;\n color: var(--dsw-alias-label-primary);\n}\n\n.changeRow_427795137:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.changeCheck_427795137 {\n width: 14px;\n height: 14px;\n accent-color: var(--dsw-alias-brand-primary);\n cursor: pointer;\n margin: 0;\n}\n\n.statusBadge_427795137 {\n width: 16px;\n height: 16px;\n border-radius: 4px;\n font-size: 10px;\n line-height: 1;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n font-weight: 700;\n color: #fff;\n box-sizing: border-box;\n}\n\n.statusA_427795137 { background: var(--dsw-alias-status-success, #218358); }\n.statusM_427795137 { background: var(--dsw-alias-label-warning, #b07000); }\n.statusD_427795137 { background: var(--dsw-alias-status-danger, #d5304f); }\n.statusR_427795137 { background: #8350d8; }\n.statusC_427795137 { background: #8350d8; }\n.statusT_427795137 { background: #5b7a9d; }\n.statusQ_427795137 { background: var(--dsw-alias-label-dimmed, #9aa0aa); }\n.statusU_427795137 { background: #d5502e; }\n\n.changePath_427795137 {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n font-family: ui-monospace, Consolas, monospace;\n font-size: 11.5px;\n direction: rtl;\n text-align: left;\n}\n\n.stageHint_427795137 {\n color: var(--dsw-alias-label-tertiary);\n font-size: 11px;\n flex: none;\n}\n\n.commitBox_427795137 {\n display: flex;\n flex-direction: column;\n gap: 8px;\n margin-top: 4px;\n}\n\n.commitInput_427795137 {\n width: 100%;\n box-sizing: border-box;\n resize: vertical;\n min-height: 52px;\n font: inherit;\n font-size: 12.5px;\n line-height: 1.5;\n border: 0.5px solid var(--dsw-alias-border-l4);\n border-radius: 8px;\n padding: 7px 10px;\n background: var(--dsw-alias-bg-layer-3);\n color: var(--dsw-alias-label-primary);\n outline: none;\n}\n\n.commitInput_427795137:focus {\n border-color: var(--dsw-alias-brand-primary);\n}\n\n.commitActions_427795137 {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n\n.commitHint_427795137 {\n color: var(--dsw-alias-label-tertiary);\n font-size: 11px;\n min-height: 16px;\n}\n\n.split_427795137 {\n margin-left: auto;\n display: flex;\n position: relative;\n}\n\n.splitMain_427795137 {\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n padding: 6px 16px;\n font-size: 13px;\n}\n\n.splitCaret_427795137 {\n border-top-left-radius: 0;\n border-bottom-left-radius: 0;\n margin-left: 1px;\n padding: 6px 6px;\n}\n\n.menu_427795137 {\n position: absolute;\n right: 0;\n top: calc(100% + 4px);\n z-index: 40;\n width: max-content;\n min-width: 0;\n background: var(--dsw-alias-surface-raised, var(--dsw-alias-bg-layer-3));\n border: 0.5px solid var(--dsw-alias-border-l4);\n border-radius: 8px;\n box-shadow: var(--dsw-shadow-lv3);\n backdrop-filter: var(--dsw-menu-backdrop-filter);\n padding: 3px;\n display: flex;\n flex-direction: column;\n}\n\n.menuItem_427795137 {\n display: block;\n width: 100%;\n text-align: left;\n background: none;\n border: 0;\n color: var(--dsw-alias-label-primary);\n font: inherit;\n font-size: 12px;\n line-height: 1.4;\n padding: 5px 10px;\n border-radius: 5px;\n cursor: pointer;\n white-space: nowrap;\n}\n\n.menuItem_427795137:hover:not([disabled]) {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.menuItem_427795137[disabled] {\n opacity: 0.5;\n cursor: default;\n}\n\n.menuSub_427795137 {\n color: var(--dsw-alias-label-tertiary);\n font-size: 11px;\n margin-top: 1px;\n}\n\n/* Branch section — two IDEA-style trees: 本地 → branches, 远程 → per-remote → branches */\n\n.treeHead_427795137 {\n display: flex;\n align-items: center;\n gap: 6px;\n padding: 4px 6px;\n border-radius: 6px;\n cursor: pointer;\n font-size: 12px;\n font-weight: 600;\n color: var(--dsw-alias-label-secondary);\n user-select: none;\n}\n\n.treeHead_427795137:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n color: var(--dsw-alias-label-primary);\n}\n\n.treeChevron_427795137 {\n display: inline-flex;\n align-items: center;\n color: var(--dsw-alias-label-tertiary);\n}\n\n.treeChildren_427795137 {\n display: flex;\n flex-direction: column;\n margin-left: 12px;\n}\n\n.treeHeadNest_427795137 {\n font-weight: 400;\n}\n\n.treeNest_427795137 {\n margin-left: 12px;\n}\n\n.branchRow_427795137 {\n display: grid;\n grid-template-columns: 20px minmax(0, 1fr) auto;\n gap: 8px;\n align-items: center;\n padding: 5px 8px;\n border-radius: 7px;\n cursor: pointer;\n font-size: 12.5px;\n color: var(--dsw-alias-label-primary);\n}\n\n.branchRow_427795137:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.branchRowCurrent_427795137 {\n background: var(--dsw-alias-bg-layer-3);\n cursor: default;\n}\n\n.branchRowCurrent_427795137:hover {\n background: var(--dsw-alias-bg-layer-3);\n}\n\n.branchMark_427795137 {\n color: var(--dsw-alias-brand-primary);\n display: inline-flex;\n align-items: center;\n justify-content: center;\n}\n\n.branchName_427795137 {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n font-weight: 500;\n}\n\n.branchKind_427795137 {\n color: var(--dsw-alias-label-tertiary);\n font-size: 10px;\n margin-left: 6px;\n}\n\n.branchTrack_427795137 {\n color: var(--dsw-alias-label-tertiary);\n font-size: 11px;\n font-variant-numeric: tabular-nums;\n flex: none;\n}\n\n/* The branch action menu is portaled; fixed positioning by row rect. */\n.floatMenu_427795137 {\n position: fixed;\n z-index: 60;\n min-width: 190px;\n}\n\n.menuHead_427795137 {\n padding: 6px 10px 5px;\n font-size: 11px;\n color: var(--dsw-alias-label-tertiary);\n border-bottom: 0.5px solid var(--dsw-alias-border-l2);\n margin-bottom: 3px;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.menuHeadStrong_427795137 {\n color: var(--dsw-alias-label-primary);\n font-weight: 500;\n}\n\n.menuDanger_427795137 {\n color: var(--dsw-alias-status-danger, #d5304f);\n}\n\n.formField_427795137 {\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n\n.formLabel_427795137 {\n font-size: 12px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.formInput_427795137 {\n font: inherit;\n font-size: 13px;\n border: 0.5px solid var(--dsw-alias-border-l4);\n border-radius: 8px;\n padding: 7px 10px;\n background: var(--dsw-alias-bg-layer-3);\n color: var(--dsw-alias-label-primary);\n outline: none;\n}\n\n.formInput_427795137:focus {\n border-color: var(--dsw-alias-brand-primary);\n}\n\n.formError_427795137 {\n font-size: 12px;\n color: var(--dsw-alias-status-danger, #d5304f);\n min-height: 16px;\n}\n\n/* Commit history */\n\n.historyHead_427795137 {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 8px 12px;\n border-bottom: 0.5px solid var(--dsw-alias-border-l2);\n}\n\n.searchBox_427795137 {\n margin-left: auto;\n display: flex;\n align-items: center;\n gap: 6px;\n flex: 1;\n max-width: 300px;\n min-width: 140px;\n border: 0.5px solid var(--dsw-alias-border-l4);\n border-radius: 7px;\n padding: 0 9px;\n height: 28px;\n color: var(--dsw-alias-label-tertiary);\n background: var(--dsw-alias-bg-layer-3);\n}\n\n.searchInput_427795137 {\n flex: 1;\n min-width: 0;\n border: 0;\n background: none;\n outline: none;\n font: inherit;\n font-size: 12px;\n color: var(--dsw-alias-label-primary);\n}\n\n.searchInput_427795137::placeholder {\n color: var(--dsw-alias-label-tertiary);\n}\n\n.commitRow_427795137 {\n position: relative;\n display: grid;\n grid-template-columns: 22px minmax(0, 1fr) auto;\n gap: 8px;\n align-items: center;\n padding: 6px 8px 6px 2px;\n cursor: pointer;\n border-radius: 7px;\n}\n\n.commitRow_427795137:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.commitRowOpen_427795137 {\n background: var(--dsw-alias-bg-layer-3);\n}\n\n.commitRowOpen_427795137:hover {\n background: var(--dsw-alias-bg-layer-3);\n}\n\n.commitGraph_427795137 {\n position: relative;\n align-self: stretch;\n min-height: 34px;\n}\n\n.commitGraph_427795137::before {\n content: \"\";\n position: absolute;\n left: 10px;\n top: 0;\n bottom: 0;\n width: 2px;\n background: var(--dsw-alias-border-l2);\n}\n\n.commitRowFirst_427795137 .commitGraph_427795137::before {\n top: 50%;\n}\n\n.commitRowLast_427795137 .commitGraph_427795137::before {\n bottom: 50%;\n}\n\n.commitDot_427795137 {\n position: absolute;\n left: 5px;\n top: 50%;\n transform: translateY(-50%);\n width: 10px;\n height: 10px;\n border-radius: 50%;\n background: var(--dsw-alias-brand-primary);\n border: 3px solid var(--dsw-alias-bg-layer-2);\n box-sizing: content-box;\n margin-left: -3px;\n}\n\n.commitDotMerge_427795137 {\n background: #8350d8;\n}\n\n.commitMain_427795137 {\n min-width: 0;\n display: flex;\n flex-direction: column;\n gap: 1px;\n}\n\n.commitSubject_427795137 {\n display: flex;\n align-items: center;\n gap: 6px;\n min-width: 0;\n font-size: 12.5px;\n color: var(--dsw-alias-label-primary);\n}\n\n.commitSubjectText_427795137 {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.commitRowOpen_427795137 .commitSubjectText_427795137 {\n font-weight: 600;\n}\n\n.refBadges_427795137 {\n display: inline-flex;\n gap: 4px;\n flex: none;\n}\n\n.refBadge_427795137 {\n font-size: 10px;\n line-height: 1;\n border-radius: 4px;\n padding: 3px 5px;\n font-weight: 500;\n white-space: nowrap;\n max-width: 140px;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.refHead_427795137 {\n background: var(--dsw-alias-bg-layer-3);\n color: var(--dsw-alias-label-primary-bluish);\n border: 0.5px solid var(--dsw-alias-border-l4);\n}\n\n.refTag_427795137 {\n background: var(--dsw-alias-bg-layer-3);\n color: var(--dsw-alias-label-warning, #b07000);\n border: 0.5px solid var(--dsw-alias-border-l4);\n}\n\n.refRemote_427795137 {\n background: var(--dsw-alias-bg-layer-3);\n color: #8350d8;\n border: 0.5px solid var(--dsw-alias-border-l4);\n}\n\n.refLocal_427795137 {\n background: var(--dsw-alias-bg-layer-3);\n color: var(--dsw-alias-status-success, #218358);\n border: 0.5px solid var(--dsw-alias-border-l4);\n}\n\n.commitMeta_427795137 {\n display: flex;\n align-items: center;\n gap: 6px;\n color: var(--dsw-alias-label-tertiary);\n font-size: 11.5px;\n}\n\n.authorDot_427795137 {\n width: 14px;\n height: 14px;\n border-radius: 50%;\n background: var(--dsw-alias-brand-primary);\n color: #fff;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n font-size: 9px;\n flex: none;\n}\n\n.commitSide_427795137 {\n display: flex;\n align-items: center;\n gap: 8px;\n color: var(--dsw-alias-label-tertiary);\n font-size: 11.5px;\n flex: none;\n}\n\n.commitHash_427795137 {\n font-family: ui-monospace, Consolas, monospace;\n}\n\n.commitExpand_427795137 {\n grid-column: 1 / -1;\n padding: 2px 4px 8px 28px;\n display: flex;\n flex-direction: column;\n gap: 2px;\n}\n\n.commitExpandTitle_427795137 {\n font-size: 11px;\n color: var(--dsw-alias-label-tertiary);\n margin-bottom: 3px;\n}\n\n.fileRow_427795137 {\n display: grid;\n grid-template-columns: 18px minmax(0, 1fr) auto;\n gap: 8px;\n align-items: center;\n padding: 3px 6px;\n border-radius: 6px;\n font-size: 12px;\n color: var(--dsw-alias-label-primary);\n}\n\n.fileRow_427795137:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.filePath_427795137 {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n font-family: ui-monospace, Consolas, monospace;\n font-size: 11.5px;\n direction: rtl;\n text-align: left;\n}\n\n.fileNums_427795137 {\n font-variant-numeric: tabular-nums;\n color: var(--dsw-alias-label-tertiary);\n flex: none;\n}\n\n.fileAdd_427795137 { color: var(--dsw-alias-status-success, #218358); }\n.fileDel_427795137 { color: var(--dsw-alias-status-danger, #d5304f); }\n\n.empty_427795137 {\n padding: 28px 16px;\n text-align: center;\n color: var(--dsw-alias-label-tertiary);\n font-size: 12.5px;\n}\n\n.errorText_427795137 {\n padding: 20px 16px;\n text-align: center;\n color: var(--dsw-alias-status-danger, #d5304f);\n font-size: 12.5px;\n}\n\n.moreRow_427795137 {\n display: flex;\n justify-content: center;\n padding: 8px 0 2px;\n}\n";
3526
+ const tagId$2 = "dsh-git-branch-manage/GitHistoryPanel.module.css";
3527
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=\"" + tagId$2 + "\"]") === null) {
3528
+ const tag = document.createElement("style");
3529
+ tag.dataset.plugin = "dsh-git-branch-manage";
3530
+ tag.dataset.pluginCss = tagId$2;
3531
+ tag.textContent = css$2;
3532
+ document.head.appendChild(tag);
3533
+ }
3534
+ var GitHistoryPanel_module_default = {
3535
+ "page": "page_427795137",
3536
+ "topbar": "topbar_427795137",
3537
+ "back": "back_427795137",
3538
+ "title": "title_427795137",
3539
+ "repoPill": "repoPill_427795137",
3540
+ "repoName": "repoName_427795137",
3541
+ "branchName": "branchName_427795137",
3542
+ "track": "track_427795137",
3543
+ "topbarActions": "topbarActions_427795137",
3544
+ "refreshBtn": "refreshBtn_427795137",
3545
+ "scroll": "scroll_427795137",
3546
+ "section": "section_427795137",
3547
+ "sectionHead": "sectionHead_427795137",
3548
+ "sectionTitle": "sectionTitle_427795137",
3549
+ "countBadge": "countBadge_427795137",
3550
+ "sectionTools": "sectionTools_427795137",
3551
+ "sectionBody": "sectionBody_427795137",
3552
+ "act": "act_427795137",
3553
+ "actPrimary": "actPrimary_427795137",
3554
+ "groupTitleRow": "groupTitleRow_427795137",
3555
+ "linkBtn": "linkBtn_427795137",
3556
+ "changeRow": "changeRow_427795137",
3557
+ "changeCheck": "changeCheck_427795137",
3558
+ "statusBadge": "statusBadge_427795137",
3559
+ "statusA": "statusA_427795137",
3560
+ "statusM": "statusM_427795137",
3561
+ "statusD": "statusD_427795137",
3562
+ "statusR": "statusR_427795137",
3563
+ "statusC": "statusC_427795137",
3564
+ "statusT": "statusT_427795137",
3565
+ "statusQ": "statusQ_427795137",
3566
+ "statusU": "statusU_427795137",
3567
+ "changePath": "changePath_427795137",
3568
+ "stageHint": "stageHint_427795137",
3569
+ "commitBox": "commitBox_427795137",
3570
+ "commitInput": "commitInput_427795137",
3571
+ "commitActions": "commitActions_427795137",
3572
+ "commitHint": "commitHint_427795137",
3573
+ "split": "split_427795137",
3574
+ "splitMain": "splitMain_427795137",
3575
+ "splitCaret": "splitCaret_427795137",
3576
+ "menu": "menu_427795137",
3577
+ "menuItem": "menuItem_427795137",
3578
+ "menuSub": "menuSub_427795137",
3579
+ "treeHead": "treeHead_427795137",
3580
+ "treeChevron": "treeChevron_427795137",
3581
+ "treeChildren": "treeChildren_427795137",
3582
+ "treeHeadNest": "treeHeadNest_427795137",
3583
+ "treeNest": "treeNest_427795137",
3584
+ "branchRow": "branchRow_427795137",
3585
+ "branchRowCurrent": "branchRowCurrent_427795137",
3586
+ "branchMark": "branchMark_427795137",
3587
+ "branchKind": "branchKind_427795137",
3588
+ "branchTrack": "branchTrack_427795137",
3589
+ "floatMenu": "floatMenu_427795137",
3590
+ "menuHead": "menuHead_427795137",
3591
+ "menuHeadStrong": "menuHeadStrong_427795137",
3592
+ "menuDanger": "menuDanger_427795137",
3593
+ "formField": "formField_427795137",
3594
+ "formLabel": "formLabel_427795137",
3595
+ "formInput": "formInput_427795137",
3596
+ "formError": "formError_427795137",
3597
+ "historyHead": "historyHead_427795137",
3598
+ "searchBox": "searchBox_427795137",
3599
+ "searchInput": "searchInput_427795137",
3600
+ "commitRow": "commitRow_427795137",
3601
+ "commitRowOpen": "commitRowOpen_427795137",
3602
+ "commitGraph": "commitGraph_427795137",
3603
+ "commitRowFirst": "commitRowFirst_427795137",
3604
+ "commitRowLast": "commitRowLast_427795137",
3605
+ "commitDot": "commitDot_427795137",
3606
+ "commitDotMerge": "commitDotMerge_427795137",
3607
+ "commitMain": "commitMain_427795137",
3608
+ "commitSubject": "commitSubject_427795137",
3609
+ "commitSubjectText": "commitSubjectText_427795137",
3610
+ "refBadges": "refBadges_427795137",
3611
+ "refBadge": "refBadge_427795137",
3612
+ "refHead": "refHead_427795137",
3613
+ "refTag": "refTag_427795137",
3614
+ "refRemote": "refRemote_427795137",
3615
+ "refLocal": "refLocal_427795137",
3616
+ "commitMeta": "commitMeta_427795137",
3617
+ "authorDot": "authorDot_427795137",
3618
+ "commitSide": "commitSide_427795137",
3619
+ "commitHash": "commitHash_427795137",
3620
+ "commitExpand": "commitExpand_427795137",
3621
+ "commitExpandTitle": "commitExpandTitle_427795137",
3622
+ "fileRow": "fileRow_427795137",
3623
+ "filePath": "filePath_427795137",
3624
+ "fileNums": "fileNums_427795137",
3625
+ "fileAdd": "fileAdd_427795137",
3626
+ "fileDel": "fileDel_427795137",
3627
+ "empty": "empty_427795137",
3628
+ "errorText": "errorText_427795137",
3629
+ "moreRow": "moreRow_427795137"
3630
+ };
3631
+ //#endregion
3632
+ //#region src/client/panel/BranchSection.tsx
3633
+ /**
3634
+ * BranchSection: the panel's middle block — the branch list with an IDEA-style
3635
+ * action menu. Clicking any row opens a small menu anchored to it (checkout,
3636
+ * new branch from here, rename, delete, copy name — remote rows narrow it to
3637
+ * the actions a remote branch can take); the CURRENT branch's menu hides only
3638
+ * checkout and delete, so rename/new-from/copy stay reachable. Rename and
3639
+ * create run through the Modal primitive with the same client-side name
3640
+ * validation the composer chip applies; delete confirms first and always uses
3641
+ * the safe `-d` form (git refuses unmerged). Presentational: every action
3642
+ * arrives as a callback; the menu/dialog state is local and resets when the
3643
+ * branch list identity changes.
3644
+ *
3645
+ * @module git-worktree/client/panel/BranchSection
3646
+ */
3647
+ /** The action menu as a body portal, fixed-positioned next to the row. */
3648
+ function BranchFloatMenu(props) {
3649
+ const { state, t, busy, onAction, onClose } = props;
3650
+ const { branch } = state;
3651
+ const ref = (0, react.useRef)(null);
3652
+ (0, react.useEffect)(() => {
3653
+ const onPointerDown = (event) => {
3654
+ if (ref.current !== null && !ref.current.contains(event.target)) onClose();
3655
+ };
3656
+ const onKeyDown = (event) => {
3657
+ if (event.key === "Escape") onClose();
3658
+ };
3659
+ document.addEventListener("pointerdown", onPointerDown, true);
3660
+ document.addEventListener("keydown", onKeyDown);
3661
+ return () => {
3662
+ document.removeEventListener("pointerdown", onPointerDown, true);
3663
+ document.removeEventListener("keydown", onKeyDown);
3664
+ };
3665
+ }, [onClose]);
3666
+ (0, react.useLayoutEffect)(() => {
3667
+ const el = ref.current;
3668
+ if (el === null) return;
3669
+ const rect = el.getBoundingClientRect();
3670
+ const left = Math.max(8, Math.min(state.rect.left, window.innerWidth - rect.width - 8));
3671
+ let top = state.rect.top;
3672
+ if (top + rect.height > window.innerHeight - 8) top = Math.max(8, state.rect.top - rect.height - 8);
3673
+ el.style.left = `${String(Math.round(left))}px`;
3674
+ el.style.top = `${String(Math.round(top))}px`;
3675
+ }, [state]);
3676
+ return (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3677
+ ref,
3678
+ className: `${GitHistoryPanel_module_default.menu} ${GitHistoryPanel_module_default.floatMenu}`,
3679
+ role: "menu",
3680
+ children: [
3681
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3682
+ className: GitHistoryPanel_module_default.menuHead,
3683
+ children: [
3684
+ t("panel.branches"),
3685
+ " · ",
3686
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3687
+ className: GitHistoryPanel_module_default.menuHeadStrong,
3688
+ children: branch.name
3689
+ }),
3690
+ branch.current ? ` · ${t("panel.branchCurrent")}` : ""
3691
+ ]
3692
+ }),
3693
+ !branch.current ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3694
+ type: "button",
3695
+ role: "menuitem",
3696
+ className: GitHistoryPanel_module_default.menuItem,
3697
+ disabled: busy,
3698
+ onClick: () => onAction("checkout"),
3699
+ children: branch.kind === "remote" ? t("panel.branchCheckoutRemote") : t("panel.branchCheckout")
3700
+ }) : null,
3701
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3702
+ type: "button",
3703
+ role: "menuitem",
3704
+ className: GitHistoryPanel_module_default.menuItem,
3705
+ disabled: busy,
3706
+ onClick: () => onAction("new-from"),
3707
+ children: t("panel.branchNewFrom")
3708
+ }),
3709
+ branch.kind === "local" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3710
+ type: "button",
3711
+ role: "menuitem",
3712
+ className: GitHistoryPanel_module_default.menuItem,
3713
+ disabled: busy,
3714
+ onClick: () => onAction("rename"),
3715
+ children: t("panel.branchRename")
3716
+ }) : null,
3717
+ branch.kind === "local" && !branch.current ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3718
+ type: "button",
3719
+ role: "menuitem",
3720
+ className: `${GitHistoryPanel_module_default.menuItem} ${GitHistoryPanel_module_default.menuDanger}`,
3721
+ disabled: busy,
3722
+ onClick: () => onAction("delete"),
3723
+ children: t("panel.branchDelete")
3724
+ }) : null,
3725
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3726
+ type: "button",
3727
+ role: "menuitem",
3728
+ className: GitHistoryPanel_module_default.menuItem,
3729
+ disabled: busy,
3730
+ onClick: () => onAction("copy"),
3731
+ children: t("panel.branchCopy")
3732
+ })
3733
+ ]
3734
+ }), document.body);
3735
+ }
3736
+ /** The branches section. */
3737
+ function BranchSection(props) {
3738
+ const { t, locals, remotes, busy, onCheckout, onRename, onDelete, onCreate } = props;
3739
+ const [menu, setMenu] = (0, react.useState)(null);
3740
+ const [dialog, setDialog] = (0, react.useState)(null);
3741
+ const [draft, setDraft] = (0, react.useState)("");
3742
+ const [collapsedGroups, setCollapsedGroups] = (0, react.useState)(/* @__PURE__ */ new Set());
3743
+ const toggleGroup = (key) => {
3744
+ setCollapsedGroups((prev) => {
3745
+ const next = new Set(prev);
3746
+ if (next.has(key)) next.delete(key);
3747
+ else next.add(key);
3748
+ return next;
3749
+ });
3750
+ };
3751
+ const total = locals.length + remotes.length;
3752
+ const openMenu = (branch, event) => {
3753
+ const rect = event.currentTarget.getBoundingClientRect();
3754
+ setMenu({
3755
+ branch,
3756
+ rect: {
3757
+ left: rect.left,
3758
+ top: rect.bottom + 5
3759
+ }
3760
+ });
3761
+ };
3762
+ const openDialog = (kind, branch) => {
3763
+ setMenu(null);
3764
+ setDraft(kind === "rename" ? branch?.name ?? "" : "");
3765
+ setDialog({
3766
+ kind,
3767
+ ...branch === void 0 ? {} : { branch }
3768
+ });
3769
+ };
3770
+ const menuAction = (action) => {
3771
+ const state = menu;
3772
+ if (state === null) return;
3773
+ if (action === "checkout") {
3774
+ setMenu(null);
3775
+ onCheckout(state.branch);
3776
+ return;
3777
+ }
3778
+ if (action === "copy") {
3779
+ setMenu(null);
3780
+ navigator.clipboard.writeText(state.branch.name).catch(() => {});
3781
+ return;
3782
+ }
3783
+ openDialog(action, state.branch);
3784
+ };
3785
+ const draftIssue = dialog !== null && dialog.kind !== "delete" ? branchNameIssue(draft) : void 0;
3786
+ const duplicateIssue = dialog !== null && dialog.kind !== "delete" && draftIssue === void 0 && [...locals, ...remotes].some((row) => row.kind === "local" && row.name === draft) ? t("menuNewBranchExists") : void 0;
3787
+ const inputError = draftIssue ?? duplicateIssue;
3788
+ const submitDialog = () => {
3789
+ if (dialog === null || busy) return;
3790
+ if (dialog.kind === "delete") {
3791
+ if (dialog.branch !== void 0) onDelete(dialog.branch);
3792
+ setDialog(null);
3793
+ return;
3794
+ }
3795
+ if (draft === "" || inputError !== void 0) return;
3796
+ if (dialog.kind === "rename" && dialog.branch !== void 0) onRename(dialog.branch, draft);
3797
+ else if (dialog.kind === "new") onCreate(draft);
3798
+ else if (dialog.kind === "new-from" && dialog.branch !== void 0) onCreate(draft, dialog.branch.name);
3799
+ setDialog(null);
3800
+ };
3801
+ const dialogTitle = dialog === null ? "" : dialog.kind === "rename" && dialog.branch !== void 0 ? t("renameBranchTitle", { branch: dialog.branch.name }) : dialog.kind === "new-from" && dialog.branch !== void 0 ? t("createFlyAskFrom", { branch: dialog.branch.name }) : t("createFlyAsk");
3802
+ const groupHead = (key, label, count, nested) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3803
+ className: `${GitHistoryPanel_module_default.treeHead} ${nested ? GitHistoryPanel_module_default.treeHeadNest : ""}`,
3804
+ role: "button",
3805
+ tabIndex: 0,
3806
+ "aria-expanded": !collapsedGroups.has(key),
3807
+ onClick: () => toggleGroup(key),
3808
+ onKeyDown: (event) => {
3809
+ if (event.key === "Enter" || event.key === " ") {
3810
+ event.preventDefault();
3811
+ toggleGroup(key);
3812
+ }
3813
+ },
3814
+ children: [
3815
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3816
+ className: GitHistoryPanel_module_default.treeChevron,
3817
+ children: collapsedGroups.has(key) ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronRightOutlineRegular, { size: 12 }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, { size: 12 })
3818
+ }),
3819
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: label }),
3820
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3821
+ className: GitHistoryPanel_module_default.countBadge,
3822
+ children: String(count)
3823
+ })
3824
+ ]
3825
+ });
3826
+ const renderBranchRow = (row, displayName) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3827
+ className: `${GitHistoryPanel_module_default.branchRow} ${row.current ? GitHistoryPanel_module_default.branchRowCurrent : ""}`,
3828
+ role: "button",
3829
+ tabIndex: 0,
3830
+ onClick: (event) => openMenu(row, event),
3831
+ onKeyDown: (event) => {
3832
+ if (event.key === "Enter" || event.key === " ") {
3833
+ event.preventDefault();
3834
+ openMenu(row, event);
3835
+ }
3836
+ },
3837
+ children: [
3838
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3839
+ className: GitHistoryPanel_module_default.branchMark,
3840
+ children: row.current ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCheckOutlineRegular, { size: 13 }) : "⎇"
3841
+ }),
3842
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3843
+ className: GitHistoryPanel_module_default.branchName,
3844
+ title: row.name,
3845
+ children: displayName ?? row.name
3846
+ }),
3847
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3848
+ className: GitHistoryPanel_module_default.branchTrack,
3849
+ children: row.ahead !== void 0 || row.behind !== void 0 ? `↓${String(row.behind ?? 0)} ↑${String(row.ahead ?? 0)}` : ""
3850
+ })
3851
+ ]
3852
+ }, row.name);
3853
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
3854
+ className: GitHistoryPanel_module_default.section,
3855
+ "aria-label": t("panel.branches"),
3856
+ children: [
3857
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3858
+ className: GitHistoryPanel_module_default.sectionHead,
3859
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
3860
+ className: GitHistoryPanel_module_default.sectionTitle,
3861
+ children: t("panel.branches")
3862
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3863
+ className: GitHistoryPanel_module_default.sectionTools,
3864
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
3865
+ type: "button",
3866
+ className: GitHistoryPanel_module_default.act,
3867
+ disabled: busy,
3868
+ onClick: () => openDialog("new"),
3869
+ children: ["+ ", t("panel.branchNew")]
3870
+ })
3871
+ })]
3872
+ }),
3873
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3874
+ className: GitHistoryPanel_module_default.sectionBody,
3875
+ children: [
3876
+ total === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3877
+ className: GitHistoryPanel_module_default.empty,
3878
+ children: t("menuNoBranches")
3879
+ }) : null,
3880
+ locals.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [groupHead("local", t("panel.branchLocals"), locals.length, false), !collapsedGroups.has("local") ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3881
+ className: GitHistoryPanel_module_default.treeChildren,
3882
+ children: locals.map((row) => renderBranchRow(row))
3883
+ }) : null] }) : null,
3884
+ remotes.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [groupHead("remote", t("panel.branchRemotes"), remotes.length, false), !collapsedGroups.has("remote") ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3885
+ className: GitHistoryPanel_module_default.treeNest,
3886
+ children: groupRemotes(remotes).map((group) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [groupHead(`remote:${group.remote}`, group.remote, group.branches.length, true), !collapsedGroups.has(`remote:${group.remote}`) ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3887
+ className: GitHistoryPanel_module_default.treeChildren,
3888
+ children: group.branches.map((row) => renderBranchRow(row, row.short))
3889
+ }) : null] }, group.remote))
3890
+ }) : null] }) : null
3891
+ ]
3892
+ }),
3893
+ menu !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BranchFloatMenu, {
3894
+ state: menu,
3895
+ t,
3896
+ busy,
3897
+ onAction: menuAction,
3898
+ onClose: () => setMenu(null)
3899
+ }) : null,
3900
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
3901
+ open: dialog !== null && dialog.kind !== "delete",
3902
+ onClose: () => setDialog(null),
3903
+ title: dialogTitle,
3904
+ closeLabel: t("close"),
3905
+ footer: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
3906
+ size: "sm",
3907
+ variant: "ghost",
3908
+ onClick: () => setDialog(null),
3909
+ children: t("cancel")
3910
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
3911
+ size: "sm",
3912
+ variant: "primary",
3913
+ disabled: busy || draft === "" || inputError !== void 0,
3914
+ onClick: submitDialog,
3915
+ children: t("actionConfirm")
3916
+ })] }),
3917
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3918
+ className: GitHistoryPanel_module_default.formField,
3919
+ children: [
3920
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
3921
+ className: GitHistoryPanel_module_default.formLabel,
3922
+ htmlFor: "git-panel-branch-name",
3923
+ children: t("menuNewBranchPlaceholder")
3924
+ }),
3925
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
3926
+ id: "git-panel-branch-name",
3927
+ className: GitHistoryPanel_module_default.formInput,
3928
+ value: draft,
3929
+ "data-modal-autofocus": true,
3930
+ onChange: (event) => setDraft(event.target.value),
3931
+ onKeyDown: (event) => {
3932
+ if (event.key === "Enter") submitDialog();
3933
+ }
3934
+ }),
3935
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3936
+ className: GitHistoryPanel_module_default.formError,
3937
+ children: inputError ?? ""
3938
+ })
3939
+ ]
3940
+ })
3941
+ }),
3942
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
3943
+ open: dialog !== null && dialog.kind === "delete" && dialog.branch !== void 0,
3944
+ onClose: () => setDialog(null),
3945
+ title: t("ctxDelete"),
3946
+ closeLabel: t("close"),
3947
+ footer: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
3948
+ size: "sm",
3949
+ variant: "ghost",
3950
+ onClick: () => setDialog(null),
3951
+ children: t("cancel")
3952
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
3953
+ size: "sm",
3954
+ variant: "primary",
3955
+ disabled: busy,
3956
+ onClick: submitDialog,
3957
+ children: t("actionConfirm")
3958
+ })] }),
3959
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3960
+ className: GitHistoryPanel_module_default.formLabel,
3961
+ children: dialog?.branch !== void 0 ? t("deleteBranchAsk", { branch: dialog.branch.name }) : ""
3962
+ })
3963
+ })
3964
+ ]
3965
+ });
3966
+ }
3967
+ //#endregion
3968
+ //#region src/client/panel/HistorySection.tsx
3969
+ /**
3970
+ * HistorySection: the panel's third block — the commit list. One row shows
3971
+ * the subject, its refs badges, the author and the relative time, and the
3972
+ * file count with the short hash; hovering a row raises the framework
3973
+ * Tooltip (subject · author <email> · absolute time · hash); CLICKING a row
3974
+ * expands the commit's changed files INLINE below it (exactly one open at a
3975
+ * time), loading them on first open. The search input filters the loaded
3976
+ * page client-side; "load more" fetches the next page.
3977
+ *
3978
+ * @module git-worktree/client/panel/HistorySection
3979
+ */
3980
+ function statusTitle$1(status, t) {
3981
+ if (status === "A") return t("panel.stAdd");
3982
+ if (status === "D") return t("panel.stDel");
3983
+ if (status === "R") return t("panel.stRen");
3984
+ if (status === "C") return t("panel.stCopy");
3985
+ if (status === "T") return t("panel.stType");
3986
+ return t("panel.stMod");
3987
+ }
3988
+ function FileRow(props) {
3989
+ const { file, t } = props;
3990
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3991
+ className: GitHistoryPanel_module_default.fileRow,
3992
+ children: [
3993
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3994
+ className: `${GitHistoryPanel_module_default.statusBadge} ${GitHistoryPanel_module_default[`status${file.status}`]}`,
3995
+ title: statusTitle$1(file.status, t),
3996
+ children: file.status
3997
+ }),
3998
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3999
+ className: GitHistoryPanel_module_default.filePath,
4000
+ title: file.oldPath === void 0 ? file.path : `${file.oldPath} → ${file.path}`,
4001
+ children: file.oldPath === void 0 ? file.path : `${file.oldPath} → ${file.path}`
4002
+ }),
4003
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4004
+ className: GitHistoryPanel_module_default.fileNums,
4005
+ children: file.binary === true ? t("panel.binary") : file.insertions !== void 0 || file.deletions !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
4006
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("b", {
4007
+ className: GitHistoryPanel_module_default.fileAdd,
4008
+ children: `+${String(file.insertions ?? 0)}`
4009
+ }),
4010
+ " ",
4011
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("b", {
4012
+ className: GitHistoryPanel_module_default.fileDel,
4013
+ children: `-${String(file.deletions ?? 0)}`
4014
+ })
4015
+ ] }) : ""
4016
+ })
4017
+ ]
4018
+ });
4019
+ }
4020
+ /** One history row; `ref` and the tooltip's hover handlers arrive through
4021
+ * the anchor share when the row is wrapped in a Tooltip. */
4022
+ const CommitRow = (0, react.forwardRef)(function CommitRow(props, ref) {
4023
+ const { commit, first, last, open, files, t, onToggle, timeLabelFor } = props;
4024
+ const badges = classifyRefs(commit.refs);
4025
+ const when = new Date(commit.date);
4026
+ const absolute = Number.isFinite(commit.date) && commit.date > 0 ? `${when.toLocaleDateString()} ${when.toLocaleTimeString()}` : "";
4027
+ const authorInitial = commit.authorName.slice(0, 1);
4028
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4029
+ ref,
4030
+ className: [
4031
+ GitHistoryPanel_module_default.commitRow,
4032
+ first ? GitHistoryPanel_module_default.commitRowFirst : "",
4033
+ last ? GitHistoryPanel_module_default.commitRowLast : "",
4034
+ open ? GitHistoryPanel_module_default.commitRowOpen : ""
4035
+ ].filter((name) => name !== "").join(" "),
4036
+ role: "button",
4037
+ tabIndex: 0,
4038
+ "aria-expanded": open,
4039
+ "aria-describedby": props["aria-describedby"],
4040
+ onMouseEnter: props.onMouseEnter,
4041
+ onMouseLeave: props.onMouseLeave,
4042
+ onFocus: props.onFocus,
4043
+ onBlur: props.onBlur,
4044
+ onClick: () => onToggle(commit.hash),
4045
+ onKeyDown: (event) => {
4046
+ if (event.key === "Enter" || event.key === " ") {
4047
+ event.preventDefault();
4048
+ onToggle(commit.hash);
4049
+ }
4050
+ },
4051
+ children: [
4052
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4053
+ className: GitHistoryPanel_module_default.commitGraph,
4054
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: `${GitHistoryPanel_module_default.commitDot} ${commit.parents.length > 1 ? GitHistoryPanel_module_default.commitDotMerge : ""}` })
4055
+ }),
4056
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4057
+ className: GitHistoryPanel_module_default.commitMain,
4058
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4059
+ className: GitHistoryPanel_module_default.commitSubject,
4060
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4061
+ className: GitHistoryPanel_module_default.commitSubjectText,
4062
+ children: commit.subject
4063
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4064
+ className: GitHistoryPanel_module_default.refBadges,
4065
+ children: badges.map((badge) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4066
+ className: `${GitHistoryPanel_module_default.refBadge} ${GitHistoryPanel_module_default[`ref${badge.kind[0]?.toUpperCase() ?? ""}${badge.kind.slice(1)}`]}`,
4067
+ children: badge.text
4068
+ }, `${badge.kind}:${badge.text}`))
4069
+ })]
4070
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4071
+ className: GitHistoryPanel_module_default.commitMeta,
4072
+ children: [
4073
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4074
+ className: GitHistoryPanel_module_default.authorDot,
4075
+ "aria-hidden": "true",
4076
+ children: authorInitial
4077
+ }),
4078
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: commit.authorName }),
4079
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: timeLabelFor(commit.date) })
4080
+ ]
4081
+ })]
4082
+ }),
4083
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4084
+ className: GitHistoryPanel_module_default.commitSide,
4085
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4086
+ className: GitHistoryPanel_module_default.commitHash,
4087
+ children: commit.short
4088
+ })
4089
+ }),
4090
+ open ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4091
+ className: GitHistoryPanel_module_default.commitExpand,
4092
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4093
+ className: GitHistoryPanel_module_default.commitExpandTitle,
4094
+ children: t("panel.filesTitle", {
4095
+ n: files?.kind === "loaded" ? files.files.length : 0,
4096
+ author: commit.authorName,
4097
+ date: absolute
4098
+ })
4099
+ }), files === void 0 || files.kind === "loading" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4100
+ className: GitHistoryPanel_module_default.commitExpandTitle,
4101
+ children: t("manager.loading")
4102
+ }) : files.kind === "failed" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4103
+ className: GitHistoryPanel_module_default.errorText,
4104
+ children: files.message
4105
+ }) : files.files.map((file) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FileRow, {
4106
+ file,
4107
+ t
4108
+ }, `${file.status}:${file.path}`))]
4109
+ }) : null
4110
+ ]
4111
+ });
4112
+ });
4113
+ /** Tooltip label resolver: one line spelling subject, author, time, hash. */
4114
+ function tooltipLabel(commit) {
4115
+ const when = new Date(commit.date);
4116
+ const absolute = Number.isFinite(commit.date) && commit.date > 0 ? `${when.toLocaleDateString()} ${when.toLocaleTimeString()}` : "";
4117
+ return `${commit.subject} · ${commit.authorName} <${commit.authorEmail}> · ${absolute} · ${commit.short}`;
4118
+ }
4119
+ function HistorySection(props) {
4120
+ const { t, commits, loaded, loadError, loading, hasMore, loadingMore, expanded, filesByHash, onLoadMore, onToggle, timeLabelFor } = props;
4121
+ const [query, setQuery] = (0, react.useState)("");
4122
+ const needle = query.trim().toLowerCase();
4123
+ const visible = filterCommits(commits, query);
4124
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
4125
+ className: GitHistoryPanel_module_default.section,
4126
+ "aria-label": t("panel.history"),
4127
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4128
+ className: GitHistoryPanel_module_default.historyHead,
4129
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
4130
+ className: GitHistoryPanel_module_default.sectionTitle,
4131
+ children: t("panel.history")
4132
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4133
+ className: GitHistoryPanel_module_default.searchBox,
4134
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4135
+ "aria-hidden": "true",
4136
+ children: "⌕"
4137
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
4138
+ type: "text",
4139
+ className: GitHistoryPanel_module_default.searchInput,
4140
+ value: query,
4141
+ placeholder: t("panel.historySearch"),
4142
+ "aria-label": t("panel.historySearch"),
4143
+ onChange: (event) => setQuery(event.target.value)
4144
+ })]
4145
+ })]
4146
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4147
+ className: GitHistoryPanel_module_default.sectionBody,
4148
+ children: [loadError !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4149
+ className: GitHistoryPanel_module_default.errorText,
4150
+ children: loadError
4151
+ }) : !loaded && loading ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4152
+ className: GitHistoryPanel_module_default.empty,
4153
+ children: t("manager.loading")
4154
+ }) : visible.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4155
+ className: GitHistoryPanel_module_default.empty,
4156
+ children: needle === "" ? t("panel.noCommits") : t("panel.noMatches")
4157
+ }) : visible.map((commit, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
4158
+ label: () => tooltipLabel(commit),
4159
+ side: "bottom",
4160
+ portal: true,
4161
+ maxWidth: 380,
4162
+ delayMs: 250,
4163
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CommitRow, {
4164
+ commit,
4165
+ first: index === 0,
4166
+ last: index === visible.length - 1,
4167
+ open: expanded === commit.hash,
4168
+ files: filesByHash[commit.hash],
4169
+ t,
4170
+ onToggle,
4171
+ timeLabelFor
4172
+ })
4173
+ }, commit.hash)), hasMore && visible.length === commits.length ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4174
+ className: GitHistoryPanel_module_default.moreRow,
4175
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4176
+ type: "button",
4177
+ className: GitHistoryPanel_module_default.act,
4178
+ disabled: loadingMore,
4179
+ onClick: onLoadMore,
4180
+ children: t("panel.loadMore")
4181
+ })
4182
+ }) : null]
4183
+ })]
4184
+ });
4185
+ }
4186
+ //#endregion
4187
+ //#region src/client/panel/UncommittedSection.tsx
4188
+ /**
4189
+ * UncommittedSection: the panel's first block — the staged/unstaged file
4190
+ * groups, the commit box, and the sync tools. Presentational only: the rows,
4191
+ * the draft, and every action arrive as props from the panel state owner, so
4192
+ * this file renders and never talks to git itself.
4193
+ *
4194
+ * The commit affordance is a split button: the main face commits, the caret
4195
+ * opens the two op variants (commit; commit and push). The AI button fills
4196
+ * the draft from the host's one-shot llm route — it renders only when the
4197
+ * panel knows the session's model selection.
4198
+ *
4199
+ * @module git-worktree/client/panel/UncommittedSection
4200
+ */
4201
+ /** Status letter → its localized title. */
4202
+ function statusTitle(status, t) {
4203
+ if (status === "?") return t("panel.stUntrack");
4204
+ if (status === "U") return t("panel.stUnmerged");
4205
+ if (status === "A") return t("panel.stAdd");
4206
+ if (status === "D") return t("panel.stDel");
4207
+ if (status === "R") return t("panel.stRen");
4208
+ if (status === "C") return t("panel.stCopy");
4209
+ if (status === "T") return t("panel.stType");
4210
+ return t("panel.stMod");
4211
+ }
4212
+ /** One file row: checkbox = stage/unstage the path. */
4213
+ function ChangeRowView(props) {
4214
+ const { row, disabled, onToggle, t } = props;
4215
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
4216
+ className: GitHistoryPanel_module_default.changeRow,
4217
+ children: [
4218
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
4219
+ type: "checkbox",
4220
+ className: GitHistoryPanel_module_default.changeCheck,
4221
+ checked: row.staged,
4222
+ disabled,
4223
+ onChange: () => onToggle(row)
4224
+ }),
4225
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4226
+ className: `${GitHistoryPanel_module_default.statusBadge} ${GitHistoryPanel_module_default[`status${row.status === "?" ? "Q" : row.status}`]}`,
4227
+ title: statusTitle(row.status, t),
4228
+ children: row.status === "?" ? "?" : row.status
4229
+ }),
4230
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4231
+ className: GitHistoryPanel_module_default.changePath,
4232
+ title: row.oldPath === void 0 ? row.path : `${row.oldPath} → ${row.path}`,
4233
+ children: row.oldPath === void 0 ? row.path : `${row.oldPath} → ${row.path}`
4234
+ }),
4235
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4236
+ className: GitHistoryPanel_module_default.stageHint,
4237
+ children: row.staged ? t("panel.unstageHint") : t("panel.stageHint")
4238
+ })
4239
+ ]
4240
+ });
4241
+ }
4242
+ /** The uncommitted-changes section body. */
4243
+ function UncommittedSection(props) {
4244
+ const { t, staged, unstaged, busy, syncBusy, truncated, commitDraft, onCommitDraft, commitMenuOpen, onToggleCommitMenu, onCommit, onToggleRow, onStageAll, onUnstageAll, aiAvailable, aiBusy, onAiGenerate, onFetch, onUpdate, onPush } = props;
4245
+ const menuRef = (0, react.useRef)(null);
4246
+ (0, react.useEffect)(() => {
4247
+ if (!commitMenuOpen) return;
4248
+ const onPointerDown = (event) => {
4249
+ if (menuRef.current !== null && !menuRef.current.contains(event.target)) onToggleCommitMenu();
4250
+ };
4251
+ const onKeyDown = (event) => {
4252
+ if (event.key === "Escape") onToggleCommitMenu();
4253
+ };
4254
+ document.addEventListener("pointerdown", onPointerDown, true);
4255
+ document.addEventListener("keydown", onKeyDown);
4256
+ return () => {
4257
+ document.removeEventListener("pointerdown", onPointerDown, true);
4258
+ document.removeEventListener("keydown", onKeyDown);
4259
+ };
4260
+ }, [commitMenuOpen, onToggleCommitMenu]);
4261
+ const total = staged.length + unstaged.length;
4262
+ const canCommit = staged.length > 0 && commitDraft.trim() !== "";
4263
+ const hint = staged.length === 0 ? t("panel.commitNeedsStaged") : commitDraft.trim() === "" ? t("panel.commitNeedsMessage") : "";
4264
+ const aiReady = aiAvailable && total > 0;
4265
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
4266
+ className: GitHistoryPanel_module_default.section,
4267
+ "aria-label": t("panel.uncommitted"),
4268
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4269
+ className: GitHistoryPanel_module_default.sectionHead,
4270
+ children: [
4271
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
4272
+ className: GitHistoryPanel_module_default.sectionTitle,
4273
+ children: t("panel.uncommitted")
4274
+ }),
4275
+ total > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4276
+ className: GitHistoryPanel_module_default.countBadge,
4277
+ children: String(total)
4278
+ }) : null,
4279
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4280
+ className: GitHistoryPanel_module_default.sectionTools,
4281
+ children: [
4282
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
4283
+ type: "button",
4284
+ className: GitHistoryPanel_module_default.act,
4285
+ disabled: syncBusy,
4286
+ onClick: onFetch,
4287
+ title: t("menuFetch"),
4288
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconDownloadOutlineMedium, { size: 13 }), t("menuFetch")]
4289
+ }),
4290
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
4291
+ type: "button",
4292
+ className: GitHistoryPanel_module_default.act,
4293
+ disabled: syncBusy,
4294
+ onClick: onUpdate,
4295
+ title: t("menuUpdate"),
4296
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconDownloadOutlineMedium, { size: 13 }), t("menuUpdate")]
4297
+ }),
4298
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
4299
+ type: "button",
4300
+ className: GitHistoryPanel_module_default.act,
4301
+ disabled: syncBusy,
4302
+ onClick: onPush,
4303
+ title: t("menuPush"),
4304
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconPaperPlaneOutlineMedium, { size: 13 }), t("menuPush")]
4305
+ })
4306
+ ]
4307
+ })
4308
+ ]
4309
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4310
+ className: GitHistoryPanel_module_default.sectionBody,
4311
+ children: [
4312
+ total === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
4313
+ className: GitHistoryPanel_module_default.empty,
4314
+ children: t("panel.clean")
4315
+ }) : null,
4316
+ truncated ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
4317
+ className: GitHistoryPanel_module_default.empty,
4318
+ children: t("panel.changesTruncated")
4319
+ }) : null,
4320
+ staged.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4321
+ className: GitHistoryPanel_module_default.groupTitleRow,
4322
+ children: [
4323
+ t("panel.staged"),
4324
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4325
+ className: GitHistoryPanel_module_default.countBadge,
4326
+ children: String(staged.length)
4327
+ }),
4328
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4329
+ type: "button",
4330
+ className: GitHistoryPanel_module_default.linkBtn,
4331
+ disabled: busy,
4332
+ onClick: onUnstageAll,
4333
+ children: t("panel.unstageAll")
4334
+ })
4335
+ ]
4336
+ }), staged.map((row) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChangeRowView, {
4337
+ row,
4338
+ disabled: busy,
4339
+ onToggle: onToggleRow,
4340
+ t
4341
+ }, `${row.staged}:${row.path}:${row.oldPath ?? ""}`))] }) : null,
4342
+ unstaged.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4343
+ className: GitHistoryPanel_module_default.groupTitleRow,
4344
+ children: [
4345
+ t("panel.unstaged"),
4346
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4347
+ className: GitHistoryPanel_module_default.countBadge,
4348
+ children: String(unstaged.length)
4349
+ }),
4350
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4351
+ type: "button",
4352
+ className: GitHistoryPanel_module_default.linkBtn,
4353
+ disabled: busy,
4354
+ onClick: onStageAll,
4355
+ children: t("panel.stageAll")
4356
+ })
4357
+ ]
4358
+ }), unstaged.map((row) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChangeRowView, {
4359
+ row,
4360
+ disabled: busy,
4361
+ onToggle: onToggleRow,
4362
+ t
4363
+ }, `u:${row.path}:${row.oldPath ?? ""}`))] }) : null,
4364
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4365
+ className: GitHistoryPanel_module_default.commitBox,
4366
+ children: [
4367
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
4368
+ className: GitHistoryPanel_module_default.commitInput,
4369
+ value: commitDraft,
4370
+ placeholder: t("panel.commitPlaceholder"),
4371
+ "aria-label": t("panel.commitPlaceholder"),
4372
+ rows: 2,
4373
+ maxLength: COMMIT_MESSAGE_LIMIT,
4374
+ onChange: (event) => onCommitDraft(event.target.value)
4375
+ }),
4376
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4377
+ className: GitHistoryPanel_module_default.commitActions,
4378
+ children: [aiAvailable ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
4379
+ type: "button",
4380
+ className: GitHistoryPanel_module_default.act,
4381
+ disabled: busy || aiBusy || !aiReady,
4382
+ title: !aiReady ? t("panel.aiNeedsChanges") : void 0,
4383
+ onClick: onAiGenerate,
4384
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconSparkleRegular, { size: 13 }), aiBusy ? t("panel.aiBusy") : t("panel.aiGenerate")]
4385
+ }) : null, /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4386
+ className: GitHistoryPanel_module_default.split,
4387
+ ref: menuRef,
4388
+ children: [
4389
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4390
+ type: "button",
4391
+ className: `${GitHistoryPanel_module_default.act} ${GitHistoryPanel_module_default.actPrimary} ${GitHistoryPanel_module_default.splitMain}`,
4392
+ disabled: !canCommit || busy,
4393
+ onClick: () => onCommit(false),
4394
+ children: busy ? t("panel.commitBusy") : t("panel.commit")
4395
+ }),
4396
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4397
+ type: "button",
4398
+ className: `${GitHistoryPanel_module_default.act} ${GitHistoryPanel_module_default.actPrimary} ${GitHistoryPanel_module_default.splitCaret}`,
4399
+ "aria-label": t("panel.commitMoreAria"),
4400
+ "aria-expanded": commitMenuOpen,
4401
+ disabled: !canCommit || busy,
4402
+ onClick: onToggleCommitMenu,
4403
+ children: "▾"
4404
+ }),
4405
+ commitMenuOpen ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4406
+ className: GitHistoryPanel_module_default.menu,
4407
+ role: "menu",
4408
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4409
+ type: "button",
4410
+ role: "menuitem",
4411
+ className: GitHistoryPanel_module_default.menuItem,
4412
+ disabled: !canCommit,
4413
+ onClick: () => onCommit(false),
4414
+ children: t("panel.commit")
4415
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4416
+ type: "button",
4417
+ role: "menuitem",
4418
+ className: GitHistoryPanel_module_default.menuItem,
4419
+ disabled: !canCommit,
4420
+ onClick: () => onCommit(true),
4421
+ children: t("panel.commitPush")
4422
+ })]
4423
+ }) : null
4424
+ ]
4425
+ })]
4426
+ }),
4427
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4428
+ className: GitHistoryPanel_module_default.commitHint,
4429
+ children: hint
4430
+ })
4431
+ ]
4432
+ })
4433
+ ]
4434
+ })]
4435
+ });
4436
+ }
4437
+ //#endregion
4438
+ //#region src/client/relative-time.ts
4439
+ /**
4440
+ * Compact relative time for trailing labels, as a structured bucket the
4441
+ * renderer localizes ("now"/"5min"/"3h"/"2d"/"4mo"/"1y" in en).
4442
+ */
4443
+ function relativeTime(updatedAt, now) {
4444
+ const MIN = 6e4;
4445
+ const HOUR = 36e5;
4446
+ const DAY = 864e5;
4447
+ const diff = Math.max(0, now - updatedAt);
4448
+ if (diff < MIN) return {
4449
+ unit: "now",
4450
+ n: 0
4451
+ };
4452
+ if (diff < HOUR) return {
4453
+ unit: "minutes",
4454
+ n: Math.floor(diff / MIN)
4455
+ };
4456
+ if (diff < DAY) return {
4457
+ unit: "hours",
4458
+ n: Math.floor(diff / HOUR)
4459
+ };
4460
+ if (diff < 30 * DAY) return {
4461
+ unit: "days",
4462
+ n: Math.floor(diff / DAY)
4463
+ };
4464
+ if (diff < 365 * DAY) return {
4465
+ unit: "months",
4466
+ n: Math.floor(diff / (30 * DAY))
4467
+ };
4468
+ return {
4469
+ unit: "years",
4470
+ n: Math.floor(diff / (365 * DAY))
4471
+ };
4472
+ }
4473
+ /** Localized compact relative time ("刚刚"/"5分钟" in zh, "now"/"5min" in en). */
4474
+ function timeLabel(updatedAt, now, t) {
4475
+ const { unit, n } = relativeTime(updatedAt, now);
4476
+ if (unit === "now") return t("time.now");
4477
+ if (unit === "minutes") return t("time.minutes", { n });
4478
+ if (unit === "hours") return t("time.hours", { n });
4479
+ if (unit === "days") return t("time.days", { n });
4480
+ if (unit === "months") return t("time.months", { n });
4481
+ return t("time.years", { n });
4482
+ }
4483
+ //#endregion
4484
+ //#region src/client/GitHistoryPanel.tsx
4485
+ /**
4486
+ * GitHistoryPanel: the git-management tab for the dsh-better-sidebar right
4487
+ * sidebar. The entry card, selection, and presentation are better-sidebar's
4488
+ * (registered through `ctx.betterSidebar.registerTab` from index.ts); this
4489
+ * file renders the TAB CONTENT: a slim chrome (repo pill, refresh) over the
4490
+ * three stacked sections (uncommitted changes, branches, commit history).
4491
+ * Session facts ride the tab call's scope (sessionId/cwd) plus the session
4492
+ * list store (the model-selection projection feeds the AI message
4493
+ * generator). All git calls ride the plugin's own routes through the api
4494
+ * module; every failure lands in a toast or an inline error line, never a
4495
+ * crash boundary.
4496
+ *
4497
+ * The state owner is {@link GitHistoryPanelData} below: one component owning
4498
+ * the status/changes/log state machine, feeding the three presentational
4499
+ * sections, keyed by cwd so a session switch starts every section fresh.
4500
+ *
4501
+ * @module git-worktree/client/GitHistoryPanel
4502
+ */
4503
+ /** One git failure → its message text (the git stderr rides error.message). */
4504
+ function failureText(error) {
4505
+ return error instanceof Error ? error.message : String(error);
4506
+ }
4507
+ /** Data owner for the three sections. Remounts when `cwd` changes (the page
4508
+ * keys the mount by cwd), so no state crosses a repository boundary. */
4509
+ const GitHistoryPanelData = (0, react.forwardRef)(function GitHistoryPanelData(props, ref) {
4510
+ const { cwd, model, t, pushToast } = props;
4511
+ const [status, setStatus] = (0, react.useState)(null);
4512
+ const [statusError, setStatusError] = (0, react.useState)();
4513
+ const [changes, setChanges] = (0, react.useState)([]);
4514
+ const [changesTruncated, setChangesTruncated] = (0, react.useState)(false);
4515
+ const [commits, setCommits] = (0, react.useState)([]);
4516
+ const [historyLoaded, setHistoryLoaded] = (0, react.useState)(false);
4517
+ const [historyLoading, setHistoryLoading] = (0, react.useState)(false);
4518
+ const [historyError, setHistoryError] = (0, react.useState)();
4519
+ const [hasMore, setHasMore] = (0, react.useState)(false);
4520
+ const [loadingMore, setLoadingMore] = (0, react.useState)(false);
4521
+ const [expanded, setExpanded] = (0, react.useState)(null);
4522
+ const [filesByHash, setFilesByHash] = (0, react.useState)({});
4523
+ const [sectionBusy, setSectionBusy] = (0, react.useState)(false);
4524
+ const [syncBusy, setSyncBusy] = (0, react.useState)(false);
4525
+ const [commitMenuOpen, setCommitMenuOpen] = (0, react.useState)(false);
4526
+ const [commitDraft, setCommitDraft] = (0, react.useState)("");
4527
+ const [aiBusy, setAiBusy] = (0, react.useState)(false);
4528
+ /** Response-sequence guard: a refresh raced against a mutation or a cwd
4529
+ * change must never land an older snapshot over a newer one. */
4530
+ const seqRef = (0, react.useRef)(0);
4531
+ const loadHistoryPage = (0, react.useCallback)(async (skip) => {
4532
+ const result = await requestLog(cwd, 50, skip);
4533
+ if (!result.ok) {
4534
+ setHistoryError(result.error);
4535
+ return;
4536
+ }
4537
+ setHistoryError(void 0);
4538
+ return {
4539
+ commits: result.commits,
4540
+ complete: result.commits.length < 50
4541
+ };
4542
+ }, [cwd]);
4543
+ const loadAll = (0, react.useCallback)(async () => {
4544
+ const seq = seqRef.current + 1;
4545
+ seqRef.current = seq;
4546
+ setHistoryLoading(true);
4547
+ const [statusResult, changesResult, logPage] = await Promise.all([
4548
+ fetchStatus(cwd),
4549
+ requestChanges(cwd),
4550
+ loadHistoryPage(0)
4551
+ ]);
4552
+ if (seq !== seqRef.current) return;
4553
+ if (statusResult.ok) {
4554
+ setStatus(statusResult);
4555
+ setStatusError(void 0);
4556
+ } else {
4557
+ setStatus(null);
4558
+ setStatusError(statusResult.error);
4559
+ }
4560
+ setChanges(changesResult.ok ? changesResult.changes : []);
4561
+ setChangesTruncated(changesResult.ok && changesResult.truncated === true);
4562
+ if (logPage !== void 0) {
4563
+ setCommits(logPage.commits);
4564
+ setHasMore(!logPage.complete);
4565
+ setHistoryLoaded(true);
4566
+ }
4567
+ setHistoryLoading(false);
4568
+ }, [cwd, loadHistoryPage]);
4569
+ (0, react.useEffect)(() => {
4570
+ loadAll();
4571
+ }, [loadAll]);
4572
+ const refresh = (0, react.useCallback)(() => {
4573
+ loadAll();
4574
+ }, [loadAll]);
4575
+ (0, react.useImperativeHandle)(ref, () => ({ refresh }), [refresh]);
4576
+ const repo = status !== null && status.repo ? status : null;
4577
+ /** Run one mutating verb with the section-busy guard and error toast.
4578
+ * Mutations refresh through `loadAll` so their re-read bumps the request
4579
+ * sequence — an older in-flight snapshot can never land over the fresh
4580
+ * post-mutation state. */
4581
+ const runGuarded = (0, react.useCallback)(async (verb) => {
4582
+ setSectionBusy(true);
4583
+ try {
4584
+ await verb();
4585
+ } catch (error) {
4586
+ pushToast(t("errorGeneric", { message: failureText(error) }), "error");
4587
+ } finally {
4588
+ setSectionBusy(false);
4589
+ }
4590
+ }, [pushToast, t]);
4591
+ const onToggleRow = (0, react.useCallback)((row) => {
4592
+ runGuarded(async () => {
4593
+ const paths = row.oldPath === void 0 ? [row.path] : [row.path, row.oldPath];
4594
+ const result = await requestStage(cwd, paths, !row.staged);
4595
+ if (!result.ok) throw new Error(result.error);
4596
+ await loadAll();
4597
+ });
4598
+ }, [
4599
+ cwd,
4600
+ loadAll,
4601
+ runGuarded
4602
+ ]);
4603
+ const stagePathsOf = (rows, includeOldPath) => {
4604
+ const paths = [];
4605
+ for (const row of rows) {
4606
+ paths.push(row.path);
4607
+ if (includeOldPath && row.oldPath !== void 0) paths.push(row.oldPath);
4608
+ }
4609
+ return [...new Set(paths)];
4610
+ };
4611
+ const onStageAll = (0, react.useCallback)(() => {
4612
+ runGuarded(async () => {
4613
+ const result = await requestStage(cwd, stagePathsOf(groupChanges(changes).unstaged, false), true);
4614
+ if (!result.ok) throw new Error(result.error);
4615
+ await loadAll();
4616
+ });
4617
+ }, [
4618
+ changes,
4619
+ cwd,
4620
+ loadAll,
4621
+ runGuarded
4622
+ ]);
4623
+ const onUnstageAll = (0, react.useCallback)(() => {
4624
+ runGuarded(async () => {
4625
+ const result = await requestStage(cwd, stagePathsOf(groupChanges(changes).staged, true), false);
4626
+ if (!result.ok) throw new Error(result.error);
4627
+ await loadAll();
4628
+ });
4629
+ }, [
4630
+ changes,
4631
+ cwd,
4632
+ loadAll,
4633
+ runGuarded
4634
+ ]);
4635
+ const onCommit = (0, react.useCallback)((push) => {
4636
+ const message = commitDraft.trim();
4637
+ if (message === "") return;
4638
+ runGuarded(async () => {
4639
+ const result = await requestCommit(cwd, message);
4640
+ if (!result.ok) throw new Error(result.error);
4641
+ setCommitDraft("");
4642
+ setCommitMenuOpen(false);
4643
+ if (!push) {
4644
+ pushToast(t("panel.commitDone", { subject: result.subject }), "success");
4645
+ await loadAll();
4646
+ return;
4647
+ }
4648
+ try {
4649
+ const pushResult = await requestPush(cwd);
4650
+ if (!pushResult.ok) {
4651
+ pushToast(t("panel.commitDone", { subject: result.subject }), "success");
4652
+ pushToast(t("errorGeneric", { message: pushResult.error }), "error");
4653
+ } else pushToast(t("panel.commitPushDone", { subject: result.subject }), "success");
4654
+ } catch (error) {
4655
+ pushToast(t("panel.commitDone", { subject: result.subject }), "success");
4656
+ pushToast(t("errorGeneric", { message: failureText(error) }), "error");
4657
+ }
4658
+ await loadAll();
4659
+ });
4660
+ }, [
4661
+ commitDraft,
4662
+ cwd,
4663
+ loadAll,
4664
+ pushToast,
4665
+ runGuarded,
4666
+ t
4667
+ ]);
4668
+ const onAiGenerate = (0, react.useCallback)(() => {
4669
+ if (model === void 0 || aiBusy) return;
4670
+ setAiBusy(true);
4671
+ (async () => {
4672
+ try {
4673
+ const result = await requestAiCommitMessage({
4674
+ repoPath: cwd,
4675
+ provider: model.provider,
4676
+ model: model.model,
4677
+ ...model.reasoningEffort === void 0 ? {} : { effort: model.reasoningEffort },
4678
+ ...commitDraft.trim() === "" ? {} : { hint: commitDraft.trim() }
4679
+ });
4680
+ if (!result.ok) {
4681
+ pushToast(t("panel.aiFailed", { message: result.error }), "error");
4682
+ return;
4683
+ }
4684
+ setCommitDraft(result.message);
4685
+ pushToast(t("panel.aiDone"), "success");
4686
+ } catch (error) {
4687
+ pushToast(t("panel.aiFailed", { message: failureText(error) }), "error");
4688
+ } finally {
4689
+ setAiBusy(false);
4690
+ }
4691
+ })();
4692
+ }, [
4693
+ aiBusy,
4694
+ commitDraft,
4695
+ cwd,
4696
+ model,
4697
+ pushToast,
4698
+ t
4699
+ ]);
4700
+ const onFetch = (0, react.useCallback)(() => {
4701
+ setSyncBusy(true);
4702
+ (async () => {
4703
+ try {
4704
+ const result = await requestFetch(cwd);
4705
+ if (!result.ok) pushToast(t("errorGeneric", { message: result.error }), "error");
4706
+ else pushToast(t("fetchDone"), "success");
4707
+ await loadAll();
4708
+ } finally {
4709
+ setSyncBusy(false);
4710
+ }
4711
+ })();
4712
+ }, [
4713
+ cwd,
4714
+ loadAll,
4715
+ pushToast,
4716
+ t
4717
+ ]);
4718
+ const onUpdate = (0, react.useCallback)(() => {
4719
+ setSyncBusy(true);
4720
+ (async () => {
4721
+ try {
4722
+ const result = await requestUpdate(cwd);
4723
+ if (!result.ok) pushToast(t("errorGeneric", { message: result.error }), "error");
4724
+ else pushToast(result.updated ? t("updateDone", { branch: result.branch }) : t("updateUpToDate"), "success");
4725
+ await loadAll();
4726
+ } finally {
4727
+ setSyncBusy(false);
4728
+ }
4729
+ })();
4730
+ }, [
4731
+ cwd,
4732
+ loadAll,
4733
+ pushToast,
4734
+ t
4735
+ ]);
4736
+ const onPush = (0, react.useCallback)(() => {
4737
+ setSyncBusy(true);
4738
+ (async () => {
4739
+ try {
4740
+ const result = await requestPush(cwd);
4741
+ if (!result.ok) pushToast(t("errorGeneric", { message: result.error }), "error");
4742
+ else pushToast(result.pushed ? t("pushDone", { branch: result.branch }) : t("pushUpToDate"), "success");
4743
+ await loadAll();
4744
+ } finally {
4745
+ setSyncBusy(false);
4746
+ }
4747
+ })();
4748
+ }, [
4749
+ cwd,
4750
+ loadAll,
4751
+ pushToast,
4752
+ t
4753
+ ]);
4754
+ const onCheckout = (0, react.useCallback)((branch) => {
4755
+ runGuarded(async () => {
4756
+ const result = await requestSwitch(cwd, branch.name);
4757
+ if (!result.ok) throw new Error(result.error);
4758
+ pushToast(t("panel.checkoutDone", { branch: result.branch }), "success");
4759
+ await loadAll();
4760
+ });
4761
+ }, [
4762
+ cwd,
4763
+ loadAll,
4764
+ pushToast,
4765
+ runGuarded,
4766
+ t
4767
+ ]);
4768
+ const onRename = (0, react.useCallback)((branch, newName) => {
4769
+ runGuarded(async () => {
4770
+ const result = await requestRenameBranch(cwd, branch.name, newName);
4771
+ if (!result.ok) throw new Error(result.error);
4772
+ pushToast(t("panel.renameDone", { branch: result.branch }), "success");
4773
+ await loadAll();
4774
+ });
4775
+ }, [
4776
+ cwd,
4777
+ loadAll,
4778
+ pushToast,
4779
+ runGuarded,
4780
+ t
4781
+ ]);
4782
+ const onDelete = (0, react.useCallback)((branch) => {
4783
+ runGuarded(async () => {
4784
+ const result = await requestDeleteBranch(cwd, branch.name);
4785
+ if (!result.ok) throw new Error(result.error);
4786
+ pushToast(t("panel.deleteDone", { branch: result.branch }), "success");
4787
+ await loadAll();
4788
+ });
4789
+ }, [
4790
+ cwd,
4791
+ loadAll,
4792
+ pushToast,
4793
+ runGuarded,
4794
+ t
4795
+ ]);
4796
+ const onCreate = (0, react.useCallback)((name, from) => {
4797
+ runGuarded(async () => {
4798
+ const result = await requestCreateBranch(cwd, name, from);
4799
+ if (!result.ok) throw new Error(result.error);
4800
+ pushToast(t("panel.createDone", { branch: result.branch }), "success");
4801
+ await loadAll();
4802
+ });
4803
+ }, [
4804
+ cwd,
4805
+ loadAll,
4806
+ pushToast,
4807
+ runGuarded,
4808
+ t
4809
+ ]);
4810
+ const onToggleCommit = (0, react.useCallback)((hash) => {
4811
+ if (expanded === hash) {
4812
+ setExpanded(null);
4813
+ return;
4814
+ }
4815
+ setExpanded(hash);
4816
+ if (filesByHash[hash] !== void 0) return;
4817
+ setFilesByHash((prev) => ({
4818
+ ...prev,
4819
+ [hash]: { kind: "loading" }
4820
+ }));
4821
+ (async () => {
4822
+ const result = await requestCommitFiles(cwd, hash);
4823
+ setFilesByHash((prev) => ({
4824
+ ...prev,
4825
+ [hash]: result.ok ? {
4826
+ kind: "loaded",
4827
+ files: result.files
4828
+ } : {
4829
+ kind: "failed",
4830
+ message: result.error
4831
+ }
4832
+ }));
4833
+ })();
4834
+ }, [
4835
+ cwd,
4836
+ expanded,
4837
+ filesByHash
4838
+ ]);
4839
+ const onLoadMore = (0, react.useCallback)(() => {
4840
+ if (loadingMore) return;
4841
+ setLoadingMore(true);
4842
+ const seq = seqRef.current;
4843
+ (async () => {
4844
+ try {
4845
+ const page = await loadHistoryPage(commits.length);
4846
+ if (seq !== seqRef.current) return;
4847
+ if (page !== void 0) {
4848
+ setCommits((prev) => appendPage(prev, page.commits));
4849
+ setHasMore(!page.complete);
4850
+ }
4851
+ } finally {
4852
+ setLoadingMore(false);
4853
+ }
4854
+ })();
4855
+ }, [
4856
+ commits.length,
4857
+ loadHistoryPage,
4858
+ loadingMore
4859
+ ]);
4860
+ const timeLabelFor = (0, react.useCallback)((date) => timeLabel(date, Date.now(), t), [t]);
4861
+ if (statusError !== void 0 && repo === null) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4862
+ className: GitHistoryPanel_module_default.errorText,
4863
+ children: t("panel.loadFailed", { message: statusError })
4864
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4865
+ className: GitHistoryPanel_module_default.moreRow,
4866
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4867
+ type: "button",
4868
+ className: GitHistoryPanel_module_default.act,
4869
+ onClick: refresh,
4870
+ children: t("panel.retry")
4871
+ })
4872
+ })] });
4873
+ if (status !== null && status.repo === false) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4874
+ className: GitHistoryPanel_module_default.empty,
4875
+ children: t("panel.notARepo")
4876
+ });
4877
+ if (repo === null) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4878
+ className: GitHistoryPanel_module_default.empty,
4879
+ children: t("manager.loading")
4880
+ });
4881
+ const grouped = groupChanges(changes);
4882
+ const rows = buildBranchRows(repo.branches, repo.currentBranch);
4883
+ const remoteRows = buildBranchRows(repo.remoteBranches ?? repo.branches.filter((branch) => branch.kind === "remote"), "").remotes;
4884
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4885
+ className: GitHistoryPanel_module_default.scroll,
4886
+ children: [
4887
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UncommittedSection, {
4888
+ t,
4889
+ staged: grouped.staged,
4890
+ unstaged: grouped.unstaged,
4891
+ busy: sectionBusy,
4892
+ syncBusy,
4893
+ truncated: changesTruncated,
4894
+ commitDraft,
4895
+ onCommitDraft: setCommitDraft,
4896
+ commitMenuOpen,
4897
+ onToggleCommitMenu: () => setCommitMenuOpen((open) => !open),
4898
+ onCommit,
4899
+ onToggleRow,
4900
+ onStageAll,
4901
+ onUnstageAll,
4902
+ aiAvailable: model !== void 0,
4903
+ aiBusy,
4904
+ onAiGenerate,
4905
+ onFetch,
4906
+ onUpdate,
4907
+ onPush
4908
+ }),
4909
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BranchSection, {
4910
+ t,
4911
+ locals: rows.locals,
4912
+ remotes: remoteRows,
4913
+ busy: sectionBusy,
4914
+ onCheckout,
4915
+ onRename,
4916
+ onDelete,
4917
+ onCreate
4918
+ }),
4919
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(HistorySection, {
4920
+ t,
4921
+ commits,
4922
+ loaded: historyLoaded,
4923
+ loadError: historyError,
4924
+ loading: historyLoading,
4925
+ hasMore,
4926
+ loadingMore,
4927
+ expanded,
4928
+ filesByHash,
4929
+ onLoadMore,
4930
+ onToggle: onToggleCommit,
4931
+ timeLabelFor
4932
+ })
4933
+ ]
4934
+ });
4935
+ });
4936
+ /** The registered better-sidebar tab's content: chrome + data owner, keyed
4937
+ * by cwd so a session switch starts every section from scratch. */
4938
+ function GitPanelTab(props) {
4939
+ const { ctx, scope, t } = props;
4940
+ const subscribe = (0, react.useCallback)((listener) => ctx.sessions.list.subscribe(listener), [ctx]);
4941
+ const cwd = (0, react.useSyncExternalStore)(subscribe, () => scope.cwd ?? ctx.sessions.list.getSnapshot().byId[scope.sessionId]?.cwd);
4942
+ const model = (0, react.useSyncExternalStore)(subscribe, () => {
4943
+ const selection = ctx.sessions.list.getSnapshot().projectionsBySession[scope.sessionId]?.values.modelSelection;
4944
+ return selection === void 0 ? void 0 : selection.next ?? selection.lastUsed ?? void 0;
4945
+ });
4946
+ const [toasts, setToasts] = (0, react.useState)([]);
4947
+ const toastSeq = (0, react.useRef)(0);
4948
+ const dataRef = (0, react.useRef)(null);
4949
+ const pushToast = (0, react.useCallback)((text, tone) => {
4950
+ toastSeq.current += 1;
4951
+ const seq = toastSeq.current;
4952
+ setToasts((queue) => [...queue, {
4953
+ seq,
4954
+ text,
4955
+ tone
4956
+ }]);
4957
+ }, []);
4958
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4959
+ className: GitHistoryPanel_module_default.page,
4960
+ children: [
4961
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
4962
+ className: GitHistoryPanel_module_default.topbar,
4963
+ children: [cwd !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4964
+ className: GitHistoryPanel_module_default.repoPill,
4965
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4966
+ className: GitHistoryPanel_module_default.repoName,
4967
+ children: cwd
4968
+ })
4969
+ }) : null, /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4970
+ className: GitHistoryPanel_module_default.topbarActions,
4971
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4972
+ type: "button",
4973
+ className: GitHistoryPanel_module_default.refreshBtn,
4974
+ disabled: cwd === void 0,
4975
+ title: t("panel.refresh"),
4976
+ "aria-label": t("panel.refresh"),
4977
+ onClick: () => dataRef.current?.refresh(),
4978
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconRefreshOutlineRegular, {
4979
+ size: 14,
4980
+ "aria-hidden": "true"
4981
+ })
4982
+ })
4983
+ })]
4984
+ }),
4985
+ cwd === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4986
+ className: GitHistoryPanel_module_default.empty,
4987
+ children: t("panel.noSession")
4988
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GitHistoryPanelData, {
4989
+ ref: dataRef,
4990
+ cwd,
4991
+ model,
4992
+ t,
4993
+ pushToast
4994
+ }, cwd),
4995
+ toasts.map((toast) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Toast, {
4996
+ text: toast.text,
4997
+ ...toast.tone === "success" ? { tone: "success" } : {},
4998
+ onDone: () => setToasts((queue) => queue.filter((entry) => entry.seq !== toast.seq))
4999
+ }, String(toast.seq)))
5000
+ ]
5001
+ });
5002
+ }
5003
+ //#endregion
5004
+ //#region src/client/prune-history.ts
5005
+ const KEY = "git-worktree:prune-history";
5006
+ /** The log is a recent-activity trail, not an archive: the oldest runs fall
5007
+ * off once this many are kept. */
5008
+ const MAX_RUNS = 20;
5009
+ /** Append one finished run (newest first), trimming the tail past
5010
+ * {@link MAX_RUNS}. Best-effort: a storage write that throws (private mode,
5011
+ * quota) swallows — the toast already told the user what happened. */
5012
+ function recordPruneRun(entry) {
5013
+ try {
5014
+ const next = [entry, ...readPruneHistory()].slice(0, MAX_RUNS);
5015
+ localStorage.setItem(KEY, JSON.stringify(next));
5016
+ } catch {}
5017
+ }
5018
+ /** Structural check of ONE stored run. The payload is browser-local JSON an
5019
+ * older build (or anything else holding this key) may have written in a
5020
+ * different shape, and the settings card reads `run.removed.length` straight
5021
+ * into its render — a top-level `Array.isArray` alone would let `[1, 2]`
5022
+ * through and take the whole card down with a TypeError. Only the fields the
5023
+ * card actually touches are checked; their element shapes stay trusted (a
5024
+ * malformed member degrades one line, not the page). */
5025
+ function isPruneRunEntry(value) {
5026
+ if (typeof value !== "object" || value === null) return false;
5027
+ const entry = value;
5028
+ return typeof entry.at === "number" && Array.isArray(entry.removed) && Array.isArray(entry.skippedDirty) && Array.isArray(entry.failed);
5029
+ }
5030
+ /** The recorded runs, newest first; an unreadable/foreign payload reads as
5031
+ * "no history" rather than throwing into the settings render — entry by
5032
+ * entry, so one bad record costs its own line and nothing else. */
5033
+ function readPruneHistory() {
5034
+ try {
5035
+ const raw = localStorage.getItem(KEY);
5036
+ if (raw === null) return [];
5037
+ const parsed = JSON.parse(raw);
5038
+ return Array.isArray(parsed) ? parsed.filter(isPruneRunEntry) : [];
5039
+ } catch {
5040
+ return [];
5041
+ }
5042
+ }
5043
+ //#endregion
5044
+ //#region src/client/concurrency.ts
5045
+ /**
5046
+ * Bounded-concurrency map for the browser half's fan-out calls.
5047
+ *
5048
+ * Every plugin route that answers a batch is a git process per item on the
5049
+ * host side, and on Windows a process spawn is the expensive part — the
5050
+ * `/group` route already caps its own fan-out for exactly this reason. The
5051
+ * browser side has the same duty for the calls it makes ONE PER ITEM
5052
+ * (the manager dialog inspects every scanned worktree): a bare
5053
+ * `Promise.all(items.map(...))` hands the host as many concurrent spawns as
5054
+ * the user happens to have directories.
5055
+ *
5056
+ * @module git-worktree/client/concurrency
5057
+ */
5058
+ /**
5059
+ * Map over `items` with at most `limit` calls in flight, preserving INPUT
5060
+ * ORDER in the result (the caller zips results back onto its rows, so
5061
+ * completion order must not leak). Rejections propagate like `Promise.all`:
5062
+ * callers that want per-item tolerance catch inside `fn`.
5063
+ *
5064
+ * @param items - the inputs, mapped in order.
5065
+ * @param limit - maximum concurrent calls; values under 1 are clamped to 1.
5066
+ * @param fn - the per-item async work, given the item and its index.
5067
+ * @returns the results, index-aligned with `items`.
5068
+ */
5069
+ async function mapLimit(items, limit, fn) {
5070
+ const queue = items.map((item, index) => [item, index]);
5071
+ const results = [];
5072
+ const width = Math.max(1, Math.floor(limit));
5073
+ let cursor = 0;
5074
+ const worker = async () => {
5075
+ for (;;) {
5076
+ const entry = queue[cursor];
5077
+ cursor += 1;
5078
+ if (entry === void 0) return;
5079
+ const [item, index] = entry;
5080
+ results[index] = await fn(item, index);
5081
+ }
5082
+ };
5083
+ await Promise.all(Array.from({ length: Math.min(width, queue.length) }, worker));
5084
+ return results;
5085
+ }
5086
+ //#endregion
5087
+ //#region src/client/scan-groups.ts
5088
+ /** Group the scan for display: project groups before legacy groups (same
5089
+ * repo can appear under both), repositories alphabetical within a source,
5090
+ * rows within a group by branch name, all numeric-aware so `feat/v2` sorts
5091
+ * past `feat/v10`. Grouping is display-order only — the removal flows
5092
+ * address rows by path. */
5093
+ function groupScanEntries(entries) {
5094
+ const byKey = /* @__PURE__ */ new Map();
5095
+ const orphans = [];
5096
+ for (const entry of entries) {
5097
+ if (entry.repoName === null) {
5098
+ orphans.push(entry);
5099
+ continue;
5100
+ }
5101
+ const key = `${entry.source}\u0000${entry.repoName}`;
5102
+ const bucket = byKey.get(key);
5103
+ if (bucket === void 0) byKey.set(key, {
5104
+ source: entry.source,
5105
+ repoName: entry.repoName,
5106
+ list: [entry]
5107
+ });
5108
+ else bucket.list.push(entry);
5109
+ }
5110
+ const cmp = (a, b) => a.localeCompare(b, void 0, { numeric: true });
5111
+ const groups = [...byKey.values()].sort((a, b) => a.source === b.source ? cmp(a.repoName, b.repoName) : a.source === "project" ? -1 : 1).map(({ source, repoName, list }) => ({
5112
+ repoName,
5113
+ source,
5114
+ entries: list.slice().sort((a, b) => cmp(a.branch ?? "", b.branch ?? ""))
5115
+ }));
5116
+ return orphans.length > 0 ? [...groups, {
5117
+ repoName: null,
5118
+ source: "orphans",
5119
+ entries: orphans
5120
+ }] : groups;
5121
+ }
5122
+ //#endregion
5123
+ //#region src/client/worktree-remove-flow.ts
5124
+ /**
5125
+ * Remove one worktree completely. A refused git removal rethrows UNLESS the
5126
+ * directory is actually gone (the Windows half-removal shape: git already
5127
+ * unregistered and emptied the tree, then failed the final rmdir — the git
5128
+ * half is done, so the flow continues with the DSH half). Session archives
5129
+ * never abort the flow (a rejected archive is logged and skipped); a failed
5130
+ * workspace unregistration DOES reject — the caller's UI owns the retry.
5131
+ */
5132
+ async function removeWorktreeFully(deps, target) {
5133
+ try {
5134
+ await deps.removeWorktree(target.path, target.force);
5135
+ } catch (reason) {
5136
+ if ((await deps.probeDirectories([target.path]).catch(() => void 0))?.exists[target.path] !== false) throw reason;
5137
+ }
5138
+ for (const sessionId of target.archiveSessionIds ?? []) await deps.archiveSession(sessionId).catch((reason) => {
5139
+ console.warn("session archive rejected during worktree removal:", reason);
5140
+ });
5141
+ if (target.workspaceId !== void 0) await deps.deleteWorkspace(target.workspaceId);
5142
+ }
5143
+ //#endregion
5144
+ //#region src/client/worktree-prune.ts
5145
+ /** Canonical-ish key for cross-source path comparison: both sides come from
5146
+ * the same host, but the scan assembles `<rootDir>/<child>` while the
5147
+ * workspace model stores its own canonical spelling — separators are the
5148
+ * only drift worth papering over. */
5149
+ function pathKey(path) {
5150
+ return path.replace(/\\/g, "/").replace(/\/+$/, "");
5151
+ }
5152
+ /** Freshest `updatedAt` across QUALIFYING sessions (non-blank, non-subagent
5153
+ * — the same qualification the archive set uses); 0 when nothing qualifies,
5154
+ * which reads as "least active in the room". Shared by the lazy prune's
5155
+ * plan input and the manager dialog's last-use column so the two activity
5156
+ * figures can never drift apart. */
5157
+ function freshestUpdatedAt(summaries) {
5158
+ let latest = 0;
5159
+ for (const summary of summaries) {
5160
+ if (summary === void 0 || summary.blank || summary.origin === "subagent") continue;
5161
+ if (summary.updatedAt > latest) latest = summary.updatedAt;
5162
+ }
5163
+ return latest;
5164
+ }
5165
+ /**
5166
+ * Which worktrees the lazy prune removes, least-recently-active first.
5167
+ * The cap counts every valid worktree INCLUDING the excluded ones (the
5168
+ * fresh creation counts against the cap it may have just overfilled), but
5169
+ * excluded directories are never candidates: the plan deletes the
5170
+ * overflow's worth of the least-active non-excluded paths, so a cap
5171
+ * tighter than the exclusion set simply deletes nothing this round.
5172
+ * Ties (two never-used directories) break alphabetically for a stable,
5173
+ * testable order.
3515
5174
  */
3516
5175
  function planPrune(input) {
3517
5176
  const keep = Math.max(1, Math.floor(input.keep));
@@ -4449,7 +6108,67 @@ window.__ModuleLoader__.load({
4449
6108
  "time.years": "{n}y",
4450
6109
  copy: "Copy",
4451
6110
  close: "Close",
4452
- cancel: "Cancel"
6111
+ cancel: "Cancel",
6112
+ "panel.nav": "Git panel",
6113
+ "panel.description": "Uncommitted changes, branches, and commit history",
6114
+ "panel.notARepo": "The current session is not inside a git repository.",
6115
+ "panel.noSession": "Open a session in a repository to see its git panel.",
6116
+ "panel.loadFailed": "Git panel failed to load: {message}",
6117
+ "panel.retry": "Retry",
6118
+ "panel.refresh": "Refresh",
6119
+ "panel.uncommitted": "Uncommitted changes",
6120
+ "panel.staged": "Staged",
6121
+ "panel.unstaged": "Unstaged",
6122
+ "panel.stageAll": "Stage all",
6123
+ "panel.unstageAll": "Unstage all",
6124
+ "panel.stageHint": "git add",
6125
+ "panel.unstageHint": "git reset",
6126
+ "panel.clean": "No uncommitted changes",
6127
+ "panel.changesTruncated": "Too many changes — showing only the first 500 rows",
6128
+ "panel.commitPlaceholder": "Commit message (feat: …)",
6129
+ "panel.commit": "Commit",
6130
+ "panel.commitPush": "Commit and push",
6131
+ "panel.commitMoreAria": "More commit actions",
6132
+ "panel.commitBusy": "Committing…",
6133
+ "panel.commitDone": "Committed: {subject}",
6134
+ "panel.commitPushDone": "Committed and pushed: {subject}",
6135
+ "panel.commitNeedsStaged": "Tick files to stage them first",
6136
+ "panel.commitNeedsMessage": "Enter a commit message",
6137
+ "panel.aiGenerate": "AI message",
6138
+ "panel.aiBusy": "Generating…",
6139
+ "panel.aiNeedsChanges": "Stage some changes first, then generate",
6140
+ "panel.aiDone": "AI drafted a message — review and edit before committing",
6141
+ "panel.aiFailed": "AI message failed: {message}",
6142
+ "panel.branches": "Branches",
6143
+ "panel.branchNew": "New branch",
6144
+ "panel.branchLocals": "local",
6145
+ "panel.branchRemotes": "remote",
6146
+ "panel.branchCheckout": "Check out",
6147
+ "panel.branchCheckoutRemote": "Check out (create local tracking branch)",
6148
+ "panel.branchNewFrom": "New branch from here…",
6149
+ "panel.branchRename": "Rename…",
6150
+ "panel.branchDelete": "Delete",
6151
+ "panel.branchCopy": "Copy name",
6152
+ "panel.branchCurrent": "current branch",
6153
+ "panel.checkoutDone": "Checked out {branch}",
6154
+ "panel.renameDone": "Renamed to {branch}",
6155
+ "panel.deleteDone": "Deleted branch {branch}",
6156
+ "panel.createDone": "Created branch {branch}",
6157
+ "panel.history": "Commit history",
6158
+ "panel.historySearch": "Search commits / authors",
6159
+ "panel.noCommits": "This repository has no commits yet",
6160
+ "panel.noMatches": "No matching commits",
6161
+ "panel.loadMore": "Load more",
6162
+ "panel.filesTitle": "Changed files ({n}) · {author} · {date}",
6163
+ "panel.binary": "binary",
6164
+ "panel.stAdd": "added",
6165
+ "panel.stMod": "modified",
6166
+ "panel.stDel": "deleted",
6167
+ "panel.stRen": "renamed",
6168
+ "panel.stCopy": "copied",
6169
+ "panel.stType": "type changed",
6170
+ "panel.stUntrack": "untracked",
6171
+ "panel.stUnmerged": "conflict"
4453
6172
  };
4454
6173
  /** 中文词典。 */
4455
6174
  const zh = {
@@ -4579,17 +6298,83 @@ window.__ModuleLoader__.load({
4579
6298
  "time.years": "{n}年",
4580
6299
  copy: "复制",
4581
6300
  close: "关闭",
4582
- cancel: "取消"
6301
+ cancel: "取消",
6302
+ "panel.nav": "Git 管理",
6303
+ "panel.description": "未提交变更、分支操作与提交历史",
6304
+ "panel.notARepo": "当前会话不在 git 仓库中。",
6305
+ "panel.noSession": "在 git 仓库中打开一个会话后,这里会显示它的 Git 面板。",
6306
+ "panel.loadFailed": "Git 面板加载失败:{message}",
6307
+ "panel.retry": "重试",
6308
+ "panel.refresh": "刷新",
6309
+ "panel.uncommitted": "未提交变更",
6310
+ "panel.staged": "已暂存",
6311
+ "panel.unstaged": "未暂存",
6312
+ "panel.stageAll": "全部暂存",
6313
+ "panel.unstageAll": "全部取消暂存",
6314
+ "panel.stageHint": "git add",
6315
+ "panel.unstageHint": "git reset",
6316
+ "panel.clean": "没有未提交的变更",
6317
+ "panel.changesTruncated": "变更过多,仅显示前 500 行",
6318
+ "panel.commitPlaceholder": "提交信息(feat: …)",
6319
+ "panel.commit": "提交",
6320
+ "panel.commitPush": "提交并推送",
6321
+ "panel.commitMoreAria": "更多提交操作",
6322
+ "panel.commitBusy": "提交中…",
6323
+ "panel.commitDone": "已提交:{subject}",
6324
+ "panel.commitPushDone": "已提交并推送:{subject}",
6325
+ "panel.commitNeedsStaged": "先勾选文件加入暂存区",
6326
+ "panel.commitNeedsMessage": "填写提交信息后提交",
6327
+ "panel.aiGenerate": "AI 生成",
6328
+ "panel.aiBusy": "生成中…",
6329
+ "panel.aiNeedsChanges": "先暂存一些变更再生成",
6330
+ "panel.aiDone": "AI 已生成提交信息,请检查后提交",
6331
+ "panel.aiFailed": "AI 生成失败:{message}",
6332
+ "panel.branches": "分支",
6333
+ "panel.branchNew": "新建分支",
6334
+ "panel.branchLocals": "本地",
6335
+ "panel.branchRemotes": "远程",
6336
+ "panel.branchCheckout": "检出",
6337
+ "panel.branchCheckoutRemote": "检出(新建本地跟踪分支)",
6338
+ "panel.branchNewFrom": "新建分支自此…",
6339
+ "panel.branchRename": "重命名…",
6340
+ "panel.branchDelete": "删除",
6341
+ "panel.branchCopy": "复制名称",
6342
+ "panel.branchCurrent": "当前分支",
6343
+ "panel.checkoutDone": "已检出 {branch}",
6344
+ "panel.renameDone": "已重命名为 {branch}",
6345
+ "panel.deleteDone": "已删除分支 {branch}",
6346
+ "panel.createDone": "已创建分支 {branch}",
6347
+ "panel.history": "提交历史",
6348
+ "panel.historySearch": "搜索提交 / 作者",
6349
+ "panel.noCommits": "仓库还没有提交",
6350
+ "panel.noMatches": "没有匹配的提交",
6351
+ "panel.loadMore": "加载更多",
6352
+ "panel.filesTitle": "变更文件({n})· {author} · {date}",
6353
+ "panel.binary": "二进制",
6354
+ "panel.stAdd": "新增",
6355
+ "panel.stMod": "修改",
6356
+ "panel.stDel": "删除",
6357
+ "panel.stRen": "重命名",
6358
+ "panel.stCopy": "复制",
6359
+ "panel.stType": "类型变更",
6360
+ "panel.stUntrack": "未跟踪",
6361
+ "panel.stUnmerged": "冲突"
4583
6362
  };
4584
6363
  //#endregion
4585
6364
  //#region src/client/index.ts
4586
6365
  /** Dictionary namespace owned by this plugin. */
4587
6366
  const NS = "git-worktree";
4588
6367
  /**
4589
- * Namespace of the git-worktree settings section. Spelled here rather than
4590
- * imported: a client package must not depend on a Host package.
6368
+ * Host composition entry id from cordis.patch.yml. Configuration forms use
6369
+ * this id; locale and route namespaces are independent. Kept client-local
6370
+ * because the browser bundle must not import the Host plugin.
4591
6371
  */
4592
- const GIT_WORKTREE_NS = "git-worktree";
6372
+ const PLUGIN_ENTRY_ID = "dsh-git-branch";
6373
+ /**
6374
+ * The Git panel's identity: the sidebar row id and the matching `main` panel
6375
+ * key. Package-local on purpose — the slot addresses stay inside the bundle.
6376
+ */
6377
+ const PANEL_ID = "git-history";
4593
6378
  /**
4594
6379
  * This package's own name — the key `plugins.bundle.config` dispatches by
4595
6380
  * (the slot contract keys a bundle's own configuration by the bundle's
@@ -4614,7 +6399,7 @@ window.__ModuleLoader__.load({
4614
6399
  zh,
4615
6400
  en
4616
6401
  }), "git-worktree: dictionaries");
4617
- const groupingForm = ctx.configForms.get(GIT_WORKTREE_NS);
6402
+ const settingsForm = ctx.configForms.get(PLUGIN_ENTRY_ID);
4618
6403
  /** The DSH half of one worktree removal, read from the live snapshots: the
4619
6404
  * workspace registration sitting on the directory, whether any of its
4620
6405
  * sessions is running (the verb withholds), and the archive set —
@@ -4678,7 +6463,7 @@ window.__ModuleLoader__.load({
4678
6463
  });
4679
6464
  },
4680
6465
  pruneWorktrees: async (createdPath) => {
4681
- const section = groupingForm.getSnapshot();
6466
+ const section = settingsForm.getSnapshot();
4682
6467
  if (section.status !== "ready") return void 0;
4683
6468
  if ((section.value?.autoPruneWorktrees ?? false) !== true) return void 0;
4684
6469
  const keep = section.value?.keepWorktrees ?? 30;
@@ -4822,13 +6607,13 @@ window.__ModuleLoader__.load({
4822
6607
  archiveSession: (sessionId) => ctx.workspaces.archiveSession(sessionId),
4823
6608
  deleteWorkspace: (workspaceId) => ctx.workspaces.delete(workspaceId)
4824
6609
  });
4825
- const form = new CardForm(groupingForm);
6610
+ const form = new CardForm(settingsForm);
4826
6611
  const store = form.bind();
4827
6612
  const describeFace = ctx.configForms.describe();
4828
6613
  let configDisposer;
4829
6614
  const syncCardSeat = () => {
4830
6615
  const snapshot = describeFace.getSnapshot();
4831
- const served = snapshot.status === "ready" && (snapshot.view?.namespaces.some((entry) => entry.ns === GIT_WORKTREE_NS) ?? false);
6616
+ const served = snapshot.status === "ready" && (snapshot.view?.namespaces.some((entry) => entry.ns === PLUGIN_ENTRY_ID) ?? false);
4832
6617
  if (served && configDisposer === void 0) configDisposer = ctx.slots.inject("plugins.bundle.config", () => ctx.slots.register({
4833
6618
  name: "plugins.bundle.config",
4834
6619
  key: PLUGIN_PACKAGE,
@@ -4851,6 +6636,26 @@ window.__ModuleLoader__.load({
4851
6636
  unsubscribeDescribe();
4852
6637
  if (configDisposer !== void 0) configDisposer();
4853
6638
  }, "git-worktree: plugin config card lifecycle");
6639
+ ctx.inject(["betterSidebar"], (betterCtx) => {
6640
+ const better = betterCtx.betterSidebar;
6641
+ const panelT = ctx.locale.bind(NS);
6642
+ ctx.effect(() => better.registerTab({
6643
+ id: PANEL_ID,
6644
+ title: () => panelT("panel.nav"),
6645
+ description: () => panelT("panel.description"),
6646
+ icon: (size) => (0, react.createElement)("span", { style: {
6647
+ color: "#4c6bf5",
6648
+ display: "inline-flex"
6649
+ } }, (0, react.createElement)(_deepseek_ai_dsh_client_ui_primitives.IconBranchOutlineRegular, { size })),
6650
+ order: 150,
6651
+ single: true,
6652
+ component: (tabProps) => (0, react.createElement)(GitPanelTab, {
6653
+ ctx: betterCtx,
6654
+ scope: tabProps.scope,
6655
+ t: panelT
6656
+ })
6657
+ }), "git-worktree: better-sidebar tab");
6658
+ });
4854
6659
  }
4855
6660
  //#endregion
4856
6661
  exports.apply = apply;