dsh-git-branch-manage 1.0.2 → 1.0.4

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,87 @@ 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
+ }
393
+ //#endregion
394
+ //#region src/client/git-events.ts
395
+ const listeners = /* @__PURE__ */ new Set();
396
+ /** Announce that a surface changed repository state (checkout, rename,
397
+ * delete, create, commit, fetch, update, push). */
398
+ function emitGitMutated() {
399
+ for (const listener of [...listeners]) listener();
400
+ }
401
+ /** Subscribe; returns the unsubscribe function. */
402
+ function onGitMutated(listener) {
403
+ listeners.add(listener);
404
+ return () => {
405
+ listeners.delete(listener);
406
+ };
407
+ }
310
408
  //#endregion
311
409
  //#region src/client/branch-tree.ts
312
410
  /** Whether a bare `git push` has a known upstream and local commits ahead. */
@@ -499,13 +597,13 @@ window.__ModuleLoader__.load({
499
597
  }
500
598
  //#endregion
501
599
  //#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) {
600
+ 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";
601
+ const tagId$3 = "dsh-git-branch-manage/BranchChip.module.css";
602
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=\"" + tagId$3 + "\"]") === null) {
505
603
  const tag = document.createElement("style");
506
604
  tag.dataset.plugin = "dsh-git-branch-manage";
507
- tag.dataset.pluginCss = tagId$2;
508
- tag.textContent = css$2;
605
+ tag.dataset.pluginCss = tagId$3;
606
+ tag.textContent = css$3;
509
607
  document.head.appendChild(tag);
510
608
  }
511
609
  var BranchChip_module_default = {
@@ -2401,7 +2499,7 @@ window.__ModuleLoader__.load({
2401
2499
  * {@link buildLinkedWorktreeRows}), so nothing here ever has to express
2402
2500
  * "not usable in this directory".
2403
2501
  */
2404
- function buildBranchRows(branches, worktrees) {
2502
+ function buildBranchRows$1(branches, worktrees) {
2405
2503
  const held = new Set(worktrees.flatMap((w) => w.main || w.branch === void 0 ? [] : [w.branch]));
2406
2504
  const localMap = new Map(branches.filter((b) => b.kind === "local").map((b) => [b.name, b]));
2407
2505
  return [
@@ -2652,6 +2750,12 @@ window.__ModuleLoader__.load({
2652
2750
  document.removeEventListener("visibilitychange", onVisibility);
2653
2751
  };
2654
2752
  }, [refresh]);
2753
+ (0, react.useEffect)(() => {
2754
+ return onGitMutated(() => {
2755
+ if (busyRef.current) return;
2756
+ refresh();
2757
+ });
2758
+ }, [refresh]);
2655
2759
  const showError = (0, react.useCallback)((message) => {
2656
2760
  pushToast(t("errorGeneric", { message }));
2657
2761
  }, [pushToast, t]);
@@ -2673,6 +2777,7 @@ window.__ModuleLoader__.load({
2673
2777
  setConfirm(null);
2674
2778
  if (!keepOpen) setMenuOpen(false);
2675
2779
  onSettled?.();
2780
+ emitGitMutated();
2676
2781
  }, [showError]);
2677
2782
  /** In-place switch flow: POST /switch, then refetch the status. */
2678
2783
  const doSwitch = (0, react.useCallback)((branch, keepOpen = false) => runGuarded(async () => {
@@ -2913,7 +3018,7 @@ window.__ModuleLoader__.load({
2913
3018
  const rows = (0, react.useMemo)(() => {
2914
3019
  if (facts === null) return [];
2915
3020
  if (inLinkedWorktree) return buildLinkedWorktreeRows(facts.branches, facts.worktrees, facts.currentBranch);
2916
- return buildBranchRows(facts.branches, facts.worktrees);
3021
+ return buildBranchRows$1(facts.branches, facts.worktrees);
2917
3022
  }, [facts, inLinkedWorktree]);
2918
3023
  const localNames = (0, react.useMemo)(() => rows.filter((row) => row.kind !== "remote").map((row) => row.name), [rows]);
2919
3024
  if (facts === null) return null;
@@ -3296,205 +3401,1921 @@ window.__ModuleLoader__.load({
3296
3401
  }
3297
3402
  };
3298
3403
  //#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 [];
3404
+ //#region src/client/git-panel-data.ts
3405
+ /**
3406
+ * Classify raw %D decoration segments: `HEAD -> main` (and bare `HEAD`) is
3407
+ * the head badge, `tag: x` a tag, anything containing `/` a remote branch,
3408
+ * the rest a local branch.
3409
+ */
3410
+ function classifyRefs(refs) {
3411
+ const badges = [];
3412
+ for (const ref of refs) {
3413
+ if (ref === "HEAD" || ref.startsWith("HEAD -> ")) {
3414
+ badges.push({
3415
+ kind: "head",
3416
+ text: ref
3417
+ });
3418
+ continue;
3419
+ }
3420
+ if (ref.startsWith("tag: ")) {
3421
+ badges.push({
3422
+ kind: "tag",
3423
+ text: ref.slice(5)
3424
+ });
3425
+ continue;
3426
+ }
3427
+ if (ref.includes("/")) {
3428
+ badges.push({
3429
+ kind: "remote",
3430
+ text: ref
3431
+ });
3432
+ continue;
3433
+ }
3434
+ badges.push({
3435
+ kind: "local",
3436
+ text: ref
3437
+ });
3336
3438
  }
3439
+ return badges;
3337
3440
  }
3338
- //#endregion
3339
- //#region src/client/relative-time.ts
3340
3441
  /**
3341
- * Compact relative time for trailing labels, as a structured bucket the
3342
- * renderer localizes ("now"/"5min"/"3h"/"2d"/"4mo"/"1y" in en).
3442
+ * Split the working-tree rows into the two panel groups. Untracked files
3443
+ * render in the unstaged group as `?` (git add still stages them); an
3444
+ * unmerged conflict renders in the unstaged group as `U` unchecked (git add
3445
+ * resolves it); a rename's checkbox toggles BOTH paths (unstage needs the
3446
+ * old path).
3343
3447
  */
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
- };
3448
+ function groupChanges(changes) {
3449
+ const staged = [];
3450
+ const unstaged = [];
3451
+ for (const change of changes) {
3452
+ if (change.untracked === true) {
3453
+ unstaged.push({
3454
+ path: change.path,
3455
+ status: "?",
3456
+ staged: false
3457
+ });
3458
+ continue;
3459
+ }
3460
+ if (change.staged !== void 0) staged.push({
3461
+ path: change.path,
3462
+ ...change.oldPath !== void 0 ? { oldPath: change.oldPath } : {},
3463
+ status: change.staged,
3464
+ staged: true
3465
+ });
3466
+ if (change.unstaged !== void 0) unstaged.push({
3467
+ path: change.path,
3468
+ ...change.oldPath !== void 0 ? { oldPath: change.oldPath } : {},
3469
+ status: change.unstaged,
3470
+ staged: false
3471
+ });
3472
+ }
3369
3473
  return {
3370
- unit: "years",
3371
- n: Math.floor(diff / (365 * DAY))
3474
+ staged,
3475
+ unstaged
3372
3476
  };
3373
3477
  }
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 });
3478
+ /** Subject-case-insensitive filter over the commit subject and author. */
3479
+ function filterCommits(commits, query) {
3480
+ const needle = query.trim().toLowerCase();
3481
+ if (needle === "") return [...commits];
3482
+ return commits.filter((commit) => commit.subject.toLowerCase().includes(needle) || commit.authorName.toLowerCase().includes(needle));
3383
3483
  }
3384
- //#endregion
3385
- //#region src/client/concurrency.ts
3386
3484
  /**
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
- */
3399
- /**
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`.
3485
+ * Case-insensitive substring filter over the branch name — the branch card's
3486
+ * head search box. A remote row matches on its full `remote/name`, which
3487
+ * contains the prefix-stripped short name the tree renders, so searching what
3488
+ * the user sees and searching the remote prefix both hit.
3409
3489
  */
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);
3422
- }
3423
- };
3424
- await Promise.all(Array.from({ length: Math.min(width, queue.length) }, worker));
3425
- return results;
3490
+ function filterBranches(rows, query) {
3491
+ const needle = query.trim().toLowerCase();
3492
+ if (needle === "") return [...rows];
3493
+ return rows.filter((row) => row.name.toLowerCase().includes(needle));
3426
3494
  }
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);
3440
- continue;
3441
- }
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]
3448
- });
3449
- else bucket.list.push(entry);
3495
+ /** Merge the next page into the accumulated list, dropping hash duplicates
3496
+ * (a ref moved between fetches must not duplicate rows). */
3497
+ function appendPage(current, page) {
3498
+ const seen = new Set(current.map((commit) => commit.hash));
3499
+ const merged = [...current];
3500
+ for (const commit of page) {
3501
+ if (seen.has(commit.hash)) continue;
3502
+ seen.add(commit.hash);
3503
+ merged.push(commit);
3450
3504
  }
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;
3505
+ return merged;
3506
+ }
3507
+ /** Build the branch rows from a RepoStatus: locals first (current first),
3508
+ * then remote-only branches, sorted by name. */
3509
+ function buildBranchRows(branches, currentBranch) {
3510
+ const locals = [];
3511
+ const remotes = [];
3512
+ for (const branch of branches) {
3513
+ const row = {
3514
+ name: branch.name,
3515
+ kind: branch.kind,
3516
+ current: branch.kind === "local" && branch.name === currentBranch,
3517
+ ...branch.ahead !== void 0 ? { ahead: branch.ahead } : {},
3518
+ ...branch.behind !== void 0 ? { behind: branch.behind } : {}
3519
+ };
3520
+ if (branch.kind === "local") locals.push(row);
3521
+ else remotes.push(row);
3522
+ }
3523
+ const byName = (left, right) => left.name.localeCompare(right.name);
3524
+ locals.sort((left, right) => Number(right.current) - Number(left.current) || byName(left, right));
3525
+ remotes.sort(byName);
3526
+ return {
3527
+ locals,
3528
+ remotes
3529
+ };
3462
3530
  }
3463
- //#endregion
3464
- //#region src/client/worktree-remove-flow.ts
3465
3531
  /**
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.
3532
+ * Nest remote-only branches under their remote (`origin/feat/x` lands in
3533
+ * the `origin` subtree as `feat/x`) — the IDEA-style two-level 远程 tree.
3534
+ * A bare ref directly under refs/remotes (no `/`) is skipped: the branch
3535
+ * list never offers it.
3472
3536
  */
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;
3537
+ function groupRemotes(remotes) {
3538
+ const groups = /* @__PURE__ */ new Map();
3539
+ for (const row of remotes) {
3540
+ const slash = row.name.indexOf("/");
3541
+ if (slash <= 0 || slash === row.name.length - 1) continue;
3542
+ const remote = row.name.slice(0, slash);
3543
+ const short = row.name.slice(slash + 1);
3544
+ const list = groups.get(remote) ?? [];
3545
+ list.push({
3546
+ ...row,
3547
+ short
3548
+ });
3549
+ groups.set(remote, list);
3478
3550
  }
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);
3551
+ return [...groups.entries()].map(([remote, branches]) => ({
3552
+ remote,
3553
+ branches
3554
+ })).sort((left, right) => left.remote.localeCompare(right.remote));
3483
3555
  }
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(/\/+$/, "");
3556
+ /** Every group key the branch tree renders — the collapse-all target set.
3557
+ * The bare `remote` key folds the whole remote tree; the nested
3558
+ * `remote:<name>` keys matter only for the groups it re-opens. */
3559
+ function branchGroupKeys(remotes) {
3560
+ return [
3561
+ "local",
3562
+ "remote",
3563
+ ...groupRemotes(remotes).map((group) => `remote:${group.remote}`)
3564
+ ];
3492
3565
  }
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. */
3566
+ //#endregion
3567
+ //#region \0git-worktree-css:/home/runner/work/dsh-git-branch/dsh-git-branch/src/client/GitHistoryPanel.module.css?inline
3568
+ 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/* The page column below the topbar is a definite-height flex column and the\n * three cards split it 2:1:2 (uncommitted : branches : history) — five\n * shares, the topbar and gaps taking their natural height first. Each body\n * scrolls INSIDE its share — min-height: 0 is what lets a flex child shrink\n * below its content (the old natural-height model painted overflowing rows\n * over the sections below). A floor keeps a very short panel from\n * collapsing a card to its bare head; past the floor the column itself\n * scrolls. */\n.section_427795137 {\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 min-height: 0;\n}\n\n.sectionFill_427795137 {\n flex: 1 1 0;\n min-height: 84px;\n}\n\n/* The three flex-grow overrides of the 2:1:2 split. */\n.sectionChanges_427795137 {\n flex-grow: 2;\n}\n\n.sectionBranches_427795137 {\n flex-grow: 1;\n}\n\n.sectionHistory_427795137 {\n flex-grow: 2;\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 flex: 1 1 auto;\n min-height: 0;\n}\n\n/* The branch and history bodies scroll as a whole inside their share. */\n.sectionBodyScroll_427795137 {\n overflow-y: auto;\n overscroll-behavior: contain;\n}\n\n/* Inside the uncommitted card only the file lists scroll; the commit box\n * below stays pinned however long the change tree is. */\n.changeList_427795137 {\n display: flex;\n flex-direction: column;\n gap: 8px;\n flex: 1 1 auto;\n min-height: 0;\n overflow-y: auto;\n overscroll-behavior: contain;\n}\n\n/* The 全部展开/全部折叠 text chips in the branch card head. */\n.treeToggle_427795137 {\n background: none;\n border: 0;\n padding: 2px 5px;\n font: inherit;\n font-size: 11.5px;\n line-height: 1.4;\n color: var(--dsw-alias-label-secondary);\n cursor: pointer;\n border-radius: 5px;\n white-space: nowrap;\n}\n\n.treeToggle_427795137:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n color: var(--dsw-alias-brand-primary);\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 flex: none;\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 /* The action menu opens on RIGHT-CLICK (Enter/Space for keyboard) — an\n * arrow, not the pointing hand a left-click affordance would promise. */\n cursor: default;\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/* Whole-row highlight for the current branch: the ✓ alone read as too\n * quiet inside the tree. A translucent brand-blue wash plus the inset left\n * accent and a colored name survives both themes; #4c6bf5 is the same\n * hardcoded blue actPrimary uses (the brand token does not resolve\n * reliably inside the better-sidebar panel). */\n.branchRowCurrent_427795137 {\n background: rgba(76, 107, 245, 0.16);\n box-shadow: inset 2px 0 0 #4c6bf5;\n cursor: default;\n}\n\n.branchRowCurrent_427795137:hover {\n background: rgba(76, 107, 245, 0.16);\n}\n\n.branchRowCurrent_427795137 .branchName_427795137 {\n color: #4c6bf5;\n font-weight: 600;\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/* The branch card's head variant: sits right of the title and grows toward\n * the tools instead of being right-aligned by `margin-left: auto`. The\n * overrides win because both selectors are single classes and this one is\n * declared later. */\n.searchBoxHead_427795137 {\n margin-left: 0;\n flex: 1 1 auto;\n max-width: 320px;\n min-width: 90px;\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/* A small plain dot on the rail — the old 10px dot with a fat card-colored\n * ring read as a heavy black blob. #4c6bf5 for the same reason actPrimary\n * hardcodes it (the brand token does not resolve reliably in this panel). */\n.commitDot_427795137 {\n position: absolute;\n left: 8px;\n top: 50%;\n transform: translateY(-50%);\n width: 6px;\n height: 6px;\n border-radius: 50%;\n background: #4c6bf5;\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.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";
3569
+ const tagId$2 = "dsh-git-branch-manage/GitHistoryPanel.module.css";
3570
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=\"" + tagId$2 + "\"]") === null) {
3571
+ const tag = document.createElement("style");
3572
+ tag.dataset.plugin = "dsh-git-branch-manage";
3573
+ tag.dataset.pluginCss = tagId$2;
3574
+ tag.textContent = css$2;
3575
+ document.head.appendChild(tag);
3576
+ }
3577
+ var GitHistoryPanel_module_default = {
3578
+ "page": "page_427795137",
3579
+ "topbar": "topbar_427795137",
3580
+ "back": "back_427795137",
3581
+ "title": "title_427795137",
3582
+ "repoPill": "repoPill_427795137",
3583
+ "repoName": "repoName_427795137",
3584
+ "branchName": "branchName_427795137",
3585
+ "track": "track_427795137",
3586
+ "topbarActions": "topbarActions_427795137",
3587
+ "refreshBtn": "refreshBtn_427795137",
3588
+ "scroll": "scroll_427795137",
3589
+ "section": "section_427795137",
3590
+ "sectionFill": "sectionFill_427795137",
3591
+ "sectionChanges": "sectionChanges_427795137",
3592
+ "sectionBranches": "sectionBranches_427795137",
3593
+ "sectionHistory": "sectionHistory_427795137",
3594
+ "sectionHead": "sectionHead_427795137",
3595
+ "sectionTitle": "sectionTitle_427795137",
3596
+ "countBadge": "countBadge_427795137",
3597
+ "sectionTools": "sectionTools_427795137",
3598
+ "sectionBody": "sectionBody_427795137",
3599
+ "sectionBodyScroll": "sectionBodyScroll_427795137",
3600
+ "changeList": "changeList_427795137",
3601
+ "treeToggle": "treeToggle_427795137",
3602
+ "act": "act_427795137",
3603
+ "actPrimary": "actPrimary_427795137",
3604
+ "groupTitleRow": "groupTitleRow_427795137",
3605
+ "linkBtn": "linkBtn_427795137",
3606
+ "changeRow": "changeRow_427795137",
3607
+ "changeCheck": "changeCheck_427795137",
3608
+ "statusBadge": "statusBadge_427795137",
3609
+ "statusA": "statusA_427795137",
3610
+ "statusM": "statusM_427795137",
3611
+ "statusD": "statusD_427795137",
3612
+ "statusR": "statusR_427795137",
3613
+ "statusC": "statusC_427795137",
3614
+ "statusT": "statusT_427795137",
3615
+ "statusQ": "statusQ_427795137",
3616
+ "statusU": "statusU_427795137",
3617
+ "changePath": "changePath_427795137",
3618
+ "stageHint": "stageHint_427795137",
3619
+ "commitBox": "commitBox_427795137",
3620
+ "commitInput": "commitInput_427795137",
3621
+ "commitActions": "commitActions_427795137",
3622
+ "commitHint": "commitHint_427795137",
3623
+ "split": "split_427795137",
3624
+ "splitMain": "splitMain_427795137",
3625
+ "splitCaret": "splitCaret_427795137",
3626
+ "menu": "menu_427795137",
3627
+ "menuItem": "menuItem_427795137",
3628
+ "menuSub": "menuSub_427795137",
3629
+ "treeHead": "treeHead_427795137",
3630
+ "treeChevron": "treeChevron_427795137",
3631
+ "treeChildren": "treeChildren_427795137",
3632
+ "treeHeadNest": "treeHeadNest_427795137",
3633
+ "treeNest": "treeNest_427795137",
3634
+ "branchRow": "branchRow_427795137",
3635
+ "branchRowCurrent": "branchRowCurrent_427795137",
3636
+ "branchMark": "branchMark_427795137",
3637
+ "branchKind": "branchKind_427795137",
3638
+ "branchTrack": "branchTrack_427795137",
3639
+ "floatMenu": "floatMenu_427795137",
3640
+ "menuHead": "menuHead_427795137",
3641
+ "menuHeadStrong": "menuHeadStrong_427795137",
3642
+ "menuDanger": "menuDanger_427795137",
3643
+ "formField": "formField_427795137",
3644
+ "formLabel": "formLabel_427795137",
3645
+ "formInput": "formInput_427795137",
3646
+ "formError": "formError_427795137",
3647
+ "historyHead": "historyHead_427795137",
3648
+ "searchBox": "searchBox_427795137",
3649
+ "searchBoxHead": "searchBoxHead_427795137",
3650
+ "searchInput": "searchInput_427795137",
3651
+ "commitRow": "commitRow_427795137",
3652
+ "commitRowOpen": "commitRowOpen_427795137",
3653
+ "commitGraph": "commitGraph_427795137",
3654
+ "commitRowFirst": "commitRowFirst_427795137",
3655
+ "commitRowLast": "commitRowLast_427795137",
3656
+ "commitDot": "commitDot_427795137",
3657
+ "commitDotMerge": "commitDotMerge_427795137",
3658
+ "commitMain": "commitMain_427795137",
3659
+ "commitSubject": "commitSubject_427795137",
3660
+ "commitSubjectText": "commitSubjectText_427795137",
3661
+ "refBadges": "refBadges_427795137",
3662
+ "refBadge": "refBadge_427795137",
3663
+ "refHead": "refHead_427795137",
3664
+ "refTag": "refTag_427795137",
3665
+ "refRemote": "refRemote_427795137",
3666
+ "refLocal": "refLocal_427795137",
3667
+ "commitMeta": "commitMeta_427795137",
3668
+ "commitSide": "commitSide_427795137",
3669
+ "commitHash": "commitHash_427795137",
3670
+ "commitExpand": "commitExpand_427795137",
3671
+ "commitExpandTitle": "commitExpandTitle_427795137",
3672
+ "fileRow": "fileRow_427795137",
3673
+ "filePath": "filePath_427795137",
3674
+ "fileNums": "fileNums_427795137",
3675
+ "fileAdd": "fileAdd_427795137",
3676
+ "fileDel": "fileDel_427795137",
3677
+ "empty": "empty_427795137",
3678
+ "errorText": "errorText_427795137",
3679
+ "moreRow": "moreRow_427795137"
3680
+ };
3681
+ //#endregion
3682
+ //#region src/client/panel/BranchSection.tsx
3683
+ /**
3684
+ * BranchSection: the panel's middle block — the branch list with an IDEA-style
3685
+ * action menu. RIGHT-CLICKING any row opens a small menu anchored to it
3686
+ * (checkout, new branch from here, rename, delete, copy name — remote rows
3687
+ * narrow it to the actions a remote branch can take); the CURRENT branch's
3688
+ * menu hides only checkout and delete, so rename/new-from/copy stay
3689
+ * reachable. Keyboard rows keep Enter/Space as the menu opener — a context
3690
+ * menu has no keyboard twin. Rename and create run through the Modal
3691
+ * primitive with the same client-side name validation the composer chip
3692
+ * applies; delete confirms first and always uses the safe `-d` form (git
3693
+ * refuses unmerged). Presentational: every action arrives as a callback; the
3694
+ * menu/dialog state is local and resets when the branch list identity
3695
+ * changes.
3696
+ *
3697
+ * The tree opens on 本地 only (the current branch lives there) and the card
3698
+ * takes one share of the panel's fixed 2:1:2 split (its body scrolls inside
3699
+ * it), so a branch-heavy repository can no longer push the other cards off
3700
+ * screen. 全部展开/全部折叠 drive every group at once, and after a checkout
3701
+ * (or expand-all) the card re-centers on the current branch row.
3702
+ *
3703
+ * The head carries a search box: it filters both trees by branch name
3704
+ * (case-insensitive substring), and while a query is active every matching
3705
+ * group renders expanded with its count following the filter — the folded
3706
+ * state the user set is untouched and comes back when the query clears.
3707
+ *
3708
+ * @module git-worktree/client/panel/BranchSection
3709
+ */
3710
+ /** Groups folded when the card mounts: the remote tree only. The current
3711
+ * branch lives at the TOP of the local group (buildBranchRows sorts it
3712
+ * first), so 本地 stays open — the card opens showing what the panel is
3713
+ * about, without the wall of remote branches. */
3714
+ const COLLAPSED_ON_MOUNT = /* @__PURE__ */ new Set(["remote"]);
3715
+ /** The action menu as a body portal, fixed-positioned next to the row. */
3716
+ function BranchFloatMenu(props) {
3717
+ const { state, t, busy, onAction, onClose } = props;
3718
+ const { branch } = state;
3719
+ const ref = (0, react.useRef)(null);
3720
+ (0, react.useEffect)(() => {
3721
+ const onPointerDown = (event) => {
3722
+ if (ref.current !== null && !ref.current.contains(event.target)) onClose();
3723
+ };
3724
+ const onKeyDown = (event) => {
3725
+ if (event.key === "Escape") onClose();
3726
+ };
3727
+ document.addEventListener("pointerdown", onPointerDown, true);
3728
+ document.addEventListener("keydown", onKeyDown);
3729
+ return () => {
3730
+ document.removeEventListener("pointerdown", onPointerDown, true);
3731
+ document.removeEventListener("keydown", onKeyDown);
3732
+ };
3733
+ }, [onClose]);
3734
+ (0, react.useLayoutEffect)(() => {
3735
+ const el = ref.current;
3736
+ if (el === null) return;
3737
+ const rect = el.getBoundingClientRect();
3738
+ const left = Math.max(8, Math.min(state.rect.left, window.innerWidth - rect.width - 8));
3739
+ let top = state.rect.top;
3740
+ if (top + rect.height > window.innerHeight - 8) top = Math.max(8, state.rect.top - rect.height - 8);
3741
+ el.style.left = `${String(Math.round(left))}px`;
3742
+ el.style.top = `${String(Math.round(top))}px`;
3743
+ }, [state]);
3744
+ return (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3745
+ ref,
3746
+ className: `${GitHistoryPanel_module_default.menu} ${GitHistoryPanel_module_default.floatMenu}`,
3747
+ role: "menu",
3748
+ children: [
3749
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3750
+ className: GitHistoryPanel_module_default.menuHead,
3751
+ children: [
3752
+ t("panel.branches"),
3753
+ " · ",
3754
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3755
+ className: GitHistoryPanel_module_default.menuHeadStrong,
3756
+ children: branch.name
3757
+ }),
3758
+ branch.current ? ` · ${t("panel.branchCurrent")}` : ""
3759
+ ]
3760
+ }),
3761
+ !branch.current ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3762
+ type: "button",
3763
+ role: "menuitem",
3764
+ className: GitHistoryPanel_module_default.menuItem,
3765
+ disabled: busy,
3766
+ onClick: () => onAction("checkout"),
3767
+ children: branch.kind === "remote" ? t("panel.branchCheckoutRemote") : t("panel.branchCheckout")
3768
+ }) : null,
3769
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3770
+ type: "button",
3771
+ role: "menuitem",
3772
+ className: GitHistoryPanel_module_default.menuItem,
3773
+ disabled: busy,
3774
+ onClick: () => onAction("new-from"),
3775
+ children: t("panel.branchNewFrom")
3776
+ }),
3777
+ branch.kind === "local" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3778
+ type: "button",
3779
+ role: "menuitem",
3780
+ className: GitHistoryPanel_module_default.menuItem,
3781
+ disabled: busy,
3782
+ onClick: () => onAction("rename"),
3783
+ children: t("panel.branchRename")
3784
+ }) : null,
3785
+ branch.kind === "local" && !branch.current ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3786
+ type: "button",
3787
+ role: "menuitem",
3788
+ className: `${GitHistoryPanel_module_default.menuItem} ${GitHistoryPanel_module_default.menuDanger}`,
3789
+ disabled: busy,
3790
+ onClick: () => onAction("delete"),
3791
+ children: t("panel.branchDelete")
3792
+ }) : null,
3793
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3794
+ type: "button",
3795
+ role: "menuitem",
3796
+ className: GitHistoryPanel_module_default.menuItem,
3797
+ disabled: busy,
3798
+ onClick: () => onAction("copy"),
3799
+ children: t("panel.branchCopy")
3800
+ })
3801
+ ]
3802
+ }), document.body);
3803
+ }
3804
+ /** The branches section. */
3805
+ function BranchSection(props) {
3806
+ const { t, locals, remotes, busy, onCheckout, onRename, onDelete, onCreate } = props;
3807
+ const [menu, setMenu] = (0, react.useState)(null);
3808
+ const [dialog, setDialog] = (0, react.useState)(null);
3809
+ const [draft, setDraft] = (0, react.useState)("");
3810
+ const [query, setQuery] = (0, react.useState)("");
3811
+ const [collapsedGroups, setCollapsedGroups] = (0, react.useState)(COLLAPSED_ON_MOUNT);
3812
+ /** The card's scroll body — the locate math centers rows inside it. */
3813
+ const bodyRef = (0, react.useRef)(null);
3814
+ /** The current branch this mount last centered on. A refresh re-centers
3815
+ * only when the checkout moved; a same-branch refresh (stage, commit…)
3816
+ * keeps the scroll where the user left it. */
3817
+ const locatedRef = (0, react.useRef)(null);
3818
+ /** Expand-all asks for one more locate — its rows mount on the next commit. */
3819
+ const pendingLocateRef = (0, react.useRef)(false);
3820
+ const toggleGroup = (key) => {
3821
+ setCollapsedGroups((prev) => {
3822
+ const next = new Set(prev);
3823
+ if (next.has(key)) next.delete(key);
3824
+ else next.add(key);
3825
+ return next;
3826
+ });
3827
+ };
3828
+ const expandAll = () => {
3829
+ pendingLocateRef.current = true;
3830
+ setCollapsedGroups(/* @__PURE__ */ new Set());
3831
+ };
3832
+ const collapseAll = () => {
3833
+ setCollapsedGroups(new Set(branchGroupKeys(remotes)));
3834
+ };
3835
+ /** Center the current branch row in the card body (no scroll when the
3836
+ * body is shorter than its cap). Rect arithmetic, not offsetTop: no
3837
+ * positioned ancestor is guaranteed between the row and the body. */
3838
+ const locateCurrent = () => {
3839
+ const body = bodyRef.current;
3840
+ if (body === null) return;
3841
+ const row = body.querySelector("[data-current=\"true\"]");
3842
+ if (!(row instanceof HTMLElement)) return;
3843
+ const top = row.getBoundingClientRect().top - body.getBoundingClientRect().top + body.scrollTop;
3844
+ body.scrollTop = Math.max(0, top - (body.clientHeight - row.offsetHeight) / 2);
3845
+ };
3846
+ (0, react.useLayoutEffect)(() => {
3847
+ const current = locals.find((row) => row.current)?.name;
3848
+ if (current === void 0) return;
3849
+ if (!pendingLocateRef.current && current === locatedRef.current) return;
3850
+ pendingLocateRef.current = false;
3851
+ locatedRef.current = current;
3852
+ locateCurrent();
3853
+ });
3854
+ const total = locals.length + remotes.length;
3855
+ /** The head search: name-substring rows only. A live query opens every
3856
+ * group whatever the folded state is (a hidden match reads as a bug),
3857
+ * without touching that state — clearing the query restores it. */
3858
+ const searching = query.trim() !== "";
3859
+ const visibleLocals = filterBranches(locals, query);
3860
+ const visibleRemotes = filterBranches(remotes, query);
3861
+ const openGroup = (key) => searching || !collapsedGroups.has(key);
3862
+ const matched = visibleLocals.length + visibleRemotes.length;
3863
+ const openMenu = (branch, event) => {
3864
+ const rect = event.currentTarget.getBoundingClientRect();
3865
+ setMenu({
3866
+ branch,
3867
+ rect: {
3868
+ left: rect.left,
3869
+ top: rect.bottom + 5
3870
+ }
3871
+ });
3872
+ };
3873
+ const openDialog = (kind, branch) => {
3874
+ setMenu(null);
3875
+ setDraft(kind === "rename" ? branch?.name ?? "" : "");
3876
+ setDialog({
3877
+ kind,
3878
+ ...branch === void 0 ? {} : { branch }
3879
+ });
3880
+ };
3881
+ const menuAction = (action) => {
3882
+ const state = menu;
3883
+ if (state === null) return;
3884
+ if (action === "checkout") {
3885
+ setMenu(null);
3886
+ onCheckout(state.branch);
3887
+ return;
3888
+ }
3889
+ if (action === "copy") {
3890
+ setMenu(null);
3891
+ navigator.clipboard.writeText(state.branch.name).catch(() => {});
3892
+ return;
3893
+ }
3894
+ openDialog(action, state.branch);
3895
+ };
3896
+ const draftIssue = dialog !== null && dialog.kind !== "delete" ? branchNameIssue(draft) : void 0;
3897
+ const duplicateIssue = dialog !== null && dialog.kind !== "delete" && draftIssue === void 0 && [...locals, ...remotes].some((row) => row.kind === "local" && row.name === draft) ? t("menuNewBranchExists") : void 0;
3898
+ const inputError = draftIssue ?? duplicateIssue;
3899
+ const submitDialog = () => {
3900
+ if (dialog === null || busy) return;
3901
+ if (dialog.kind === "delete") {
3902
+ if (dialog.branch !== void 0) onDelete(dialog.branch);
3903
+ setDialog(null);
3904
+ return;
3905
+ }
3906
+ if (draft === "" || inputError !== void 0) return;
3907
+ if (dialog.kind === "rename" && dialog.branch !== void 0) onRename(dialog.branch, draft);
3908
+ else if (dialog.kind === "new") onCreate(draft);
3909
+ else if (dialog.kind === "new-from" && dialog.branch !== void 0) onCreate(draft, dialog.branch.name);
3910
+ setDialog(null);
3911
+ };
3912
+ 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");
3913
+ const groupHead = (key, label, count, nested) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3914
+ className: `${GitHistoryPanel_module_default.treeHead} ${nested ? GitHistoryPanel_module_default.treeHeadNest : ""}`,
3915
+ role: "button",
3916
+ tabIndex: 0,
3917
+ "aria-expanded": openGroup(key),
3918
+ onClick: () => toggleGroup(key),
3919
+ onKeyDown: (event) => {
3920
+ if (event.key === "Enter" || event.key === " ") {
3921
+ event.preventDefault();
3922
+ toggleGroup(key);
3923
+ }
3924
+ },
3925
+ children: [
3926
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3927
+ className: GitHistoryPanel_module_default.treeChevron,
3928
+ children: openGroup(key) ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, { size: 12 }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronRightOutlineRegular, { size: 12 })
3929
+ }),
3930
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: label }),
3931
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3932
+ className: GitHistoryPanel_module_default.countBadge,
3933
+ children: String(count)
3934
+ })
3935
+ ]
3936
+ });
3937
+ const renderBranchRow = (row, displayName) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3938
+ className: `${GitHistoryPanel_module_default.branchRow} ${row.current ? GitHistoryPanel_module_default.branchRowCurrent : ""}`,
3939
+ "data-current": row.current ? "true" : void 0,
3940
+ role: "button",
3941
+ tabIndex: 0,
3942
+ onContextMenu: (event) => {
3943
+ event.preventDefault();
3944
+ openMenu(row, event);
3945
+ },
3946
+ onKeyDown: (event) => {
3947
+ if (event.key === "Enter" || event.key === " ") {
3948
+ event.preventDefault();
3949
+ openMenu(row, event);
3950
+ }
3951
+ },
3952
+ children: [
3953
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3954
+ className: GitHistoryPanel_module_default.branchMark,
3955
+ children: row.current ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCheckOutlineRegular, { size: 13 }) : "⎇"
3956
+ }),
3957
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3958
+ className: GitHistoryPanel_module_default.branchName,
3959
+ title: row.name,
3960
+ children: displayName ?? row.name
3961
+ }),
3962
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3963
+ className: GitHistoryPanel_module_default.branchTrack,
3964
+ children: row.ahead !== void 0 || row.behind !== void 0 ? `↓${String(row.behind ?? 0)} ↑${String(row.ahead ?? 0)}` : ""
3965
+ })
3966
+ ]
3967
+ }, row.name);
3968
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
3969
+ className: `${GitHistoryPanel_module_default.section} ${GitHistoryPanel_module_default.sectionFill} ${GitHistoryPanel_module_default.sectionBranches}`,
3970
+ "aria-label": t("panel.branches"),
3971
+ children: [
3972
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3973
+ className: GitHistoryPanel_module_default.sectionHead,
3974
+ children: [
3975
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
3976
+ className: GitHistoryPanel_module_default.sectionTitle,
3977
+ children: t("panel.branches")
3978
+ }),
3979
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3980
+ className: `${GitHistoryPanel_module_default.searchBox} ${GitHistoryPanel_module_default.searchBoxHead}`,
3981
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3982
+ "aria-hidden": "true",
3983
+ children: "⌕"
3984
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
3985
+ type: "text",
3986
+ className: GitHistoryPanel_module_default.searchInput,
3987
+ value: query,
3988
+ placeholder: t("menuSearchPlaceholder"),
3989
+ "aria-label": t("menuSearchPlaceholder"),
3990
+ onChange: (event) => setQuery(event.target.value)
3991
+ })]
3992
+ }),
3993
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3994
+ className: GitHistoryPanel_module_default.sectionTools,
3995
+ children: [
3996
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3997
+ type: "button",
3998
+ className: GitHistoryPanel_module_default.treeToggle,
3999
+ onClick: expandAll,
4000
+ children: t("menuExpandAll")
4001
+ }),
4002
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4003
+ type: "button",
4004
+ className: GitHistoryPanel_module_default.treeToggle,
4005
+ onClick: collapseAll,
4006
+ children: t("menuCollapseAll")
4007
+ }),
4008
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
4009
+ type: "button",
4010
+ className: GitHistoryPanel_module_default.act,
4011
+ disabled: busy,
4012
+ onClick: () => openDialog("new"),
4013
+ children: ["+ ", t("panel.branchNew")]
4014
+ })
4015
+ ]
4016
+ })
4017
+ ]
4018
+ }),
4019
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4020
+ className: `${GitHistoryPanel_module_default.sectionBody} ${GitHistoryPanel_module_default.sectionBodyScroll}`,
4021
+ ref: bodyRef,
4022
+ children: [
4023
+ total === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
4024
+ className: GitHistoryPanel_module_default.empty,
4025
+ children: t("menuNoBranches")
4026
+ }) : null,
4027
+ total > 0 && matched === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
4028
+ className: GitHistoryPanel_module_default.empty,
4029
+ children: t("menuNoMatches")
4030
+ }) : null,
4031
+ visibleLocals.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [groupHead("local", t("panel.branchLocals"), visibleLocals.length, false), openGroup("local") ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4032
+ className: GitHistoryPanel_module_default.treeChildren,
4033
+ children: visibleLocals.map((row) => renderBranchRow(row))
4034
+ }) : null] }) : null,
4035
+ visibleRemotes.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [groupHead("remote", t("panel.branchRemotes"), visibleRemotes.length, false), openGroup("remote") ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4036
+ className: GitHistoryPanel_module_default.treeNest,
4037
+ children: groupRemotes(visibleRemotes).map((group) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [groupHead(`remote:${group.remote}`, group.remote, group.branches.length, true), openGroup(`remote:${group.remote}`) ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4038
+ className: GitHistoryPanel_module_default.treeChildren,
4039
+ children: group.branches.map((row) => renderBranchRow(row, row.short))
4040
+ }) : null] }, group.remote))
4041
+ }) : null] }) : null
4042
+ ]
4043
+ }),
4044
+ menu !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BranchFloatMenu, {
4045
+ state: menu,
4046
+ t,
4047
+ busy,
4048
+ onAction: menuAction,
4049
+ onClose: () => setMenu(null)
4050
+ }) : null,
4051
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
4052
+ open: dialog !== null && dialog.kind !== "delete",
4053
+ onClose: () => setDialog(null),
4054
+ title: dialogTitle,
4055
+ closeLabel: t("close"),
4056
+ 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, {
4057
+ size: "sm",
4058
+ variant: "ghost",
4059
+ onClick: () => setDialog(null),
4060
+ children: t("cancel")
4061
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
4062
+ size: "sm",
4063
+ variant: "primary",
4064
+ disabled: busy || draft === "" || inputError !== void 0,
4065
+ onClick: submitDialog,
4066
+ children: t("actionConfirm")
4067
+ })] }),
4068
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4069
+ className: GitHistoryPanel_module_default.formField,
4070
+ children: [
4071
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
4072
+ className: GitHistoryPanel_module_default.formLabel,
4073
+ htmlFor: "git-panel-branch-name",
4074
+ children: t("menuNewBranchPlaceholder")
4075
+ }),
4076
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
4077
+ id: "git-panel-branch-name",
4078
+ className: GitHistoryPanel_module_default.formInput,
4079
+ value: draft,
4080
+ "data-modal-autofocus": true,
4081
+ onChange: (event) => setDraft(event.target.value),
4082
+ onKeyDown: (event) => {
4083
+ if (event.key === "Enter") submitDialog();
4084
+ }
4085
+ }),
4086
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4087
+ className: GitHistoryPanel_module_default.formError,
4088
+ children: inputError ?? ""
4089
+ })
4090
+ ]
4091
+ })
4092
+ }),
4093
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
4094
+ open: dialog !== null && dialog.kind === "delete" && dialog.branch !== void 0,
4095
+ onClose: () => setDialog(null),
4096
+ title: t("ctxDelete"),
4097
+ closeLabel: t("close"),
4098
+ 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, {
4099
+ size: "sm",
4100
+ variant: "ghost",
4101
+ onClick: () => setDialog(null),
4102
+ children: t("cancel")
4103
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
4104
+ size: "sm",
4105
+ variant: "primary",
4106
+ disabled: busy,
4107
+ onClick: submitDialog,
4108
+ children: t("actionConfirm")
4109
+ })] }),
4110
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
4111
+ className: GitHistoryPanel_module_default.formLabel,
4112
+ children: dialog?.branch !== void 0 ? t("deleteBranchAsk", { branch: dialog.branch.name }) : ""
4113
+ })
4114
+ })
4115
+ ]
4116
+ });
4117
+ }
4118
+ //#endregion
4119
+ //#region src/client/panel/HistorySection.tsx
4120
+ /**
4121
+ * HistorySection: the panel's third block — the commit list. One row shows
4122
+ * the subject, its refs badges, the author and the relative time, and the
4123
+ * file count with the short hash; hovering a row raises the framework
4124
+ * Tooltip (subject · author <email> · absolute time · hash); CLICKING a row
4125
+ * expands the commit's changed files INLINE below it (exactly one open at a
4126
+ * time), loading them on first open. The search input filters the loaded
4127
+ * page client-side; "load more" fetches the next page.
4128
+ *
4129
+ * @module git-worktree/client/panel/HistorySection
4130
+ */
4131
+ function statusTitle$1(status, t) {
4132
+ if (status === "A") return t("panel.stAdd");
4133
+ if (status === "D") return t("panel.stDel");
4134
+ if (status === "R") return t("panel.stRen");
4135
+ if (status === "C") return t("panel.stCopy");
4136
+ if (status === "T") return t("panel.stType");
4137
+ return t("panel.stMod");
4138
+ }
4139
+ function FileRow(props) {
4140
+ const { file, t } = props;
4141
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4142
+ className: GitHistoryPanel_module_default.fileRow,
4143
+ children: [
4144
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4145
+ className: `${GitHistoryPanel_module_default.statusBadge} ${GitHistoryPanel_module_default[`status${file.status}`]}`,
4146
+ title: statusTitle$1(file.status, t),
4147
+ children: file.status
4148
+ }),
4149
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4150
+ className: GitHistoryPanel_module_default.filePath,
4151
+ title: file.oldPath === void 0 ? file.path : `${file.oldPath} → ${file.path}`,
4152
+ children: file.oldPath === void 0 ? file.path : `${file.oldPath} → ${file.path}`
4153
+ }),
4154
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4155
+ className: GitHistoryPanel_module_default.fileNums,
4156
+ 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: [
4157
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("b", {
4158
+ className: GitHistoryPanel_module_default.fileAdd,
4159
+ children: `+${String(file.insertions ?? 0)}`
4160
+ }),
4161
+ " ",
4162
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("b", {
4163
+ className: GitHistoryPanel_module_default.fileDel,
4164
+ children: `-${String(file.deletions ?? 0)}`
4165
+ })
4166
+ ] }) : ""
4167
+ })
4168
+ ]
4169
+ });
4170
+ }
4171
+ /** One history row; `ref` and the tooltip's hover handlers arrive through
4172
+ * the anchor share when the row is wrapped in a Tooltip. */
4173
+ const CommitRow = (0, react.forwardRef)(function CommitRow(props, ref) {
4174
+ const { commit, first, last, open, files, t, onToggle, timeLabelFor } = props;
4175
+ const badges = classifyRefs(commit.refs);
4176
+ const when = new Date(commit.date);
4177
+ const absolute = Number.isFinite(commit.date) && commit.date > 0 ? `${when.toLocaleDateString()} ${when.toLocaleTimeString()}` : "";
4178
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4179
+ ref,
4180
+ className: [
4181
+ GitHistoryPanel_module_default.commitRow,
4182
+ first ? GitHistoryPanel_module_default.commitRowFirst : "",
4183
+ last ? GitHistoryPanel_module_default.commitRowLast : "",
4184
+ open ? GitHistoryPanel_module_default.commitRowOpen : ""
4185
+ ].filter((name) => name !== "").join(" "),
4186
+ role: "button",
4187
+ tabIndex: 0,
4188
+ "aria-expanded": open,
4189
+ "aria-describedby": props["aria-describedby"],
4190
+ onMouseEnter: props.onMouseEnter,
4191
+ onMouseLeave: props.onMouseLeave,
4192
+ onFocus: props.onFocus,
4193
+ onBlur: props.onBlur,
4194
+ onClick: () => onToggle(commit.hash),
4195
+ onKeyDown: (event) => {
4196
+ if (event.key === "Enter" || event.key === " ") {
4197
+ event.preventDefault();
4198
+ onToggle(commit.hash);
4199
+ }
4200
+ },
4201
+ children: [
4202
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4203
+ className: GitHistoryPanel_module_default.commitGraph,
4204
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: `${GitHistoryPanel_module_default.commitDot} ${commit.parents.length > 1 ? GitHistoryPanel_module_default.commitDotMerge : ""}` })
4205
+ }),
4206
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4207
+ className: GitHistoryPanel_module_default.commitMain,
4208
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4209
+ className: GitHistoryPanel_module_default.commitSubject,
4210
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4211
+ className: GitHistoryPanel_module_default.commitSubjectText,
4212
+ children: commit.subject
4213
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4214
+ className: GitHistoryPanel_module_default.refBadges,
4215
+ children: badges.map((badge) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4216
+ className: `${GitHistoryPanel_module_default.refBadge} ${GitHistoryPanel_module_default[`ref${badge.kind[0]?.toUpperCase() ?? ""}${badge.kind.slice(1)}`]}`,
4217
+ children: badge.text
4218
+ }, `${badge.kind}:${badge.text}`))
4219
+ })]
4220
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4221
+ className: GitHistoryPanel_module_default.commitMeta,
4222
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: commit.authorName }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: timeLabelFor(commit.date) })]
4223
+ })]
4224
+ }),
4225
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4226
+ className: GitHistoryPanel_module_default.commitSide,
4227
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4228
+ className: GitHistoryPanel_module_default.commitHash,
4229
+ children: commit.short
4230
+ })
4231
+ }),
4232
+ open ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4233
+ className: GitHistoryPanel_module_default.commitExpand,
4234
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4235
+ className: GitHistoryPanel_module_default.commitExpandTitle,
4236
+ children: t("panel.filesTitle", {
4237
+ n: files?.kind === "loaded" ? files.files.length : 0,
4238
+ author: commit.authorName,
4239
+ date: absolute
4240
+ })
4241
+ }), files === void 0 || files.kind === "loading" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4242
+ className: GitHistoryPanel_module_default.commitExpandTitle,
4243
+ children: t("manager.loading")
4244
+ }) : files.kind === "failed" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4245
+ className: GitHistoryPanel_module_default.errorText,
4246
+ children: files.message
4247
+ }) : files.files.map((file) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FileRow, {
4248
+ file,
4249
+ t
4250
+ }, `${file.status}:${file.path}`))]
4251
+ }) : null
4252
+ ]
4253
+ });
4254
+ });
4255
+ /** Tooltip label resolver: one line spelling subject, author, time, hash. */
4256
+ function tooltipLabel(commit) {
4257
+ const when = new Date(commit.date);
4258
+ const absolute = Number.isFinite(commit.date) && commit.date > 0 ? `${when.toLocaleDateString()} ${when.toLocaleTimeString()}` : "";
4259
+ return `${commit.subject} · ${commit.authorName} <${commit.authorEmail}> · ${absolute} · ${commit.short}`;
4260
+ }
4261
+ function HistorySection(props) {
4262
+ const { t, commits, loaded, loadError, loading, hasMore, loadingMore, expanded, filesByHash, onLoadMore, onToggle, timeLabelFor } = props;
4263
+ const [query, setQuery] = (0, react.useState)("");
4264
+ const needle = query.trim().toLowerCase();
4265
+ const visible = filterCommits(commits, query);
4266
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
4267
+ className: `${GitHistoryPanel_module_default.section} ${GitHistoryPanel_module_default.sectionFill} ${GitHistoryPanel_module_default.sectionHistory}`,
4268
+ "aria-label": t("panel.history"),
4269
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4270
+ className: GitHistoryPanel_module_default.historyHead,
4271
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
4272
+ className: GitHistoryPanel_module_default.sectionTitle,
4273
+ children: t("panel.history")
4274
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4275
+ className: GitHistoryPanel_module_default.searchBox,
4276
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4277
+ "aria-hidden": "true",
4278
+ children: "⌕"
4279
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
4280
+ type: "text",
4281
+ className: GitHistoryPanel_module_default.searchInput,
4282
+ value: query,
4283
+ placeholder: t("panel.historySearch"),
4284
+ "aria-label": t("panel.historySearch"),
4285
+ onChange: (event) => setQuery(event.target.value)
4286
+ })]
4287
+ })]
4288
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4289
+ className: `${GitHistoryPanel_module_default.sectionBody} ${GitHistoryPanel_module_default.sectionBodyScroll}`,
4290
+ children: [loadError !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4291
+ className: GitHistoryPanel_module_default.errorText,
4292
+ children: loadError
4293
+ }) : !loaded && loading ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4294
+ className: GitHistoryPanel_module_default.empty,
4295
+ children: t("manager.loading")
4296
+ }) : visible.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4297
+ className: GitHistoryPanel_module_default.empty,
4298
+ children: needle === "" ? t("panel.noCommits") : t("panel.noMatches")
4299
+ }) : visible.map((commit, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
4300
+ label: () => tooltipLabel(commit),
4301
+ side: "bottom",
4302
+ portal: true,
4303
+ maxWidth: 380,
4304
+ delayMs: 250,
4305
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CommitRow, {
4306
+ commit,
4307
+ first: index === 0,
4308
+ last: index === visible.length - 1,
4309
+ open: expanded === commit.hash,
4310
+ files: filesByHash[commit.hash],
4311
+ t,
4312
+ onToggle,
4313
+ timeLabelFor
4314
+ })
4315
+ }, commit.hash)), hasMore && visible.length === commits.length ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4316
+ className: GitHistoryPanel_module_default.moreRow,
4317
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4318
+ type: "button",
4319
+ className: GitHistoryPanel_module_default.act,
4320
+ disabled: loadingMore,
4321
+ onClick: onLoadMore,
4322
+ children: t("panel.loadMore")
4323
+ })
4324
+ }) : null]
4325
+ })]
4326
+ });
4327
+ }
4328
+ //#endregion
4329
+ //#region src/client/panel/UncommittedSection.tsx
4330
+ /**
4331
+ * UncommittedSection: the panel's first block — the staged/unstaged file
4332
+ * groups, the commit box, and the sync tools. Presentational only: the rows,
4333
+ * the draft, and every action arrive as props from the panel state owner, so
4334
+ * this file renders and never talks to git itself.
4335
+ *
4336
+ * The card takes two shares of the panel's fixed 2:1:2 split: the file
4337
+ * lists scroll inside their own region while the commit box stays pinned
4338
+ * below — a 500-change tree must not bury the commit affordance, and a
4339
+ * clean tree must not float it up either.
4340
+ *
4341
+ * The commit affordance is a split button: the main face commits, the caret
4342
+ * opens the two op variants (commit; commit and push). The AI button fills
4343
+ * the draft from the host's one-shot llm route — it renders only when the
4344
+ * panel knows the session's model selection.
4345
+ *
4346
+ * @module git-worktree/client/panel/UncommittedSection
4347
+ */
4348
+ /** Status letter → its localized title. */
4349
+ function statusTitle(status, t) {
4350
+ if (status === "?") return t("panel.stUntrack");
4351
+ if (status === "U") return t("panel.stUnmerged");
4352
+ if (status === "A") return t("panel.stAdd");
4353
+ if (status === "D") return t("panel.stDel");
4354
+ if (status === "R") return t("panel.stRen");
4355
+ if (status === "C") return t("panel.stCopy");
4356
+ if (status === "T") return t("panel.stType");
4357
+ return t("panel.stMod");
4358
+ }
4359
+ /** One file row: checkbox = stage/unstage the path. */
4360
+ function ChangeRowView(props) {
4361
+ const { row, disabled, onToggle, t } = props;
4362
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
4363
+ className: GitHistoryPanel_module_default.changeRow,
4364
+ children: [
4365
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
4366
+ type: "checkbox",
4367
+ className: GitHistoryPanel_module_default.changeCheck,
4368
+ checked: row.staged,
4369
+ disabled,
4370
+ onChange: () => onToggle(row)
4371
+ }),
4372
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4373
+ className: `${GitHistoryPanel_module_default.statusBadge} ${GitHistoryPanel_module_default[`status${row.status === "?" ? "Q" : row.status}`]}`,
4374
+ title: statusTitle(row.status, t),
4375
+ children: row.status === "?" ? "?" : row.status
4376
+ }),
4377
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4378
+ className: GitHistoryPanel_module_default.changePath,
4379
+ title: row.oldPath === void 0 ? row.path : `${row.oldPath} → ${row.path}`,
4380
+ children: row.oldPath === void 0 ? row.path : `${row.oldPath} → ${row.path}`
4381
+ }),
4382
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4383
+ className: GitHistoryPanel_module_default.stageHint,
4384
+ children: row.staged ? t("panel.unstageHint") : t("panel.stageHint")
4385
+ })
4386
+ ]
4387
+ });
4388
+ }
4389
+ /** The uncommitted-changes section body. */
4390
+ function UncommittedSection(props) {
4391
+ const { t, staged, unstaged, busy, syncBusy, truncated, commitDraft, onCommitDraft, commitMenuOpen, onToggleCommitMenu, onCommit, onToggleRow, onStageAll, onUnstageAll, aiAvailable, aiBusy, onAiGenerate, onFetch, onUpdate, onPush } = props;
4392
+ const menuRef = (0, react.useRef)(null);
4393
+ (0, react.useEffect)(() => {
4394
+ if (!commitMenuOpen) return;
4395
+ const onPointerDown = (event) => {
4396
+ if (menuRef.current !== null && !menuRef.current.contains(event.target)) onToggleCommitMenu();
4397
+ };
4398
+ const onKeyDown = (event) => {
4399
+ if (event.key === "Escape") onToggleCommitMenu();
4400
+ };
4401
+ document.addEventListener("pointerdown", onPointerDown, true);
4402
+ document.addEventListener("keydown", onKeyDown);
4403
+ return () => {
4404
+ document.removeEventListener("pointerdown", onPointerDown, true);
4405
+ document.removeEventListener("keydown", onKeyDown);
4406
+ };
4407
+ }, [commitMenuOpen, onToggleCommitMenu]);
4408
+ const total = staged.length + unstaged.length;
4409
+ const canCommit = staged.length > 0 && commitDraft.trim() !== "";
4410
+ const hint = staged.length === 0 ? t("panel.commitNeedsStaged") : commitDraft.trim() === "" ? t("panel.commitNeedsMessage") : "";
4411
+ const aiReady = aiAvailable && total > 0;
4412
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
4413
+ className: `${GitHistoryPanel_module_default.section} ${GitHistoryPanel_module_default.sectionFill} ${GitHistoryPanel_module_default.sectionChanges}`,
4414
+ "aria-label": t("panel.uncommitted"),
4415
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4416
+ className: GitHistoryPanel_module_default.sectionHead,
4417
+ children: [
4418
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
4419
+ className: GitHistoryPanel_module_default.sectionTitle,
4420
+ children: t("panel.uncommitted")
4421
+ }),
4422
+ total > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4423
+ className: GitHistoryPanel_module_default.countBadge,
4424
+ children: String(total)
4425
+ }) : null,
4426
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4427
+ className: GitHistoryPanel_module_default.sectionTools,
4428
+ children: [
4429
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
4430
+ type: "button",
4431
+ className: GitHistoryPanel_module_default.act,
4432
+ disabled: syncBusy,
4433
+ onClick: onFetch,
4434
+ title: t("menuFetch"),
4435
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconDownloadOutlineMedium, { size: 13 }), t("menuFetch")]
4436
+ }),
4437
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
4438
+ type: "button",
4439
+ className: GitHistoryPanel_module_default.act,
4440
+ disabled: syncBusy,
4441
+ onClick: onUpdate,
4442
+ title: t("menuUpdate"),
4443
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconDownloadOutlineMedium, { size: 13 }), t("menuUpdate")]
4444
+ }),
4445
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
4446
+ type: "button",
4447
+ className: GitHistoryPanel_module_default.act,
4448
+ disabled: syncBusy,
4449
+ onClick: onPush,
4450
+ title: t("menuPush"),
4451
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconPaperPlaneOutlineMedium, { size: 13 }), t("menuPush")]
4452
+ })
4453
+ ]
4454
+ })
4455
+ ]
4456
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4457
+ className: GitHistoryPanel_module_default.sectionBody,
4458
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4459
+ className: GitHistoryPanel_module_default.changeList,
4460
+ children: [
4461
+ total === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
4462
+ className: GitHistoryPanel_module_default.empty,
4463
+ children: t("panel.clean")
4464
+ }) : null,
4465
+ truncated ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
4466
+ className: GitHistoryPanel_module_default.empty,
4467
+ children: t("panel.changesTruncated")
4468
+ }) : null,
4469
+ staged.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4470
+ className: GitHistoryPanel_module_default.groupTitleRow,
4471
+ children: [
4472
+ t("panel.staged"),
4473
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4474
+ className: GitHistoryPanel_module_default.countBadge,
4475
+ children: String(staged.length)
4476
+ }),
4477
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4478
+ type: "button",
4479
+ className: GitHistoryPanel_module_default.linkBtn,
4480
+ disabled: busy,
4481
+ onClick: onUnstageAll,
4482
+ children: t("panel.unstageAll")
4483
+ })
4484
+ ]
4485
+ }), staged.map((row) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChangeRowView, {
4486
+ row,
4487
+ disabled: busy,
4488
+ onToggle: onToggleRow,
4489
+ t
4490
+ }, `${row.staged}:${row.path}:${row.oldPath ?? ""}`))] }) : null,
4491
+ unstaged.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4492
+ className: GitHistoryPanel_module_default.groupTitleRow,
4493
+ children: [
4494
+ t("panel.unstaged"),
4495
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4496
+ className: GitHistoryPanel_module_default.countBadge,
4497
+ children: String(unstaged.length)
4498
+ }),
4499
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4500
+ type: "button",
4501
+ className: GitHistoryPanel_module_default.linkBtn,
4502
+ disabled: busy,
4503
+ onClick: onStageAll,
4504
+ children: t("panel.stageAll")
4505
+ })
4506
+ ]
4507
+ }), unstaged.map((row) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChangeRowView, {
4508
+ row,
4509
+ disabled: busy,
4510
+ onToggle: onToggleRow,
4511
+ t
4512
+ }, `u:${row.path}:${row.oldPath ?? ""}`))] }) : null
4513
+ ]
4514
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4515
+ className: GitHistoryPanel_module_default.commitBox,
4516
+ children: [
4517
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
4518
+ className: GitHistoryPanel_module_default.commitInput,
4519
+ value: commitDraft,
4520
+ placeholder: t("panel.commitPlaceholder"),
4521
+ "aria-label": t("panel.commitPlaceholder"),
4522
+ rows: 2,
4523
+ maxLength: COMMIT_MESSAGE_LIMIT,
4524
+ onChange: (event) => onCommitDraft(event.target.value)
4525
+ }),
4526
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4527
+ className: GitHistoryPanel_module_default.commitActions,
4528
+ children: [aiAvailable ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
4529
+ type: "button",
4530
+ className: GitHistoryPanel_module_default.act,
4531
+ disabled: busy || aiBusy || !aiReady,
4532
+ title: !aiReady ? t("panel.aiNeedsChanges") : void 0,
4533
+ onClick: onAiGenerate,
4534
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconSparkleRegular, { size: 13 }), aiBusy ? t("panel.aiBusy") : t("panel.aiGenerate")]
4535
+ }) : null, /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4536
+ className: GitHistoryPanel_module_default.split,
4537
+ ref: menuRef,
4538
+ children: [
4539
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4540
+ type: "button",
4541
+ className: `${GitHistoryPanel_module_default.act} ${GitHistoryPanel_module_default.actPrimary} ${GitHistoryPanel_module_default.splitMain}`,
4542
+ disabled: !canCommit || busy,
4543
+ onClick: () => onCommit(false),
4544
+ children: busy ? t("panel.commitBusy") : t("panel.commit")
4545
+ }),
4546
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4547
+ type: "button",
4548
+ className: `${GitHistoryPanel_module_default.act} ${GitHistoryPanel_module_default.actPrimary} ${GitHistoryPanel_module_default.splitCaret}`,
4549
+ "aria-label": t("panel.commitMoreAria"),
4550
+ "aria-expanded": commitMenuOpen,
4551
+ disabled: !canCommit || busy,
4552
+ onClick: onToggleCommitMenu,
4553
+ children: "▾"
4554
+ }),
4555
+ commitMenuOpen ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4556
+ className: GitHistoryPanel_module_default.menu,
4557
+ role: "menu",
4558
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4559
+ type: "button",
4560
+ role: "menuitem",
4561
+ className: GitHistoryPanel_module_default.menuItem,
4562
+ disabled: !canCommit,
4563
+ onClick: () => onCommit(false),
4564
+ children: t("panel.commit")
4565
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4566
+ type: "button",
4567
+ role: "menuitem",
4568
+ className: GitHistoryPanel_module_default.menuItem,
4569
+ disabled: !canCommit,
4570
+ onClick: () => onCommit(true),
4571
+ children: t("panel.commitPush")
4572
+ })]
4573
+ }) : null
4574
+ ]
4575
+ })]
4576
+ }),
4577
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4578
+ className: GitHistoryPanel_module_default.commitHint,
4579
+ children: hint
4580
+ })
4581
+ ]
4582
+ })]
4583
+ })]
4584
+ });
4585
+ }
4586
+ //#endregion
4587
+ //#region src/client/relative-time.ts
4588
+ /**
4589
+ * Compact relative time for trailing labels, as a structured bucket the
4590
+ * renderer localizes ("now"/"5min"/"3h"/"2d"/"4mo"/"1y" in en).
4591
+ */
4592
+ function relativeTime(updatedAt, now) {
4593
+ const MIN = 6e4;
4594
+ const HOUR = 36e5;
4595
+ const DAY = 864e5;
4596
+ const diff = Math.max(0, now - updatedAt);
4597
+ if (diff < MIN) return {
4598
+ unit: "now",
4599
+ n: 0
4600
+ };
4601
+ if (diff < HOUR) return {
4602
+ unit: "minutes",
4603
+ n: Math.floor(diff / MIN)
4604
+ };
4605
+ if (diff < DAY) return {
4606
+ unit: "hours",
4607
+ n: Math.floor(diff / HOUR)
4608
+ };
4609
+ if (diff < 30 * DAY) return {
4610
+ unit: "days",
4611
+ n: Math.floor(diff / DAY)
4612
+ };
4613
+ if (diff < 365 * DAY) return {
4614
+ unit: "months",
4615
+ n: Math.floor(diff / (30 * DAY))
4616
+ };
4617
+ return {
4618
+ unit: "years",
4619
+ n: Math.floor(diff / (365 * DAY))
4620
+ };
4621
+ }
4622
+ /** Localized compact relative time ("刚刚"/"5分钟" in zh, "now"/"5min" in en). */
4623
+ function timeLabel(updatedAt, now, t) {
4624
+ const { unit, n } = relativeTime(updatedAt, now);
4625
+ if (unit === "now") return t("time.now");
4626
+ if (unit === "minutes") return t("time.minutes", { n });
4627
+ if (unit === "hours") return t("time.hours", { n });
4628
+ if (unit === "days") return t("time.days", { n });
4629
+ if (unit === "months") return t("time.months", { n });
4630
+ return t("time.years", { n });
4631
+ }
4632
+ //#endregion
4633
+ //#region src/client/GitHistoryPanel.tsx
4634
+ /**
4635
+ * GitHistoryPanel: the git-management tab for the dsh-better-sidebar right
4636
+ * sidebar. The entry card, selection, and presentation are better-sidebar's
4637
+ * (registered through `ctx.betterSidebar.registerTab` from index.ts); this
4638
+ * file renders the TAB CONTENT: a slim chrome (repo pill, refresh) over the
4639
+ * three stacked sections (uncommitted changes, branches, commit history).
4640
+ * Session facts ride the tab call's scope (sessionId/cwd) plus the session
4641
+ * list store (the model-selection projection feeds the AI message
4642
+ * generator). All git calls ride the plugin's own routes through the api
4643
+ * module; every failure lands in a toast or an inline error line, never a
4644
+ * crash boundary.
4645
+ *
4646
+ * The state owner is {@link GitHistoryPanelData} below: one component owning
4647
+ * the status/changes/log state machine, feeding the three presentational
4648
+ * sections, keyed by cwd so a session switch starts every section fresh.
4649
+ *
4650
+ * @module git-worktree/client/GitHistoryPanel
4651
+ */
4652
+ /** One git failure → its message text (the git stderr rides error.message). */
4653
+ function failureText(error) {
4654
+ return error instanceof Error ? error.message : String(error);
4655
+ }
4656
+ /** Data owner for the three sections. Remounts when `cwd` changes (the page
4657
+ * keys the mount by cwd), so no state crosses a repository boundary. */
4658
+ const GitHistoryPanelData = (0, react.forwardRef)(function GitHistoryPanelData(props, ref) {
4659
+ const { cwd, model, t, pushToast } = props;
4660
+ const [status, setStatus] = (0, react.useState)(null);
4661
+ const [statusError, setStatusError] = (0, react.useState)();
4662
+ const [changes, setChanges] = (0, react.useState)([]);
4663
+ const [changesTruncated, setChangesTruncated] = (0, react.useState)(false);
4664
+ const [commits, setCommits] = (0, react.useState)([]);
4665
+ const [historyLoaded, setHistoryLoaded] = (0, react.useState)(false);
4666
+ const [historyLoading, setHistoryLoading] = (0, react.useState)(false);
4667
+ const [historyError, setHistoryError] = (0, react.useState)();
4668
+ const [hasMore, setHasMore] = (0, react.useState)(false);
4669
+ const [loadingMore, setLoadingMore] = (0, react.useState)(false);
4670
+ const [expanded, setExpanded] = (0, react.useState)(null);
4671
+ const [filesByHash, setFilesByHash] = (0, react.useState)({});
4672
+ const [sectionBusy, setSectionBusy] = (0, react.useState)(false);
4673
+ const [syncBusy, setSyncBusy] = (0, react.useState)(false);
4674
+ const [commitMenuOpen, setCommitMenuOpen] = (0, react.useState)(false);
4675
+ const [commitDraft, setCommitDraft] = (0, react.useState)("");
4676
+ const [aiBusy, setAiBusy] = (0, react.useState)(false);
4677
+ /** Response-sequence guard: a refresh raced against a mutation or a cwd
4678
+ * change must never land an older snapshot over a newer one. */
4679
+ const seqRef = (0, react.useRef)(0);
4680
+ const loadHistoryPage = (0, react.useCallback)(async (skip) => {
4681
+ const result = await requestLog(cwd, 50, skip);
4682
+ if (!result.ok) {
4683
+ setHistoryError(result.error);
4684
+ return;
4685
+ }
4686
+ setHistoryError(void 0);
4687
+ return {
4688
+ commits: result.commits,
4689
+ complete: result.commits.length < 50
4690
+ };
4691
+ }, [cwd]);
4692
+ const loadAll = (0, react.useCallback)(async () => {
4693
+ const seq = seqRef.current + 1;
4694
+ seqRef.current = seq;
4695
+ setHistoryLoading(true);
4696
+ const [statusResult, changesResult, logPage] = await Promise.all([
4697
+ fetchStatus(cwd),
4698
+ requestChanges(cwd),
4699
+ loadHistoryPage(0)
4700
+ ]);
4701
+ if (seq !== seqRef.current) return;
4702
+ if (statusResult.ok) {
4703
+ setStatus(statusResult);
4704
+ setStatusError(void 0);
4705
+ } else {
4706
+ setStatus(null);
4707
+ setStatusError(statusResult.error);
4708
+ }
4709
+ setChanges(changesResult.ok ? changesResult.changes : []);
4710
+ setChangesTruncated(changesResult.ok && changesResult.truncated === true);
4711
+ if (logPage !== void 0) {
4712
+ setCommits(logPage.commits);
4713
+ setHasMore(!logPage.complete);
4714
+ setHistoryLoaded(true);
4715
+ }
4716
+ setHistoryLoading(false);
4717
+ }, [cwd, loadHistoryPage]);
4718
+ (0, react.useEffect)(() => {
4719
+ loadAll();
4720
+ }, [loadAll]);
4721
+ const refresh = (0, react.useCallback)(() => {
4722
+ loadAll();
4723
+ }, [loadAll]);
4724
+ (0, react.useEffect)(() => onGitMutated(refresh), [refresh]);
4725
+ (0, react.useImperativeHandle)(ref, () => ({ refresh }), [refresh]);
4726
+ const repo = status !== null && status.repo ? status : null;
4727
+ /** Run one mutating verb with the section-busy guard and error toast.
4728
+ * Mutations refresh through `loadAll` so their re-read bumps the request
4729
+ * sequence — an older in-flight snapshot can never land over the fresh
4730
+ * post-mutation state. A SUCCESS also emits the cross-surface notice so
4731
+ * the composer chip drops the branch git just left (stage toggles ride
4732
+ * along — one extra idempotent /status beats per-verb wiring). */
4733
+ const runGuarded = (0, react.useCallback)(async (verb) => {
4734
+ setSectionBusy(true);
4735
+ try {
4736
+ await verb();
4737
+ emitGitMutated();
4738
+ } catch (error) {
4739
+ pushToast(t("errorGeneric", { message: failureText(error) }), "error");
4740
+ } finally {
4741
+ setSectionBusy(false);
4742
+ }
4743
+ }, [pushToast, t]);
4744
+ const onToggleRow = (0, react.useCallback)((row) => {
4745
+ runGuarded(async () => {
4746
+ const paths = row.oldPath === void 0 ? [row.path] : [row.path, row.oldPath];
4747
+ const result = await requestStage(cwd, paths, !row.staged);
4748
+ if (!result.ok) throw new Error(result.error);
4749
+ await loadAll();
4750
+ });
4751
+ }, [
4752
+ cwd,
4753
+ loadAll,
4754
+ runGuarded
4755
+ ]);
4756
+ const stagePathsOf = (rows, includeOldPath) => {
4757
+ const paths = [];
4758
+ for (const row of rows) {
4759
+ paths.push(row.path);
4760
+ if (includeOldPath && row.oldPath !== void 0) paths.push(row.oldPath);
4761
+ }
4762
+ return [...new Set(paths)];
4763
+ };
4764
+ const onStageAll = (0, react.useCallback)(() => {
4765
+ runGuarded(async () => {
4766
+ const result = await requestStage(cwd, stagePathsOf(groupChanges(changes).unstaged, false), true);
4767
+ if (!result.ok) throw new Error(result.error);
4768
+ await loadAll();
4769
+ });
4770
+ }, [
4771
+ changes,
4772
+ cwd,
4773
+ loadAll,
4774
+ runGuarded
4775
+ ]);
4776
+ const onUnstageAll = (0, react.useCallback)(() => {
4777
+ runGuarded(async () => {
4778
+ const result = await requestStage(cwd, stagePathsOf(groupChanges(changes).staged, true), false);
4779
+ if (!result.ok) throw new Error(result.error);
4780
+ await loadAll();
4781
+ });
4782
+ }, [
4783
+ changes,
4784
+ cwd,
4785
+ loadAll,
4786
+ runGuarded
4787
+ ]);
4788
+ const onCommit = (0, react.useCallback)((push) => {
4789
+ const message = commitDraft.trim();
4790
+ if (message === "") return;
4791
+ runGuarded(async () => {
4792
+ const result = await requestCommit(cwd, message);
4793
+ if (!result.ok) throw new Error(result.error);
4794
+ setCommitDraft("");
4795
+ setCommitMenuOpen(false);
4796
+ if (!push) {
4797
+ pushToast(t("panel.commitDone", { subject: result.subject }), "success");
4798
+ await loadAll();
4799
+ return;
4800
+ }
4801
+ try {
4802
+ const pushResult = await requestPush(cwd);
4803
+ if (!pushResult.ok) {
4804
+ pushToast(t("panel.commitDone", { subject: result.subject }), "success");
4805
+ pushToast(t("errorGeneric", { message: pushResult.error }), "error");
4806
+ } else pushToast(t("panel.commitPushDone", { subject: result.subject }), "success");
4807
+ } catch (error) {
4808
+ pushToast(t("panel.commitDone", { subject: result.subject }), "success");
4809
+ pushToast(t("errorGeneric", { message: failureText(error) }), "error");
4810
+ }
4811
+ await loadAll();
4812
+ });
4813
+ }, [
4814
+ commitDraft,
4815
+ cwd,
4816
+ loadAll,
4817
+ pushToast,
4818
+ runGuarded,
4819
+ t
4820
+ ]);
4821
+ const onAiGenerate = (0, react.useCallback)(() => {
4822
+ if (model === void 0 || aiBusy) return;
4823
+ setAiBusy(true);
4824
+ (async () => {
4825
+ try {
4826
+ const result = await requestAiCommitMessage({
4827
+ repoPath: cwd,
4828
+ provider: model.provider,
4829
+ model: model.model,
4830
+ ...model.reasoningEffort === void 0 ? {} : { effort: model.reasoningEffort },
4831
+ ...commitDraft.trim() === "" ? {} : { hint: commitDraft.trim() }
4832
+ });
4833
+ if (!result.ok) {
4834
+ pushToast(t("panel.aiFailed", { message: result.error }), "error");
4835
+ return;
4836
+ }
4837
+ setCommitDraft(result.message);
4838
+ pushToast(t("panel.aiDone"), "success");
4839
+ } catch (error) {
4840
+ pushToast(t("panel.aiFailed", { message: failureText(error) }), "error");
4841
+ } finally {
4842
+ setAiBusy(false);
4843
+ }
4844
+ })();
4845
+ }, [
4846
+ aiBusy,
4847
+ commitDraft,
4848
+ cwd,
4849
+ model,
4850
+ pushToast,
4851
+ t
4852
+ ]);
4853
+ const onFetch = (0, react.useCallback)(() => {
4854
+ setSyncBusy(true);
4855
+ (async () => {
4856
+ try {
4857
+ const result = await requestFetch(cwd);
4858
+ if (!result.ok) pushToast(t("errorGeneric", { message: result.error }), "error");
4859
+ else {
4860
+ pushToast(t("fetchDone"), "success");
4861
+ emitGitMutated();
4862
+ }
4863
+ await loadAll();
4864
+ } finally {
4865
+ setSyncBusy(false);
4866
+ }
4867
+ })();
4868
+ }, [
4869
+ cwd,
4870
+ loadAll,
4871
+ pushToast,
4872
+ t
4873
+ ]);
4874
+ const onUpdate = (0, react.useCallback)(() => {
4875
+ setSyncBusy(true);
4876
+ (async () => {
4877
+ try {
4878
+ const result = await requestUpdate(cwd);
4879
+ if (!result.ok) pushToast(t("errorGeneric", { message: result.error }), "error");
4880
+ else {
4881
+ pushToast(result.updated ? t("updateDone", { branch: result.branch }) : t("updateUpToDate"), "success");
4882
+ emitGitMutated();
4883
+ }
4884
+ await loadAll();
4885
+ } finally {
4886
+ setSyncBusy(false);
4887
+ }
4888
+ })();
4889
+ }, [
4890
+ cwd,
4891
+ loadAll,
4892
+ pushToast,
4893
+ t
4894
+ ]);
4895
+ const onPush = (0, react.useCallback)(() => {
4896
+ setSyncBusy(true);
4897
+ (async () => {
4898
+ try {
4899
+ const result = await requestPush(cwd);
4900
+ if (!result.ok) pushToast(t("errorGeneric", { message: result.error }), "error");
4901
+ else {
4902
+ pushToast(result.pushed ? t("pushDone", { branch: result.branch }) : t("pushUpToDate"), "success");
4903
+ emitGitMutated();
4904
+ }
4905
+ await loadAll();
4906
+ } finally {
4907
+ setSyncBusy(false);
4908
+ }
4909
+ })();
4910
+ }, [
4911
+ cwd,
4912
+ loadAll,
4913
+ pushToast,
4914
+ t
4915
+ ]);
4916
+ const onCheckout = (0, react.useCallback)((branch) => {
4917
+ runGuarded(async () => {
4918
+ const result = await requestSwitch(cwd, branch.name);
4919
+ if (!result.ok) throw new Error(result.error);
4920
+ pushToast(t("panel.checkoutDone", { branch: result.branch }), "success");
4921
+ await loadAll();
4922
+ });
4923
+ }, [
4924
+ cwd,
4925
+ loadAll,
4926
+ pushToast,
4927
+ runGuarded,
4928
+ t
4929
+ ]);
4930
+ const onRename = (0, react.useCallback)((branch, newName) => {
4931
+ runGuarded(async () => {
4932
+ const result = await requestRenameBranch(cwd, branch.name, newName);
4933
+ if (!result.ok) throw new Error(result.error);
4934
+ pushToast(t("panel.renameDone", { branch: result.branch }), "success");
4935
+ await loadAll();
4936
+ });
4937
+ }, [
4938
+ cwd,
4939
+ loadAll,
4940
+ pushToast,
4941
+ runGuarded,
4942
+ t
4943
+ ]);
4944
+ const onDelete = (0, react.useCallback)((branch) => {
4945
+ runGuarded(async () => {
4946
+ const result = await requestDeleteBranch(cwd, branch.name);
4947
+ if (!result.ok) throw new Error(result.error);
4948
+ pushToast(t("panel.deleteDone", { branch: result.branch }), "success");
4949
+ await loadAll();
4950
+ });
4951
+ }, [
4952
+ cwd,
4953
+ loadAll,
4954
+ pushToast,
4955
+ runGuarded,
4956
+ t
4957
+ ]);
4958
+ const onCreate = (0, react.useCallback)((name, from) => {
4959
+ runGuarded(async () => {
4960
+ const result = await requestCreateBranch(cwd, name, from);
4961
+ if (!result.ok) throw new Error(result.error);
4962
+ pushToast(t("panel.createDone", { branch: result.branch }), "success");
4963
+ await loadAll();
4964
+ });
4965
+ }, [
4966
+ cwd,
4967
+ loadAll,
4968
+ pushToast,
4969
+ runGuarded,
4970
+ t
4971
+ ]);
4972
+ const onToggleCommit = (0, react.useCallback)((hash) => {
4973
+ if (expanded === hash) {
4974
+ setExpanded(null);
4975
+ return;
4976
+ }
4977
+ setExpanded(hash);
4978
+ if (filesByHash[hash] !== void 0) return;
4979
+ setFilesByHash((prev) => ({
4980
+ ...prev,
4981
+ [hash]: { kind: "loading" }
4982
+ }));
4983
+ (async () => {
4984
+ const result = await requestCommitFiles(cwd, hash);
4985
+ setFilesByHash((prev) => ({
4986
+ ...prev,
4987
+ [hash]: result.ok ? {
4988
+ kind: "loaded",
4989
+ files: result.files
4990
+ } : {
4991
+ kind: "failed",
4992
+ message: result.error
4993
+ }
4994
+ }));
4995
+ })();
4996
+ }, [
4997
+ cwd,
4998
+ expanded,
4999
+ filesByHash
5000
+ ]);
5001
+ const onLoadMore = (0, react.useCallback)(() => {
5002
+ if (loadingMore) return;
5003
+ setLoadingMore(true);
5004
+ const seq = seqRef.current;
5005
+ (async () => {
5006
+ try {
5007
+ const page = await loadHistoryPage(commits.length);
5008
+ if (seq !== seqRef.current) return;
5009
+ if (page !== void 0) {
5010
+ setCommits((prev) => appendPage(prev, page.commits));
5011
+ setHasMore(!page.complete);
5012
+ }
5013
+ } finally {
5014
+ setLoadingMore(false);
5015
+ }
5016
+ })();
5017
+ }, [
5018
+ commits.length,
5019
+ loadHistoryPage,
5020
+ loadingMore
5021
+ ]);
5022
+ const timeLabelFor = (0, react.useCallback)((date) => timeLabel(date, Date.now(), t), [t]);
5023
+ if (statusError !== void 0 && repo === null) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5024
+ className: GitHistoryPanel_module_default.errorText,
5025
+ children: t("panel.loadFailed", { message: statusError })
5026
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5027
+ className: GitHistoryPanel_module_default.moreRow,
5028
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
5029
+ type: "button",
5030
+ className: GitHistoryPanel_module_default.act,
5031
+ onClick: refresh,
5032
+ children: t("panel.retry")
5033
+ })
5034
+ })] });
5035
+ if (status !== null && status.repo === false) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5036
+ className: GitHistoryPanel_module_default.empty,
5037
+ children: t("panel.notARepo")
5038
+ });
5039
+ if (repo === null) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5040
+ className: GitHistoryPanel_module_default.empty,
5041
+ children: t("manager.loading")
5042
+ });
5043
+ const grouped = groupChanges(changes);
5044
+ const rows = buildBranchRows(repo.branches, repo.currentBranch);
5045
+ const remoteRows = buildBranchRows(repo.remoteBranches ?? repo.branches.filter((branch) => branch.kind === "remote"), "").remotes;
5046
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5047
+ className: GitHistoryPanel_module_default.scroll,
5048
+ children: [
5049
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UncommittedSection, {
5050
+ t,
5051
+ staged: grouped.staged,
5052
+ unstaged: grouped.unstaged,
5053
+ busy: sectionBusy,
5054
+ syncBusy,
5055
+ truncated: changesTruncated,
5056
+ commitDraft,
5057
+ onCommitDraft: setCommitDraft,
5058
+ commitMenuOpen,
5059
+ onToggleCommitMenu: () => setCommitMenuOpen((open) => !open),
5060
+ onCommit,
5061
+ onToggleRow,
5062
+ onStageAll,
5063
+ onUnstageAll,
5064
+ aiAvailable: model !== void 0,
5065
+ aiBusy,
5066
+ onAiGenerate,
5067
+ onFetch,
5068
+ onUpdate,
5069
+ onPush
5070
+ }),
5071
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BranchSection, {
5072
+ t,
5073
+ locals: rows.locals,
5074
+ remotes: remoteRows,
5075
+ busy: sectionBusy,
5076
+ onCheckout,
5077
+ onRename,
5078
+ onDelete,
5079
+ onCreate
5080
+ }),
5081
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(HistorySection, {
5082
+ t,
5083
+ commits,
5084
+ loaded: historyLoaded,
5085
+ loadError: historyError,
5086
+ loading: historyLoading,
5087
+ hasMore,
5088
+ loadingMore,
5089
+ expanded,
5090
+ filesByHash,
5091
+ onLoadMore,
5092
+ onToggle: onToggleCommit,
5093
+ timeLabelFor
5094
+ })
5095
+ ]
5096
+ });
5097
+ });
5098
+ /** The registered better-sidebar tab's content: chrome + data owner, keyed
5099
+ * by cwd so a session switch starts every section from scratch. */
5100
+ function GitPanelTab(props) {
5101
+ const { ctx, scope, t } = props;
5102
+ const subscribe = (0, react.useCallback)((listener) => ctx.sessions.list.subscribe(listener), [ctx]);
5103
+ const cwd = (0, react.useSyncExternalStore)(subscribe, () => scope.cwd ?? ctx.sessions.list.getSnapshot().byId[scope.sessionId]?.cwd);
5104
+ const model = (0, react.useSyncExternalStore)(subscribe, () => {
5105
+ const selection = ctx.sessions.list.getSnapshot().projectionsBySession[scope.sessionId]?.values.modelSelection;
5106
+ return selection === void 0 ? void 0 : selection.next ?? selection.lastUsed ?? void 0;
5107
+ });
5108
+ const [toasts, setToasts] = (0, react.useState)([]);
5109
+ const toastSeq = (0, react.useRef)(0);
5110
+ const dataRef = (0, react.useRef)(null);
5111
+ const pushToast = (0, react.useCallback)((text, tone) => {
5112
+ toastSeq.current += 1;
5113
+ const seq = toastSeq.current;
5114
+ setToasts((queue) => [...queue, {
5115
+ seq,
5116
+ text,
5117
+ tone
5118
+ }]);
5119
+ }, []);
5120
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5121
+ className: GitHistoryPanel_module_default.page,
5122
+ children: [
5123
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
5124
+ className: GitHistoryPanel_module_default.topbar,
5125
+ children: [cwd !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5126
+ className: GitHistoryPanel_module_default.repoPill,
5127
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5128
+ className: GitHistoryPanel_module_default.repoName,
5129
+ children: cwd
5130
+ })
5131
+ }) : null, /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5132
+ className: GitHistoryPanel_module_default.topbarActions,
5133
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
5134
+ type: "button",
5135
+ className: GitHistoryPanel_module_default.refreshBtn,
5136
+ disabled: cwd === void 0,
5137
+ title: t("panel.refresh"),
5138
+ "aria-label": t("panel.refresh"),
5139
+ onClick: () => dataRef.current?.refresh(),
5140
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconRefreshOutlineRegular, {
5141
+ size: 14,
5142
+ "aria-hidden": "true"
5143
+ })
5144
+ })
5145
+ })]
5146
+ }),
5147
+ cwd === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5148
+ className: GitHistoryPanel_module_default.empty,
5149
+ children: t("panel.noSession")
5150
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GitHistoryPanelData, {
5151
+ ref: dataRef,
5152
+ cwd,
5153
+ model,
5154
+ t,
5155
+ pushToast
5156
+ }, cwd),
5157
+ toasts.map((toast) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Toast, {
5158
+ text: toast.text,
5159
+ ...toast.tone === "success" ? { tone: "success" } : {},
5160
+ onDone: () => setToasts((queue) => queue.filter((entry) => entry.seq !== toast.seq))
5161
+ }, String(toast.seq)))
5162
+ ]
5163
+ });
5164
+ }
5165
+ //#endregion
5166
+ //#region src/client/prune-history.ts
5167
+ const KEY = "git-worktree:prune-history";
5168
+ /** The log is a recent-activity trail, not an archive: the oldest runs fall
5169
+ * off once this many are kept. */
5170
+ const MAX_RUNS = 20;
5171
+ /** Append one finished run (newest first), trimming the tail past
5172
+ * {@link MAX_RUNS}. Best-effort: a storage write that throws (private mode,
5173
+ * quota) swallows — the toast already told the user what happened. */
5174
+ function recordPruneRun(entry) {
5175
+ try {
5176
+ const next = [entry, ...readPruneHistory()].slice(0, MAX_RUNS);
5177
+ localStorage.setItem(KEY, JSON.stringify(next));
5178
+ } catch {}
5179
+ }
5180
+ /** Structural check of ONE stored run. The payload is browser-local JSON an
5181
+ * older build (or anything else holding this key) may have written in a
5182
+ * different shape, and the settings card reads `run.removed.length` straight
5183
+ * into its render — a top-level `Array.isArray` alone would let `[1, 2]`
5184
+ * through and take the whole card down with a TypeError. Only the fields the
5185
+ * card actually touches are checked; their element shapes stay trusted (a
5186
+ * malformed member degrades one line, not the page). */
5187
+ function isPruneRunEntry(value) {
5188
+ if (typeof value !== "object" || value === null) return false;
5189
+ const entry = value;
5190
+ return typeof entry.at === "number" && Array.isArray(entry.removed) && Array.isArray(entry.skippedDirty) && Array.isArray(entry.failed);
5191
+ }
5192
+ /** The recorded runs, newest first; an unreadable/foreign payload reads as
5193
+ * "no history" rather than throwing into the settings render — entry by
5194
+ * entry, so one bad record costs its own line and nothing else. */
5195
+ function readPruneHistory() {
5196
+ try {
5197
+ const raw = localStorage.getItem(KEY);
5198
+ if (raw === null) return [];
5199
+ const parsed = JSON.parse(raw);
5200
+ return Array.isArray(parsed) ? parsed.filter(isPruneRunEntry) : [];
5201
+ } catch {
5202
+ return [];
5203
+ }
5204
+ }
5205
+ //#endregion
5206
+ //#region src/client/concurrency.ts
5207
+ /**
5208
+ * Bounded-concurrency map for the browser half's fan-out calls.
5209
+ *
5210
+ * Every plugin route that answers a batch is a git process per item on the
5211
+ * host side, and on Windows a process spawn is the expensive part — the
5212
+ * `/group` route already caps its own fan-out for exactly this reason. The
5213
+ * browser side has the same duty for the calls it makes ONE PER ITEM
5214
+ * (the manager dialog inspects every scanned worktree): a bare
5215
+ * `Promise.all(items.map(...))` hands the host as many concurrent spawns as
5216
+ * the user happens to have directories.
5217
+ *
5218
+ * @module git-worktree/client/concurrency
5219
+ */
5220
+ /**
5221
+ * Map over `items` with at most `limit` calls in flight, preserving INPUT
5222
+ * ORDER in the result (the caller zips results back onto its rows, so
5223
+ * completion order must not leak). Rejections propagate like `Promise.all`:
5224
+ * callers that want per-item tolerance catch inside `fn`.
5225
+ *
5226
+ * @param items - the inputs, mapped in order.
5227
+ * @param limit - maximum concurrent calls; values under 1 are clamped to 1.
5228
+ * @param fn - the per-item async work, given the item and its index.
5229
+ * @returns the results, index-aligned with `items`.
5230
+ */
5231
+ async function mapLimit(items, limit, fn) {
5232
+ const queue = items.map((item, index) => [item, index]);
5233
+ const results = [];
5234
+ const width = Math.max(1, Math.floor(limit));
5235
+ let cursor = 0;
5236
+ const worker = async () => {
5237
+ for (;;) {
5238
+ const entry = queue[cursor];
5239
+ cursor += 1;
5240
+ if (entry === void 0) return;
5241
+ const [item, index] = entry;
5242
+ results[index] = await fn(item, index);
5243
+ }
5244
+ };
5245
+ await Promise.all(Array.from({ length: Math.min(width, queue.length) }, worker));
5246
+ return results;
5247
+ }
5248
+ //#endregion
5249
+ //#region src/client/scan-groups.ts
5250
+ /** Group the scan for display: project groups before legacy groups (same
5251
+ * repo can appear under both), repositories alphabetical within a source,
5252
+ * rows within a group by branch name, all numeric-aware so `feat/v2` sorts
5253
+ * past `feat/v10`. Grouping is display-order only — the removal flows
5254
+ * address rows by path. */
5255
+ function groupScanEntries(entries) {
5256
+ const byKey = /* @__PURE__ */ new Map();
5257
+ const orphans = [];
5258
+ for (const entry of entries) {
5259
+ if (entry.repoName === null) {
5260
+ orphans.push(entry);
5261
+ continue;
5262
+ }
5263
+ const key = `${entry.source}\u0000${entry.repoName}`;
5264
+ const bucket = byKey.get(key);
5265
+ if (bucket === void 0) byKey.set(key, {
5266
+ source: entry.source,
5267
+ repoName: entry.repoName,
5268
+ list: [entry]
5269
+ });
5270
+ else bucket.list.push(entry);
5271
+ }
5272
+ const cmp = (a, b) => a.localeCompare(b, void 0, { numeric: true });
5273
+ 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 }) => ({
5274
+ repoName,
5275
+ source,
5276
+ entries: list.slice().sort((a, b) => cmp(a.branch ?? "", b.branch ?? ""))
5277
+ }));
5278
+ return orphans.length > 0 ? [...groups, {
5279
+ repoName: null,
5280
+ source: "orphans",
5281
+ entries: orphans
5282
+ }] : groups;
5283
+ }
5284
+ //#endregion
5285
+ //#region src/client/worktree-remove-flow.ts
5286
+ /**
5287
+ * Remove one worktree completely. A refused git removal rethrows UNLESS the
5288
+ * directory is actually gone (the Windows half-removal shape: git already
5289
+ * unregistered and emptied the tree, then failed the final rmdir — the git
5290
+ * half is done, so the flow continues with the DSH half). Session archives
5291
+ * never abort the flow (a rejected archive is logged and skipped); a failed
5292
+ * workspace unregistration DOES reject — the caller's UI owns the retry.
5293
+ */
5294
+ async function removeWorktreeFully(deps, target) {
5295
+ try {
5296
+ await deps.removeWorktree(target.path, target.force);
5297
+ } catch (reason) {
5298
+ if ((await deps.probeDirectories([target.path]).catch(() => void 0))?.exists[target.path] !== false) throw reason;
5299
+ }
5300
+ for (const sessionId of target.archiveSessionIds ?? []) await deps.archiveSession(sessionId).catch((reason) => {
5301
+ console.warn("session archive rejected during worktree removal:", reason);
5302
+ });
5303
+ if (target.workspaceId !== void 0) await deps.deleteWorkspace(target.workspaceId);
5304
+ }
5305
+ //#endregion
5306
+ //#region src/client/worktree-prune.ts
5307
+ /** Canonical-ish key for cross-source path comparison: both sides come from
5308
+ * the same host, but the scan assembles `<rootDir>/<child>` while the
5309
+ * workspace model stores its own canonical spelling — separators are the
5310
+ * only drift worth papering over. */
5311
+ function pathKey(path) {
5312
+ return path.replace(/\\/g, "/").replace(/\/+$/, "");
5313
+ }
5314
+ /** Freshest `updatedAt` across QUALIFYING sessions (non-blank, non-subagent
5315
+ * — the same qualification the archive set uses); 0 when nothing qualifies,
5316
+ * which reads as "least active in the room". Shared by the lazy prune's
5317
+ * plan input and the manager dialog's last-use column so the two activity
5318
+ * figures can never drift apart. */
3498
5319
  function freshestUpdatedAt(summaries) {
3499
5320
  let latest = 0;
3500
5321
  for (const summary of summaries) {
@@ -4449,7 +6270,67 @@ window.__ModuleLoader__.load({
4449
6270
  "time.years": "{n}y",
4450
6271
  copy: "Copy",
4451
6272
  close: "Close",
4452
- cancel: "Cancel"
6273
+ cancel: "Cancel",
6274
+ "panel.nav": "Git panel",
6275
+ "panel.description": "Uncommitted changes, branches, and commit history",
6276
+ "panel.notARepo": "The current session is not inside a git repository.",
6277
+ "panel.noSession": "Open a session in a repository to see its git panel.",
6278
+ "panel.loadFailed": "Git panel failed to load: {message}",
6279
+ "panel.retry": "Retry",
6280
+ "panel.refresh": "Refresh",
6281
+ "panel.uncommitted": "Uncommitted changes",
6282
+ "panel.staged": "Staged",
6283
+ "panel.unstaged": "Unstaged",
6284
+ "panel.stageAll": "Stage all",
6285
+ "panel.unstageAll": "Unstage all",
6286
+ "panel.stageHint": "git add",
6287
+ "panel.unstageHint": "git reset",
6288
+ "panel.clean": "No uncommitted changes",
6289
+ "panel.changesTruncated": "Too many changes — showing only the first 500 rows",
6290
+ "panel.commitPlaceholder": "Commit message (feat: …)",
6291
+ "panel.commit": "Commit",
6292
+ "panel.commitPush": "Commit and push",
6293
+ "panel.commitMoreAria": "More commit actions",
6294
+ "panel.commitBusy": "Committing…",
6295
+ "panel.commitDone": "Committed: {subject}",
6296
+ "panel.commitPushDone": "Committed and pushed: {subject}",
6297
+ "panel.commitNeedsStaged": "Tick files to stage them first",
6298
+ "panel.commitNeedsMessage": "Enter a commit message",
6299
+ "panel.aiGenerate": "AI message",
6300
+ "panel.aiBusy": "Generating…",
6301
+ "panel.aiNeedsChanges": "Stage some changes first, then generate",
6302
+ "panel.aiDone": "AI drafted a message — review and edit before committing",
6303
+ "panel.aiFailed": "AI message failed: {message}",
6304
+ "panel.branches": "Branches",
6305
+ "panel.branchNew": "New branch",
6306
+ "panel.branchLocals": "local",
6307
+ "panel.branchRemotes": "remote",
6308
+ "panel.branchCheckout": "Check out",
6309
+ "panel.branchCheckoutRemote": "Check out (create local tracking branch)",
6310
+ "panel.branchNewFrom": "New branch from here…",
6311
+ "panel.branchRename": "Rename…",
6312
+ "panel.branchDelete": "Delete",
6313
+ "panel.branchCopy": "Copy name",
6314
+ "panel.branchCurrent": "current branch",
6315
+ "panel.checkoutDone": "Checked out {branch}",
6316
+ "panel.renameDone": "Renamed to {branch}",
6317
+ "panel.deleteDone": "Deleted branch {branch}",
6318
+ "panel.createDone": "Created branch {branch}",
6319
+ "panel.history": "Commit history",
6320
+ "panel.historySearch": "Search commits / authors",
6321
+ "panel.noCommits": "This repository has no commits yet",
6322
+ "panel.noMatches": "No matching commits",
6323
+ "panel.loadMore": "Load more",
6324
+ "panel.filesTitle": "Changed files ({n}) · {author} · {date}",
6325
+ "panel.binary": "binary",
6326
+ "panel.stAdd": "added",
6327
+ "panel.stMod": "modified",
6328
+ "panel.stDel": "deleted",
6329
+ "panel.stRen": "renamed",
6330
+ "panel.stCopy": "copied",
6331
+ "panel.stType": "type changed",
6332
+ "panel.stUntrack": "untracked",
6333
+ "panel.stUnmerged": "conflict"
4453
6334
  };
4454
6335
  /** 中文词典。 */
4455
6336
  const zh = {
@@ -4579,7 +6460,67 @@ window.__ModuleLoader__.load({
4579
6460
  "time.years": "{n}年",
4580
6461
  copy: "复制",
4581
6462
  close: "关闭",
4582
- cancel: "取消"
6463
+ cancel: "取消",
6464
+ "panel.nav": "Git 管理",
6465
+ "panel.description": "未提交变更、分支操作与提交历史",
6466
+ "panel.notARepo": "当前会话不在 git 仓库中。",
6467
+ "panel.noSession": "在 git 仓库中打开一个会话后,这里会显示它的 Git 面板。",
6468
+ "panel.loadFailed": "Git 面板加载失败:{message}",
6469
+ "panel.retry": "重试",
6470
+ "panel.refresh": "刷新",
6471
+ "panel.uncommitted": "未提交变更",
6472
+ "panel.staged": "已暂存",
6473
+ "panel.unstaged": "未暂存",
6474
+ "panel.stageAll": "全部暂存",
6475
+ "panel.unstageAll": "全部取消暂存",
6476
+ "panel.stageHint": "git add",
6477
+ "panel.unstageHint": "git reset",
6478
+ "panel.clean": "没有未提交的变更",
6479
+ "panel.changesTruncated": "变更过多,仅显示前 500 行",
6480
+ "panel.commitPlaceholder": "提交信息(feat: …)",
6481
+ "panel.commit": "提交",
6482
+ "panel.commitPush": "提交并推送",
6483
+ "panel.commitMoreAria": "更多提交操作",
6484
+ "panel.commitBusy": "提交中…",
6485
+ "panel.commitDone": "已提交:{subject}",
6486
+ "panel.commitPushDone": "已提交并推送:{subject}",
6487
+ "panel.commitNeedsStaged": "先勾选文件加入暂存区",
6488
+ "panel.commitNeedsMessage": "填写提交信息后提交",
6489
+ "panel.aiGenerate": "AI 生成",
6490
+ "panel.aiBusy": "生成中…",
6491
+ "panel.aiNeedsChanges": "先暂存一些变更再生成",
6492
+ "panel.aiDone": "AI 已生成提交信息,请检查后提交",
6493
+ "panel.aiFailed": "AI 生成失败:{message}",
6494
+ "panel.branches": "分支",
6495
+ "panel.branchNew": "新建分支",
6496
+ "panel.branchLocals": "本地",
6497
+ "panel.branchRemotes": "远程",
6498
+ "panel.branchCheckout": "检出",
6499
+ "panel.branchCheckoutRemote": "检出(新建本地跟踪分支)",
6500
+ "panel.branchNewFrom": "新建分支自此…",
6501
+ "panel.branchRename": "重命名…",
6502
+ "panel.branchDelete": "删除",
6503
+ "panel.branchCopy": "复制名称",
6504
+ "panel.branchCurrent": "当前分支",
6505
+ "panel.checkoutDone": "已检出 {branch}",
6506
+ "panel.renameDone": "已重命名为 {branch}",
6507
+ "panel.deleteDone": "已删除分支 {branch}",
6508
+ "panel.createDone": "已创建分支 {branch}",
6509
+ "panel.history": "提交历史",
6510
+ "panel.historySearch": "搜索提交 / 作者",
6511
+ "panel.noCommits": "仓库还没有提交",
6512
+ "panel.noMatches": "没有匹配的提交",
6513
+ "panel.loadMore": "加载更多",
6514
+ "panel.filesTitle": "变更文件({n})· {author} · {date}",
6515
+ "panel.binary": "二进制",
6516
+ "panel.stAdd": "新增",
6517
+ "panel.stMod": "修改",
6518
+ "panel.stDel": "删除",
6519
+ "panel.stRen": "重命名",
6520
+ "panel.stCopy": "复制",
6521
+ "panel.stType": "类型变更",
6522
+ "panel.stUntrack": "未跟踪",
6523
+ "panel.stUnmerged": "冲突"
4583
6524
  };
4584
6525
  //#endregion
4585
6526
  //#region src/client/index.ts
@@ -4592,6 +6533,11 @@ window.__ModuleLoader__.load({
4592
6533
  */
4593
6534
  const PLUGIN_ENTRY_ID = "dsh-git-branch";
4594
6535
  /**
6536
+ * The Git panel's identity: the sidebar row id and the matching `main` panel
6537
+ * key. Package-local on purpose — the slot addresses stay inside the bundle.
6538
+ */
6539
+ const PANEL_ID = "git-history";
6540
+ /**
4595
6541
  * This package's own name — the key `plugins.bundle.config` dispatches by
4596
6542
  * (the slot contract keys a bundle's own configuration by the bundle's
4597
6543
  * package name; it matches the `cordis.patch.yml` entry verbatim).
@@ -4852,6 +6798,26 @@ window.__ModuleLoader__.load({
4852
6798
  unsubscribeDescribe();
4853
6799
  if (configDisposer !== void 0) configDisposer();
4854
6800
  }, "git-worktree: plugin config card lifecycle");
6801
+ ctx.inject(["betterSidebar"], (betterCtx) => {
6802
+ const better = betterCtx.betterSidebar;
6803
+ const panelT = ctx.locale.bind(NS);
6804
+ ctx.effect(() => better.registerTab({
6805
+ id: PANEL_ID,
6806
+ title: () => panelT("panel.nav"),
6807
+ description: () => panelT("panel.description"),
6808
+ icon: (size) => (0, react.createElement)("span", { style: {
6809
+ color: "#4c6bf5",
6810
+ display: "inline-flex"
6811
+ } }, (0, react.createElement)(_deepseek_ai_dsh_client_ui_primitives.IconBranchOutlineRegular, { size })),
6812
+ order: 150,
6813
+ single: true,
6814
+ component: (tabProps) => (0, react.createElement)(GitPanelTab, {
6815
+ ctx: betterCtx,
6816
+ scope: tabProps.scope,
6817
+ t: panelT
6818
+ })
6819
+ }), "git-worktree: better-sidebar tab");
6820
+ });
4855
6821
  }
4856
6822
  //#endregion
4857
6823
  exports.apply = apply;