dsh-git-branch-manage 1.0.6 → 1.0.8

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
@@ -129,6 +129,25 @@ window.__ModuleLoader__.load({
129
129
  const ROUTE_COMMIT_FILES = `${ROUTE_PREFIX}/commit-files`;
130
130
  /** POST ROUTE_PREFIX/tag — create a tag at an existing commit. */
131
131
  const ROUTE_TAG = `${ROUTE_PREFIX}/tag`;
132
+ /** POST ROUTE_PREFIX/tag-push — push ONE existing local tag to its remote. */
133
+ const ROUTE_TAG_PUSH = `${ROUTE_PREFIX}/tag-push`;
134
+ /** POST ROUTE_PREFIX/credentials — the global per-host credentials, secrets
135
+ * masked (the stored secret never leaves the host). */
136
+ const ROUTE_CREDENTIALS = `${ROUTE_PREFIX}/credentials`;
137
+ /** POST ROUTE_PREFIX/credentials-save — create or update one host credential. */
138
+ const ROUTE_CREDENTIALS_SAVE = `${ROUTE_PREFIX}/credentials-save`;
139
+ /** POST ROUTE_PREFIX/credentials-delete — forget one host credential. */
140
+ const ROUTE_CREDENTIALS_DELETE = `${ROUTE_PREFIX}/credentials-delete`;
141
+ /** POST ROUTE_PREFIX/credentials-oauth-start — begin a browser sign-in for
142
+ * one host (GitHub device flow); answers the one-time user code. */
143
+ const ROUTE_CREDENTIALS_OAUTH_START = `${ROUTE_PREFIX}/credentials-oauth-start`;
144
+ /** POST ROUTE_PREFIX/credentials-oauth-poll — poll a started browser sign-in;
145
+ * stores the token and answers the fresh masked list once the user approves. */
146
+ const ROUTE_CREDENTIALS_OAUTH_POLL = `${ROUTE_PREFIX}/credentials-oauth-poll`;
147
+ /** POST ROUTE_PREFIX/remotes — the repository's configured remotes. */
148
+ const ROUTE_REMOTES = `${ROUTE_PREFIX}/remotes`;
149
+ /** POST ROUTE_PREFIX/remote — add, re-point or remove ONE remote. */
150
+ const ROUTE_REMOTE = `${ROUTE_PREFIX}/remote`;
132
151
  /** POST ROUTE_PREFIX/ai-commit-message — one-shot LLM commit message from
133
152
  * the staged diff (mounted only where the host provides the llm service). */
134
153
  const ROUTE_AI_COMMIT_MESSAGE = `${ROUTE_PREFIX}/ai-commit-message`;
@@ -136,6 +155,10 @@ window.__ModuleLoader__.load({
136
155
  * bodies and the client caps its input at the same value, so the two ends
137
156
  * can never disagree. */
138
157
  const COMMIT_MESSAGE_LIMIT = 2e3;
158
+ /** Hosts whose browser sign-in the credential dialog offers. Mirrors the
159
+ * host-side device-flow support (`src/oauth.ts`); a shared constant keeps the
160
+ * browser list from drifting away from what the host can actually do. */
161
+ const BROWSER_SIGN_IN_HOSTS = ["github.com"];
139
162
  //#endregion
140
163
  //#region src/client/api.ts
141
164
  /** POST one JSON body and parse the uniform envelope. */
@@ -433,6 +456,72 @@ window.__ModuleLoader__.load({
433
456
  });
434
457
  }
435
458
  /**
459
+ * Push ONE local tag to its remote (the host resolves which remote). Unlike
460
+ * {@link requestPush}, which sends the current branch alone.
461
+ * @param repoPath - absolute directory inside the repository.
462
+ * @param name - an existing local tag name (verified host-side).
463
+ */
464
+ function requestPushTag(repoPath, name) {
465
+ return post(ROUTE_TAG_PUSH, {
466
+ repoPath,
467
+ name
468
+ });
469
+ }
470
+ /**
471
+ * Every stored GLOBAL credential, secrets masked. One row per host: a token
472
+ * saved for github.com serves every repository that talks to github.com.
473
+ */
474
+ function requestCredentials() {
475
+ return post(ROUTE_CREDENTIALS, {});
476
+ }
477
+ /**
478
+ * Create or update the credential of ONE host. A blank `secret` on an
479
+ * existing host keeps the stored secret.
480
+ * @param body - `{ host, username, method, secret? }`.
481
+ */
482
+ function requestSaveCredential(body) {
483
+ return post(ROUTE_CREDENTIALS_SAVE, body);
484
+ }
485
+ /**
486
+ * Forget ONE host's credential.
487
+ * @param body - `{ host }`.
488
+ */
489
+ function requestDeleteCredential(body) {
490
+ return post(ROUTE_CREDENTIALS_DELETE, body);
491
+ }
492
+ /**
493
+ * Start a browser sign-in for ONE host (GitHub device flow). The answer
494
+ * carries the one-time user code the dialog shows and the verification page
495
+ * it opens.
496
+ * @param body - `{ host }`.
497
+ */
498
+ function requestOAuthStart(body) {
499
+ return post(ROUTE_CREDENTIALS_OAUTH_START, body);
500
+ }
501
+ /**
502
+ * Poll a started browser sign-in. Resolves `{ status: 'pending' }` until the
503
+ * user approves; then the token is stored host-side and the fresh masked
504
+ * list comes back (the token itself never reaches the browser).
505
+ * @param body - `{ host, deviceCode }`.
506
+ */
507
+ function requestOAuthPoll(body) {
508
+ return post(ROUTE_CREDENTIALS_OAUTH_POLL, body);
509
+ }
510
+ /**
511
+ * The repository's configured remotes (fetch/push URLs).
512
+ * @param repoPath - absolute directory inside the repository.
513
+ */
514
+ function requestRemotes(repoPath) {
515
+ return post(ROUTE_REMOTES, { repoPath });
516
+ }
517
+ /**
518
+ * Add, re-point or remove one remote; resolves with the fresh list.
519
+ * @param body - `{ repoPath, action, name, url? }`.
520
+ */
521
+ function requestMutateRemote(body) {
522
+ return post(ROUTE_REMOTE, body);
523
+ }
524
+ /**
436
525
  * Generate a commit message from the checkout's diff with the requesting
437
526
  * session's own model (a one-shot host llm call).
438
527
  * @param body - the repo path plus the session's model selection and an
@@ -3635,6 +3724,14 @@ window.__ModuleLoader__.load({
3635
3724
  return badges;
3636
3725
  }
3637
3726
  /**
3727
+ * The TAG names one commit's decoration carries, in decoration order — the
3728
+ * per-tag publish targets the commit menu offers. Built on
3729
+ * {@link classifyRefs} so "what counts as a tag" has exactly one definition.
3730
+ */
3731
+ function commitTags(refs) {
3732
+ return classifyRefs(refs).filter((badge) => badge.kind === "tag").map((badge) => badge.text);
3733
+ }
3734
+ /**
3638
3735
  * Split the working-tree rows into the two panel groups. Untracked files
3639
3736
  * render in the unstaged group as `?` (git add still stages them); an
3640
3737
  * unmerged conflict renders in the unstaged group as `U` unchecked (git add
@@ -3847,7 +3944,7 @@ window.__ModuleLoader__.load({
3847
3944
  }
3848
3945
  //#endregion
3849
3946
  //#region \0git-worktree-css:/home/runner/work/dsh-git-branch/dsh-git-branch/src/client/GitHistoryPanel.module.css?inline
3850
- 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/* Cn-accelerate: the topbar 国内加速 button and its anchored popover. The\n * button's on-state is brand tint + brand text + green dot; the popover\n * mirrors the settings card's switch/input affordances over this panel's\n * tokens (popup chrome follows .menu_427795137 above). */\n\n.cnWrap_427795137 {\n position: relative;\n display: inline-flex;\n}\n\n.cnBtn_427795137,\n.cnBtnOn_427795137 {\n display: inline-flex;\n align-items: center;\n gap: 5px;\n height: 28px;\n padding: 0 10px;\n border: 0;\n border-radius: 8px;\n background: none;\n color: var(--dsw-alias-label-secondary);\n font: inherit;\n font-size: 12px;\n line-height: 1;\n cursor: pointer;\n}\n\n.cnBtn_427795137:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n color: var(--dsw-alias-label-primary);\n}\n\n.cnBtnOn_427795137 {\n background: rgba(76, 107, 245, 0.16);\n color: var(--dsw-alias-brand-primary);\n}\n\n.cnBtnOn_427795137:hover {\n background: rgba(76, 107, 245, 0.22);\n color: var(--dsw-alias-brand-primary);\n}\n\n.cnDot_427795137 {\n width: 6px;\n height: 6px;\n border-radius: 50%;\n background: var(--dsw-alias-status-success, #218358);\n}\n\n.cnPop_427795137 {\n position: absolute;\n right: 0;\n top: calc(100% + 6px);\n z-index: 40;\n width: 320px;\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: 12px;\n display: flex;\n flex-direction: column;\n gap: 8px;\n}\n\n.cnPopHead_427795137 {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 8px;\n}\n\n.cnPopTitle_427795137 {\n font-size: 13px;\n font-weight: 600;\n color: var(--dsw-alias-label-primary);\n}\n\n.cnSwitchRow_427795137 {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n font-size: 12px;\n color: var(--dsw-alias-label-secondary);\n cursor: pointer;\n}\n\n.cnSwitch_427795137 {\n width: 16px;\n height: 16px;\n accent-color: var(--dsw-alias-brand-primary);\n cursor: pointer;\n}\n\n.cnSwitch_427795137:disabled {\n cursor: default;\n}\n\n.cnHint_427795137 {\n margin: 0;\n font-size: 11px;\n line-height: 16px;\n color: var(--dsw-alias-label-tertiary);\n}\n\n.cnFieldHead_427795137 {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 8px;\n font-size: 12px;\n}\n\n.cnFieldLabel_427795137 {\n color: var(--dsw-alias-label-primary);\n font-weight: 500;\n}\n\n.cnLink_427795137 {\n border: 0;\n background: none;\n padding: 0;\n font: inherit;\n font-size: 12px;\n color: var(--dsw-alias-brand-primary);\n cursor: pointer;\n}\n\n.cnLink_427795137:hover {\n text-decoration: underline;\n}\n\n.cnLink_427795137:disabled {\n opacity: 0.5;\n cursor: default;\n text-decoration: none;\n}\n\n.cnInput_427795137,\n.cnInputBad_427795137 {\n box-sizing: border-box;\n width: 100%;\n border: 0.5px solid var(--dsw-alias-border-l4);\n border-radius: 6px;\n background: var(--dsw-alias-bg-layer-3);\n color: var(--dsw-alias-label-primary);\n font: inherit;\n font-size: 12px;\n padding: 6px 8px;\n outline: none;\n}\n\n.cnInput_427795137:focus {\n border-color: var(--dsw-alias-brand-primary);\n}\n\n.cnInputBad_427795137,\n.cnInputBad_427795137:focus {\n border-color: var(--dsw-alias-status-danger, #d5304f);\n}\n\n.cnError_427795137 {\n font-size: 11px;\n line-height: 16px;\n color: var(--dsw-alias-status-danger, #d5304f);\n}\n\n.cnFooter_427795137 {\n display: flex;\n justify-content: flex-end;\n gap: 6px;\n margin-top: 2px;\n}\n\n.cnGhost_427795137,\n.cnSave_427795137 {\n border: 0;\n border-radius: 6px;\n font: inherit;\n font-size: 12px;\n padding: 5px 12px;\n cursor: pointer;\n}\n\n.cnGhost_427795137 {\n background: none;\n color: var(--dsw-alias-label-secondary);\n}\n\n.cnGhost_427795137:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n color: var(--dsw-alias-label-primary);\n}\n\n.cnSave_427795137 {\n background: var(--dsw-alias-brand-primary);\n color: #fff;\n}\n\n.cnSave_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/* The head carries the title plus three filters (branch, author, time). The\n * row wraps at narrow widths rather than squeezing the date inputs into\n * unreadable slivers. */\n.historyFilters_427795137 {\n margin-left: auto;\n display: flex;\n align-items: center;\n gap: 6px;\n flex-wrap: wrap;\n justify-content: flex-end;\n}\n\n.filterSelect_427795137,\n.filterInput_427795137,\n.filterDate_427795137 {\n height: 26px;\n border: 0.5px solid var(--dsw-alias-border-l4);\n border-radius: 7px;\n background: var(--dsw-alias-bg-layer-3);\n color: var(--dsw-alias-label-primary);\n font: inherit;\n font-size: 12px;\n padding: 0 6px;\n outline: none;\n}\n\n.filterSelect_427795137 {\n max-width: 150px;\n}\n\n.filterInput_427795137 {\n width: 120px;\n}\n\n.filterInput_427795137::placeholder {\n color: var(--dsw-alias-label-tertiary);\n}\n\n.filterDate_427795137 {\n width: 122px;\n color-scheme: light dark;\n}\n\n.filterDash_427795137 {\n color: var(--dsw-alias-label-tertiary);\n font-size: 12px;\n}\n\n.commitList_427795137 {\n /* Keep the history body gap between blocks, never between graph rows.\n * A non-shrinking block also prevents a long history from compressing rows\n * inside the card's scrolling flex body. */\n flex: none;\n}\n\n.commitRow_427795137 {\n position: relative;\n display: grid;\n grid-template-columns: var(--graph-w, 22px) minmax(0, 1fr) auto;\n column-gap: 8px;\n align-items: center;\n padding: 0 8px 0 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/* The SVG fills the actual grid row, including the text cells' padding.\n * Its paths scale vertically; the separate dot keeps a constant radius. */\n.commitGraph_427795137 {\n position: relative;\n align-self: stretch;\n min-height: 44px;\n}\n\n.commitGraphSvg_427795137 {\n position: absolute;\n inset: 0;\n width: 100%;\n height: 100%;\n display: block;\n overflow: visible;\n}\n\n.commitDot_427795137 {\n position: absolute;\n top: 50%;\n width: 8px;\n height: 8px;\n box-sizing: border-box;\n border: 2px solid currentColor;\n border-radius: 50%;\n transform: translate(-50%, -50%);\n pointer-events: none;\n}\n\n.commitExpandGraph_427795137 {\n position: absolute;\n top: 0;\n left: 0;\n height: 100%;\n display: block;\n pointer-events: none;\n}\n\n.commitMain_427795137 {\n padding: 6px 0;\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 min-width: 0;\n}\n\n.commitAuthor_427795137 {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.commitSide_427795137 {\n padding: 6px 0;\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/* The absolute stamp column: tabular figures so every row's digits line up\n * under the one above, which is what makes the column scannable. */\n.commitDate_427795137 {\n font-variant-numeric: tabular-nums;\n white-space: nowrap;\n}\n\n.commitHash_427795137 {\n font-family: ui-monospace, Consolas, monospace;\n}\n\n.commitExpand_427795137 {\n position: relative;\n grid-column: 1 / -1;\n /* Align the expanded file list with the subject column, whatever width the\n * graph band needs in this repository. */\n padding: 2px 4px 8px calc(var(--graph-w, 22px) + 8px);\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";
3947
+ 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/* Cn-accelerate: the topbar 国内加速 button and its anchored popover. The\n * button's on-state is brand tint + brand text + green dot; the popover\n * mirrors the settings card's switch/input affordances over this panel's\n * tokens (popup chrome follows .menu_427795137 above). */\n\n.cnWrap_427795137 {\n position: relative;\n display: inline-flex;\n}\n\n.cnBtn_427795137,\n.cnBtnOn_427795137 {\n display: inline-flex;\n align-items: center;\n gap: 5px;\n height: 28px;\n padding: 0 10px;\n border: 0;\n border-radius: 8px;\n background: none;\n color: var(--dsw-alias-label-secondary);\n font: inherit;\n font-size: 12px;\n line-height: 1;\n cursor: pointer;\n}\n\n.cnBtn_427795137:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n color: var(--dsw-alias-label-primary);\n}\n\n.cnBtnOn_427795137 {\n background: rgba(76, 107, 245, 0.16);\n color: var(--dsw-alias-brand-primary);\n}\n\n.cnBtnOn_427795137:hover {\n background: rgba(76, 107, 245, 0.22);\n color: var(--dsw-alias-brand-primary);\n}\n\n.cnDot_427795137 {\n width: 6px;\n height: 6px;\n border-radius: 50%;\n background: var(--dsw-alias-status-success, #218358);\n}\n\n.cnPop_427795137 {\n position: absolute;\n right: 0;\n top: calc(100% + 6px);\n z-index: 40;\n width: 320px;\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: 12px;\n display: flex;\n flex-direction: column;\n gap: 8px;\n}\n\n.cnPopHead_427795137 {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 8px;\n}\n\n.cnPopTitle_427795137 {\n font-size: 13px;\n font-weight: 600;\n color: var(--dsw-alias-label-primary);\n}\n\n.cnSwitchRow_427795137 {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n font-size: 12px;\n color: var(--dsw-alias-label-secondary);\n cursor: pointer;\n}\n\n.cnSwitch_427795137 {\n width: 16px;\n height: 16px;\n accent-color: var(--dsw-alias-brand-primary);\n cursor: pointer;\n}\n\n.cnSwitch_427795137:disabled {\n cursor: default;\n}\n\n.cnHint_427795137 {\n margin: 0;\n font-size: 11px;\n line-height: 16px;\n color: var(--dsw-alias-label-tertiary);\n}\n\n.cnFieldHead_427795137 {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 8px;\n font-size: 12px;\n}\n\n.cnFieldLabel_427795137 {\n color: var(--dsw-alias-label-primary);\n font-weight: 500;\n}\n\n.cnLink_427795137 {\n border: 0;\n background: none;\n padding: 0;\n font: inherit;\n font-size: 12px;\n color: var(--dsw-alias-brand-primary);\n cursor: pointer;\n}\n\n.cnLink_427795137:hover {\n text-decoration: underline;\n}\n\n.cnLink_427795137:disabled {\n opacity: 0.5;\n cursor: default;\n text-decoration: none;\n}\n\n.cnInput_427795137,\n.cnInputBad_427795137 {\n box-sizing: border-box;\n width: 100%;\n border: 0.5px solid var(--dsw-alias-border-l4);\n border-radius: 6px;\n background: var(--dsw-alias-bg-layer-3);\n color: var(--dsw-alias-label-primary);\n font: inherit;\n font-size: 12px;\n padding: 6px 8px;\n outline: none;\n}\n\n.cnInput_427795137:focus {\n border-color: var(--dsw-alias-brand-primary);\n}\n\n.cnInputBad_427795137,\n.cnInputBad_427795137:focus {\n border-color: var(--dsw-alias-status-danger, #d5304f);\n}\n\n.cnError_427795137 {\n font-size: 11px;\n line-height: 16px;\n color: var(--dsw-alias-status-danger, #d5304f);\n}\n\n.cnFooter_427795137 {\n display: flex;\n justify-content: flex-end;\n gap: 6px;\n margin-top: 2px;\n}\n\n.cnGhost_427795137,\n.cnSave_427795137 {\n border: 0;\n border-radius: 6px;\n font: inherit;\n font-size: 12px;\n padding: 5px 12px;\n cursor: pointer;\n}\n\n.cnGhost_427795137 {\n background: none;\n color: var(--dsw-alias-label-secondary);\n}\n\n.cnGhost_427795137:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n color: var(--dsw-alias-label-primary);\n}\n\n.cnSave_427795137 {\n background: var(--dsw-alias-brand-primary);\n color: #fff;\n}\n\n.cnSave_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/* The head carries the title plus three filters (branch, author, time). The\n * row wraps at narrow widths rather than squeezing the date inputs into\n * unreadable slivers. */\n.historyFilters_427795137 {\n margin-left: auto;\n display: flex;\n align-items: center;\n gap: 6px;\n flex-wrap: wrap;\n justify-content: flex-end;\n}\n\n.filterSelect_427795137,\n.filterInput_427795137,\n.filterDate_427795137 {\n height: 26px;\n border: 0.5px solid var(--dsw-alias-border-l4);\n border-radius: 7px;\n background: var(--dsw-alias-bg-layer-3);\n color: var(--dsw-alias-label-primary);\n font: inherit;\n font-size: 12px;\n padding: 0 6px;\n outline: none;\n}\n\n.filterSelect_427795137 {\n max-width: 150px;\n}\n\n.filterInput_427795137 {\n width: 120px;\n}\n\n.filterInput_427795137::placeholder {\n color: var(--dsw-alias-label-tertiary);\n}\n\n.filterDate_427795137 {\n width: 122px;\n color-scheme: light dark;\n}\n\n.filterDash_427795137 {\n color: var(--dsw-alias-label-tertiary);\n font-size: 12px;\n}\n\n.commitList_427795137 {\n /* Keep the history body gap between blocks, never between graph rows.\n * A non-shrinking block also prevents a long history from compressing rows\n * inside the card's scrolling flex body. */\n flex: none;\n}\n\n.commitRow_427795137 {\n position: relative;\n display: grid;\n grid-template-columns: var(--graph-w, 22px) minmax(0, 1fr) auto;\n column-gap: 8px;\n align-items: center;\n padding: 0 8px 0 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/* The SVG fills the actual grid row, including the text cells' padding.\n * Its paths scale vertically; the separate dot keeps a constant radius. */\n.commitGraph_427795137 {\n position: relative;\n align-self: stretch;\n min-height: 44px;\n}\n\n.commitGraphSvg_427795137 {\n position: absolute;\n inset: 0;\n width: 100%;\n height: 100%;\n display: block;\n overflow: visible;\n}\n\n.commitDot_427795137 {\n position: absolute;\n top: 50%;\n width: 8px;\n height: 8px;\n box-sizing: border-box;\n border: 2px solid currentColor;\n border-radius: 50%;\n transform: translate(-50%, -50%);\n pointer-events: none;\n}\n\n.commitExpandGraph_427795137 {\n position: absolute;\n top: 0;\n left: 0;\n height: 100%;\n display: block;\n pointer-events: none;\n}\n\n.commitMain_427795137 {\n padding: 6px 0;\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 min-width: 0;\n}\n\n.commitAuthor_427795137 {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.commitSide_427795137 {\n padding: 6px 0;\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/* The absolute stamp column: tabular figures so every row's digits line up\n * under the one above, which is what makes the column scannable. */\n.commitDate_427795137 {\n font-variant-numeric: tabular-nums;\n white-space: nowrap;\n}\n\n.commitHash_427795137 {\n font-family: ui-monospace, Consolas, monospace;\n}\n\n.commitExpand_427795137 {\n position: relative;\n grid-column: 1 / -1;\n /* Align the expanded file list with the subject column, whatever width the\n * graph band needs in this repository. */\n padding: 2px 4px 8px calc(var(--graph-w, 22px) + 8px);\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\n/* Credential + remote dialogs. Both share one scaffold: a compact list of\n * stored rows above a single inline editor (the panel never stacks modals),\n * so the classes are named for the credential case and reused by remotes. */\n\n.credHint_427795137 {\n margin: 0 0 10px;\n font-size: 12px;\n line-height: 1.6;\n color: var(--dsw-alias-label-secondary);\n}\n\n.credList_427795137 {\n display: flex;\n flex-direction: column;\n gap: 2px;\n max-height: 220px;\n overflow-y: auto;\n margin-bottom: 4px;\n}\n\n.credRow_427795137 {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 6px 8px;\n border-radius: 8px;\n}\n\n.credRow_427795137:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.credMain_427795137 {\n display: flex;\n flex-direction: column;\n gap: 2px;\n min-width: 0;\n flex: 1;\n}\n\n.credHost_427795137 {\n font-size: 13px;\n font-weight: 500;\n color: var(--dsw-alias-label-primary);\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.credMeta_427795137 {\n font-size: 11.5px;\n color: var(--dsw-alias-label-tertiary);\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.credMeta_427795137 code {\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n}\n\n.credActions_427795137 {\n display: flex;\n gap: 2px;\n flex: none;\n}\n\n.credDivider_427795137 {\n height: 0.5px;\n background: var(--dsw-alias-border-l2);\n margin: 10px 0;\n}\n\n.credFormTitle_427795137 {\n font-size: 12px;\n font-weight: 500;\n color: var(--dsw-alias-label-secondary);\n margin-bottom: 8px;\n}\n\n/* The editor's fields: the Modal body has no gap of its own, so the stack\n * carries the rhythm instead of one margin per field. */\n.credForm_427795137 {\n display: flex;\n flex-direction: column;\n gap: 10px;\n margin-top: 2px;\n}\n\n.credPresets_427795137 {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: 6px;\n}\n\n.credPresetLabel_427795137 {\n font-size: 12px;\n color: var(--dsw-alias-label-secondary);\n margin-right: 2px;\n}\n\n.credPreset_427795137,\n.credPresetOn_427795137 {\n font: inherit;\n font-size: 12px;\n border: 0.5px solid var(--dsw-alias-border-l4);\n border-radius: 999px;\n padding: 2px 10px;\n background: transparent;\n color: var(--dsw-alias-label-secondary);\n cursor: pointer;\n}\n\n.credPreset_427795137:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.credPresetOn_427795137 {\n border-color: var(--dsw-alias-brand-primary);\n color: var(--dsw-alias-brand-primary);\n}\n\n.credMethodRow_427795137 {\n display: flex;\n gap: 6px;\n}\n\n.credMethod_427795137,\n.credMethodOn_427795137 {\n font: inherit;\n font-size: 12px;\n border: 0.5px solid var(--dsw-alias-border-l4);\n border-radius: 8px;\n padding: 4px 12px;\n background: var(--dsw-alias-bg-layer-3);\n color: var(--dsw-alias-label-secondary);\n cursor: pointer;\n}\n\n.credMethodOn_427795137 {\n border-color: var(--dsw-alias-brand-primary);\n color: var(--dsw-alias-brand-primary);\n}\n\n.credFieldHead_427795137 {\n display: flex;\n align-items: baseline;\n justify-content: space-between;\n gap: 8px;\n}\n\n.credLink_427795137 {\n font-size: 12px;\n color: var(--dsw-alias-brand-primary);\n text-decoration: none;\n}\n\n.credLink_427795137:hover {\n text-decoration: underline;\n}\n\n/* The token field's trailing 「创建」 button: the input keeps the row's width\n * and the button hugs its end, mirroring the browser token page it opens. */\n.credSecretRow_427795137 {\n display: flex;\n align-items: center;\n gap: 6px;\n}\n\n.credSecretRow_427795137 .formInput_427795137 {\n flex: 1;\n min-width: 0;\n}\n\n/* The browser sign-in block: the only control on that authentication path —\n * no username or secret field sits above it any more. */\n.credOAuth_427795137 {\n display: flex;\n flex-direction: column;\n gap: 6px;\n}\n\n/* The browser sign-in block: the one-time code the user types into the page\n * the dialog just opened, plus its waiting state. */\n.credDevice_427795137 {\n display: flex;\n flex-direction: column;\n gap: 5px;\n padding: 8px 10px;\n border-radius: 8px;\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.credDeviceRow_427795137 {\n display: flex;\n align-items: baseline;\n gap: 8px;\n}\n\n.credDeviceCode_427795137 {\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-size: 15px;\n font-weight: 600;\n letter-spacing: 1px;\n color: var(--dsw-alias-label-primary);\n}\n\n.credDeviceHint_427795137 {\n font-size: 11.5px;\n color: var(--dsw-alias-label-tertiary);\n}\n\n.credDeviceWaiting_427795137 {\n font-size: 11.5px;\n color: var(--dsw-alias-label-secondary);\n}\n\n/* The editor's action row: both actions sit on the right, cancel nearest the\n * form, save outermost. Only while the form itself is open. */\n.credFooter_427795137 {\n display: flex;\n justify-content: flex-end;\n align-items: center;\n gap: 6px;\n margin-top: 10px;\n}\n\n.credTop_427795137 {\n display: flex;\n align-items: flex-start;\n gap: 8px;\n margin-bottom: 10px;\n}\n\n.credTop_427795137 .credHint_427795137 {\n flex: 1;\n margin: 0;\n}\n\n.credAdd_427795137 {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex: none;\n width: 26px;\n height: 26px;\n padding: 0;\n border: 0.5px solid var(--dsw-alias-border-l2);\n border-radius: 7px;\n background: none;\n color: var(--dsw-alias-label-secondary);\n font: inherit;\n font-size: 16px;\n line-height: 1;\n cursor: pointer;\n}\n\n.credAdd_427795137:hover {\n border-color: var(--dsw-alias-brand-primary);\n color: var(--dsw-alias-brand-primary);\n}\n";
3851
3948
  const tagId$2 = "dsh-git-branch-manage/GitHistoryPanel.module.css";
3852
3949
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=\"" + tagId$2 + "\"]") === null) {
3853
3950
  const tag = document.createElement("style");
@@ -3984,7 +4081,36 @@ window.__ModuleLoader__.load({
3984
4081
  "fileDel": "fileDel_427795137",
3985
4082
  "empty": "empty_427795137",
3986
4083
  "errorText": "errorText_427795137",
3987
- "moreRow": "moreRow_427795137"
4084
+ "moreRow": "moreRow_427795137",
4085
+ "credHint": "credHint_427795137",
4086
+ "credList": "credList_427795137",
4087
+ "credRow": "credRow_427795137",
4088
+ "credMain": "credMain_427795137",
4089
+ "credHost": "credHost_427795137",
4090
+ "credMeta": "credMeta_427795137",
4091
+ "credActions": "credActions_427795137",
4092
+ "credDivider": "credDivider_427795137",
4093
+ "credFormTitle": "credFormTitle_427795137",
4094
+ "credForm": "credForm_427795137",
4095
+ "credPresets": "credPresets_427795137",
4096
+ "credPresetLabel": "credPresetLabel_427795137",
4097
+ "credPreset": "credPreset_427795137",
4098
+ "credPresetOn": "credPresetOn_427795137",
4099
+ "credMethodRow": "credMethodRow_427795137",
4100
+ "credMethod": "credMethod_427795137",
4101
+ "credMethodOn": "credMethodOn_427795137",
4102
+ "credFieldHead": "credFieldHead_427795137",
4103
+ "credLink": "credLink_427795137",
4104
+ "credSecretRow": "credSecretRow_427795137",
4105
+ "credOAuth": "credOAuth_427795137",
4106
+ "credDevice": "credDevice_427795137",
4107
+ "credDeviceRow": "credDeviceRow_427795137",
4108
+ "credDeviceCode": "credDeviceCode_427795137",
4109
+ "credDeviceHint": "credDeviceHint_427795137",
4110
+ "credDeviceWaiting": "credDeviceWaiting_427795137",
4111
+ "credFooter": "credFooter_427795137",
4112
+ "credTop": "credTop_427795137",
4113
+ "credAdd": "credAdd_427795137"
3988
4114
  };
3989
4115
  //#endregion
3990
4116
  //#region src/client/panel/FloatMenu.tsx
@@ -4146,7 +4272,7 @@ window.__ModuleLoader__.load({
4146
4272
  }
4147
4273
  /** The branches section. */
4148
4274
  function BranchSection(props) {
4149
- const { t, locals, remotes, busy, onCheckout, onRename, onDelete, onCreate } = props;
4275
+ const { t, locals, remotes, busy, onCheckout, onRename, onDelete, onCreate, onManageRemotes } = props;
4150
4276
  const [menu, setMenu] = (0, react.useState)(null);
4151
4277
  const [dialog, setDialog] = (0, react.useState)(null);
4152
4278
  const [draft, setDraft] = (0, react.useState)("");
@@ -4348,6 +4474,13 @@ window.__ModuleLoader__.load({
4348
4474
  onClick: collapseAll,
4349
4475
  children: t("menuCollapseAll")
4350
4476
  }),
4477
+ onManageRemotes === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4478
+ type: "button",
4479
+ className: GitHistoryPanel_module_default.treeToggle,
4480
+ disabled: busy,
4481
+ onClick: onManageRemotes,
4482
+ children: t("panel.remotes")
4483
+ }),
4351
4484
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
4352
4485
  type: "button",
4353
4486
  className: GitHistoryPanel_module_default.act,
@@ -4588,6 +4721,528 @@ window.__ModuleLoader__.load({
4588
4721
  });
4589
4722
  }
4590
4723
  //#endregion
4724
+ //#region src/client/panel/CredentialModal.tsx
4725
+ /**
4726
+ * CredentialModal: the GLOBAL git credential editor behind the topbar's
4727
+ * 「密钥配置」 button.
4728
+ *
4729
+ * Credentials are keyed by remote HOST, not by repository: one github.com
4730
+ * token serves every checkout on this machine. The list therefore never
4731
+ * shows a secret — the host routes return a mask only — and the editor
4732
+ * treats an empty secret as "keep the stored one", which is why the field's
4733
+ * placeholder changes once a host already has a credential.
4734
+ *
4735
+ * Validation is split: this component only rejects empty fields (instant
4736
+ * feedback); the host/secret shape is decided by the host half
4737
+ * (normalizeCredentialHost), whose message rides the error envelope back
4738
+ * here. Nothing in this file imports `../../credentials.ts` — that module
4739
+ * touches node:fs, which has no place in the browser bundle.
4740
+ *
4741
+ * Authentication has two paths and no password pair: device-flow hosts open
4742
+ * the browser (the token lands host-side, so the dialog only sees the mask),
4743
+ * every other host takes a pasted token. GitHub retired password auth in
4744
+ * 2021 and GitLab/Gitee expect a PAT, so a username/password form only
4745
+ * collected credentials the hosts reject.
4746
+ *
4747
+ * @module git-worktree/client/panel/CredentialModal
4748
+ */
4749
+ /** The hosts the dialog offers as one-click presets (custom hosts are typed
4750
+ * in by hand and validated host-side). */
4751
+ const PRESET_HOSTS = [
4752
+ "github.com",
4753
+ "gitlab.com",
4754
+ "gitee.com"
4755
+ ];
4756
+ /** Where each preset host mints a token. Only token-based hosts get a link:
4757
+ * a self-hosted GitLab or Gitee clone has no well-known settings page. */
4758
+ const TOKEN_PAGES = {
4759
+ "github.com": "https://github.com/settings/tokens/new?description=dsh-git-branch",
4760
+ "gitlab.com": "https://gitlab.com/-/user_settings/personal_access_tokens",
4761
+ "gitee.com": "https://gitee.com/profile/personal_access_tokens"
4762
+ };
4763
+ /** One polling interval, kept as a promise so the loop stays sequential. */
4764
+ function delay(ms) {
4765
+ return new Promise((resolve) => {
4766
+ setTimeout(resolve, ms);
4767
+ });
4768
+ }
4769
+ /** Consecutive unreachable polls before the dialog gives up. One TLS blip —
4770
+ * the reset keep-alive connection this environment sees — must not discard a
4771
+ * grant the user already approved in the browser. */
4772
+ const OAUTH_NETWORK_RETRY_LIMIT = 5;
4773
+ /** The credential editor dialog. Loads the global list on mount and after
4774
+ * every write; each write returns the fresh masked list, so no reload loop
4775
+ * (and no stale row) is possible. */
4776
+ function CredentialModal(props) {
4777
+ const { t, pushToast, onClose } = props;
4778
+ const [entries, setEntries] = (0, react.useState)([]);
4779
+ const [loaded, setLoaded] = (0, react.useState)(false);
4780
+ const [loadError, setLoadError] = (0, react.useState)();
4781
+ const [host, setHost] = (0, react.useState)("");
4782
+ const [method, setMethod] = (0, react.useState)("token");
4783
+ const [secret, setSecret] = (0, react.useState)("");
4784
+ const [seed, setSeed] = (0, react.useState)();
4785
+ const [formError, setFormError] = (0, react.useState)();
4786
+ const [busy, setBusy] = (0, react.useState)(false);
4787
+ /** Two-step delete: the first click only arms the row. */
4788
+ const [confirmHost, setConfirmHost] = (0, react.useState)();
4789
+ /** The editor starts collapsed; the "+" icon in the header row opens it,
4790
+ * and the row's 编辑 button re-opens it prefilled. Keeping the form out of
4791
+ * the DOM until it is asked for leaves the list as the modal's default. */
4792
+ const [editorOpen, setEditorOpen] = (0, react.useState)(false);
4793
+ /** The browser sign-in currently shown under the password fields. */
4794
+ const [oauth, setOauth] = (0, react.useState)({ phase: "idle" });
4795
+ /** Generation counter: one bump abandons an in-flight poll loop (cancel,
4796
+ * close, unmount), so a late answer cannot revive a closed editor. */
4797
+ const oauthRun = (0, react.useRef)(0);
4798
+ (0, react.useEffect)(() => () => {
4799
+ oauthRun.current += 1;
4800
+ }, []);
4801
+ const load = (0, react.useCallback)(async () => {
4802
+ const result = await requestCredentials();
4803
+ if (!result.ok) {
4804
+ setLoadError(result.error);
4805
+ setLoaded(true);
4806
+ return;
4807
+ }
4808
+ setEntries(result.credentials);
4809
+ setLoadError(void 0);
4810
+ setLoaded(true);
4811
+ }, []);
4812
+ (0, react.useEffect)(() => {
4813
+ load();
4814
+ }, [load]);
4815
+ const applyEditor = (0, react.useCallback)((next) => {
4816
+ oauthRun.current += 1;
4817
+ setOauth({ phase: "idle" });
4818
+ setSeed(next);
4819
+ setHost(next === void 0 ? "" : next.host);
4820
+ setMethod(next === void 0 ? "token" : next.method === "token" ? "token" : "browser");
4821
+ setSecret("");
4822
+ setFormError(void 0);
4823
+ setConfirmHost(void 0);
4824
+ }, []);
4825
+ const openNew = (0, react.useCallback)(() => {
4826
+ applyEditor(void 0);
4827
+ setEditorOpen(true);
4828
+ }, [applyEditor]);
4829
+ /** Cancel closes the form only; the modal keeps the list it was opened for. */
4830
+ const closeEditor = (0, react.useCallback)(() => {
4831
+ applyEditor(void 0);
4832
+ setEditorOpen(false);
4833
+ }, [applyEditor]);
4834
+ /** Switching path abandons whatever sign-in the other path was waiting on,
4835
+ * so a late poll answer cannot rewrite the form the user now sees. */
4836
+ const chooseMode = (0, react.useCallback)((next) => {
4837
+ if (next === method) return;
4838
+ oauthRun.current += 1;
4839
+ setOauth({ phase: "idle" });
4840
+ setMethod(next);
4841
+ }, [method]);
4842
+ const applyPreset = (0, react.useCallback)((preset) => {
4843
+ const existing = entries.find((entry) => entry.host === preset);
4844
+ if (existing !== void 0) {
4845
+ applyEditor(existing);
4846
+ return;
4847
+ }
4848
+ applyEditor(void 0);
4849
+ setHost(preset);
4850
+ setMethod(BROWSER_SIGN_IN_HOSTS.includes(preset) ? "browser" : "token");
4851
+ }, [applyEditor, entries]);
4852
+ const normalizedHost = host.trim().toLowerCase();
4853
+ const existing = entries.find((entry) => entry.host === normalizedHost);
4854
+ const tokenPage = TOKEN_PAGES[normalizedHost];
4855
+ /** Device flow is only implemented for a few hosts; the rest get the token
4856
+ * field alone, with no method switch that could promise a dead button. */
4857
+ const browserSignIn = BROWSER_SIGN_IN_HOSTS.includes(normalizedHost);
4858
+ /** The mode actually rendered: without device flow the token field wins,
4859
+ * whatever stale selection the form was carrying. */
4860
+ const authMode = browserSignIn ? method : "token";
4861
+ const save = (0, react.useCallback)(async () => {
4862
+ if (busy) return;
4863
+ const trimmedSecret = secret.trim();
4864
+ if (host.trim() === "") {
4865
+ setFormError(t("cred.hostEmpty"));
4866
+ return;
4867
+ }
4868
+ if (existing === void 0 && trimmedSecret === "") {
4869
+ setFormError(t("cred.secretEmpty"));
4870
+ return;
4871
+ }
4872
+ setBusy(true);
4873
+ const result = await requestSaveCredential({
4874
+ host: host.trim(),
4875
+ username: "",
4876
+ method: "token",
4877
+ ...trimmedSecret === "" ? {} : { secret: trimmedSecret }
4878
+ });
4879
+ setBusy(false);
4880
+ if (!result.ok) {
4881
+ setFormError(result.error);
4882
+ return;
4883
+ }
4884
+ setEntries(result.credentials);
4885
+ pushToast(t("cred.saved", { host: host.trim() }), "success");
4886
+ applyEditor(void 0);
4887
+ setEditorOpen(false);
4888
+ }, [
4889
+ applyEditor,
4890
+ busy,
4891
+ existing,
4892
+ host,
4893
+ pushToast,
4894
+ secret,
4895
+ t
4896
+ ]);
4897
+ /**
4898
+ * IDEA-style site sign-in: start a device-flow grant, open the verification
4899
+ * page, then poll until the user approves. The token is stored host-side,
4900
+ * so the dialog only ever receives the refreshed masked list.
4901
+ */
4902
+ const connect = (0, react.useCallback)(async () => {
4903
+ if (busy || oauth.phase !== "idle" || !browserSignIn) return;
4904
+ const target = normalizedHost;
4905
+ if (target === "") {
4906
+ setFormError(t("cred.hostEmpty"));
4907
+ return;
4908
+ }
4909
+ const run = ++oauthRun.current;
4910
+ setFormError(void 0);
4911
+ setOauth({ phase: "connecting" });
4912
+ const tab = window.open("", "_blank");
4913
+ const started = await requestOAuthStart({ host: target });
4914
+ if (oauthRun.current !== run) {
4915
+ tab?.close();
4916
+ return;
4917
+ }
4918
+ if (!started.ok) {
4919
+ tab?.close();
4920
+ setOauth({ phase: "idle" });
4921
+ setFormError(started.error);
4922
+ return;
4923
+ }
4924
+ const page = started.verificationUriComplete ?? started.verificationUri;
4925
+ if (tab === null || tab.closed) window.open(page, "_blank", "noopener,noreferrer");
4926
+ else {
4927
+ tab.opener = null;
4928
+ tab.location.href = page;
4929
+ }
4930
+ setOauth({
4931
+ phase: "waiting",
4932
+ userCode: started.userCode,
4933
+ verificationUri: started.verificationUri
4934
+ });
4935
+ const deadline = Date.now() + started.expiresInSeconds * 1e3;
4936
+ let intervalMs = Math.max(started.intervalSeconds, 1) * 1e3;
4937
+ let networkFailures = 0;
4938
+ while (Date.now() < deadline) {
4939
+ await delay(intervalMs);
4940
+ if (oauthRun.current !== run) return;
4941
+ const polled = await requestOAuthPoll({
4942
+ host: target,
4943
+ deviceCode: started.deviceCode
4944
+ });
4945
+ if (oauthRun.current !== run) return;
4946
+ if (!polled.ok) {
4947
+ setOauth({ phase: "idle" });
4948
+ setFormError(polled.error);
4949
+ return;
4950
+ }
4951
+ if (polled.status === "network") {
4952
+ networkFailures += 1;
4953
+ if (networkFailures >= OAUTH_NETWORK_RETRY_LIMIT) {
4954
+ setOauth({ phase: "idle" });
4955
+ setFormError(t("cred.deviceNetwork", { host: target }));
4956
+ return;
4957
+ }
4958
+ continue;
4959
+ }
4960
+ networkFailures = 0;
4961
+ if (polled.status === "pending") continue;
4962
+ if (polled.status === "slow_down") {
4963
+ intervalMs = Math.min(intervalMs * 2, 6e4);
4964
+ continue;
4965
+ }
4966
+ setEntries(polled.credentials);
4967
+ setOauth({ phase: "idle" });
4968
+ applyEditor(void 0);
4969
+ setEditorOpen(false);
4970
+ pushToast(t("cred.saved", { host: target }), "success");
4971
+ return;
4972
+ }
4973
+ if (oauthRun.current !== run) return;
4974
+ setOauth({ phase: "idle" });
4975
+ setFormError(t("cred.deviceExpired"));
4976
+ }, [
4977
+ applyEditor,
4978
+ browserSignIn,
4979
+ busy,
4980
+ normalizedHost,
4981
+ oauth.phase,
4982
+ pushToast,
4983
+ t
4984
+ ]);
4985
+ const remove = (0, react.useCallback)(async (target) => {
4986
+ if (busy) return;
4987
+ setBusy(true);
4988
+ const result = await requestDeleteCredential({ host: target });
4989
+ setBusy(false);
4990
+ if (!result.ok) {
4991
+ setFormError(result.error);
4992
+ setConfirmHost(void 0);
4993
+ return;
4994
+ }
4995
+ setEntries(result.credentials);
4996
+ pushToast(t("cred.deleted", { host: target }), "success");
4997
+ setConfirmHost(void 0);
4998
+ if (seed?.host === target) {
4999
+ applyEditor(void 0);
5000
+ setEditorOpen(false);
5001
+ }
5002
+ }, [
5003
+ applyEditor,
5004
+ busy,
5005
+ pushToast,
5006
+ seed,
5007
+ t
5008
+ ]);
5009
+ const title = seed === void 0 ? t("cred.add") : t("cred.edit");
5010
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
5011
+ open: true,
5012
+ onClose,
5013
+ title: t("cred.title"),
5014
+ closeLabel: t("cred.cancel"),
5015
+ footer: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
5016
+ size: "sm",
5017
+ variant: "ghost",
5018
+ onClick: onClose,
5019
+ children: t("close")
5020
+ }),
5021
+ children: [
5022
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5023
+ className: GitHistoryPanel_module_default.credTop,
5024
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
5025
+ className: GitHistoryPanel_module_default.credHint,
5026
+ children: t("cred.hint")
5027
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
5028
+ type: "button",
5029
+ className: GitHistoryPanel_module_default.credAdd,
5030
+ title: t("cred.add"),
5031
+ "aria-label": t("cred.add"),
5032
+ "aria-expanded": editorOpen,
5033
+ onClick: openNew,
5034
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5035
+ "aria-hidden": "true",
5036
+ children: "+"
5037
+ })
5038
+ })]
5039
+ }),
5040
+ loadError !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5041
+ className: GitHistoryPanel_module_default.formError,
5042
+ children: loadError
5043
+ }) : null,
5044
+ loaded && loadError === void 0 && entries.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
5045
+ className: GitHistoryPanel_module_default.empty,
5046
+ children: t("cred.empty")
5047
+ }) : null,
5048
+ entries.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5049
+ className: GitHistoryPanel_module_default.credList,
5050
+ children: entries.map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5051
+ className: GitHistoryPanel_module_default.credRow,
5052
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5053
+ className: GitHistoryPanel_module_default.credMain,
5054
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5055
+ className: GitHistoryPanel_module_default.credHost,
5056
+ children: entry.host
5057
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
5058
+ className: GitHistoryPanel_module_default.credMeta,
5059
+ children: [
5060
+ entry.username === "" ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [entry.username, " · "] }),
5061
+ entry.method === "token" ? t("cred.methodToken") : t("cred.methodPassword"),
5062
+ " · ",
5063
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", { children: entry.secretMask })
5064
+ ]
5065
+ })]
5066
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5067
+ className: GitHistoryPanel_module_default.credActions,
5068
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
5069
+ size: "sm",
5070
+ variant: "ghost",
5071
+ onClick: () => {
5072
+ applyEditor(entry);
5073
+ setEditorOpen(true);
5074
+ },
5075
+ children: t("cred.edit")
5076
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
5077
+ size: "sm",
5078
+ variant: "ghost",
5079
+ disabled: busy,
5080
+ onClick: () => confirmHost === entry.host ? void remove(entry.host) : setConfirmHost(entry.host),
5081
+ children: confirmHost === entry.host ? t("cred.confirmDelete") : t("cred.delete")
5082
+ })]
5083
+ })]
5084
+ }, entry.host))
5085
+ }) : null,
5086
+ editorOpen ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
5087
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: GitHistoryPanel_module_default.credDivider }),
5088
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5089
+ className: GitHistoryPanel_module_default.credFormTitle,
5090
+ children: title
5091
+ }),
5092
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5093
+ className: GitHistoryPanel_module_default.credForm,
5094
+ children: [
5095
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5096
+ className: GitHistoryPanel_module_default.credPresets,
5097
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5098
+ className: GitHistoryPanel_module_default.credPresetLabel,
5099
+ children: t("cred.presets")
5100
+ }), PRESET_HOSTS.map((preset) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
5101
+ type: "button",
5102
+ className: normalizedHost === preset ? GitHistoryPanel_module_default.credPresetOn : GitHistoryPanel_module_default.credPreset,
5103
+ onClick: () => applyPreset(preset),
5104
+ children: preset
5105
+ }, preset))]
5106
+ }),
5107
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5108
+ className: GitHistoryPanel_module_default.formField,
5109
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
5110
+ className: GitHistoryPanel_module_default.formLabel,
5111
+ htmlFor: "git-credential-host",
5112
+ children: t("cred.hostLabel")
5113
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
5114
+ id: "git-credential-host",
5115
+ className: GitHistoryPanel_module_default.formInput,
5116
+ value: host,
5117
+ placeholder: t("cred.hostPlaceholder"),
5118
+ "data-modal-autofocus": seed === void 0 ? true : void 0,
5119
+ onChange: (event) => setHost(event.target.value)
5120
+ })]
5121
+ }),
5122
+ browserSignIn ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5123
+ className: GitHistoryPanel_module_default.formField,
5124
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5125
+ className: GitHistoryPanel_module_default.formLabel,
5126
+ children: t("cred.method")
5127
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5128
+ className: GitHistoryPanel_module_default.credMethodRow,
5129
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
5130
+ type: "button",
5131
+ "aria-pressed": authMode === "browser",
5132
+ className: authMode === "browser" ? GitHistoryPanel_module_default.credMethodOn : GitHistoryPanel_module_default.credMethod,
5133
+ onClick: () => chooseMode("browser"),
5134
+ children: t("cred.methodBrowser")
5135
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
5136
+ type: "button",
5137
+ "aria-pressed": authMode === "token",
5138
+ className: authMode === "token" ? GitHistoryPanel_module_default.credMethodOn : GitHistoryPanel_module_default.credMethod,
5139
+ onClick: () => chooseMode("token"),
5140
+ children: t("cred.methodToken")
5141
+ })]
5142
+ })]
5143
+ }) : null,
5144
+ authMode === "token" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5145
+ className: GitHistoryPanel_module_default.formField,
5146
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5147
+ className: GitHistoryPanel_module_default.credFieldHead,
5148
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
5149
+ className: GitHistoryPanel_module_default.formLabel,
5150
+ htmlFor: "git-credential-secret",
5151
+ children: t("cred.secretTokenLabel")
5152
+ })
5153
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5154
+ className: GitHistoryPanel_module_default.credSecretRow,
5155
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
5156
+ id: "git-credential-secret",
5157
+ className: GitHistoryPanel_module_default.formInput,
5158
+ type: "password",
5159
+ value: secret,
5160
+ placeholder: existing === void 0 ? t("cred.secretPlaceholder") : t("cred.secretKeep"),
5161
+ onChange: (event) => setSecret(event.target.value),
5162
+ onKeyDown: (event) => {
5163
+ if (event.key !== "Enter") return;
5164
+ event.preventDefault();
5165
+ save();
5166
+ }
5167
+ }), tokenPage !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
5168
+ size: "sm",
5169
+ variant: "outline",
5170
+ onClick: () => window.open(tokenPage, "_blank", "noopener,noreferrer"),
5171
+ children: t("cred.createToken")
5172
+ }) : null]
5173
+ })]
5174
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5175
+ className: GitHistoryPanel_module_default.credOAuth,
5176
+ children: [
5177
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5178
+ className: GitHistoryPanel_module_default.credDeviceHint,
5179
+ children: t("cred.browserHint")
5180
+ }),
5181
+ oauth.phase === "idle" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
5182
+ size: "sm",
5183
+ variant: "primary",
5184
+ disabled: busy,
5185
+ onClick: () => void connect(),
5186
+ children: t("cred.connect")
5187
+ }) : null,
5188
+ oauth.phase === "connecting" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5189
+ className: GitHistoryPanel_module_default.credDeviceHint,
5190
+ children: t("cred.connecting")
5191
+ }) : null,
5192
+ oauth.phase === "waiting" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5193
+ className: GitHistoryPanel_module_default.credDevice,
5194
+ children: [
5195
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5196
+ className: GitHistoryPanel_module_default.credDeviceRow,
5197
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5198
+ className: GitHistoryPanel_module_default.credDeviceCode,
5199
+ children: oauth.userCode ?? ""
5200
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
5201
+ className: GitHistoryPanel_module_default.credLink,
5202
+ href: oauth.verificationUri,
5203
+ target: "_blank",
5204
+ rel: "noreferrer",
5205
+ children: oauth.verificationUri
5206
+ })]
5207
+ }),
5208
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5209
+ className: GitHistoryPanel_module_default.credDeviceHint,
5210
+ children: t("cred.deviceHint")
5211
+ }),
5212
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5213
+ className: GitHistoryPanel_module_default.credDeviceWaiting,
5214
+ children: t("cred.deviceWaiting")
5215
+ })
5216
+ ]
5217
+ }) : null
5218
+ ]
5219
+ }),
5220
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5221
+ className: GitHistoryPanel_module_default.formError,
5222
+ children: formError ?? ""
5223
+ })
5224
+ ]
5225
+ }),
5226
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5227
+ className: GitHistoryPanel_module_default.credFooter,
5228
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
5229
+ size: "sm",
5230
+ variant: "outline",
5231
+ onClick: closeEditor,
5232
+ children: t("cancel")
5233
+ }), authMode === "token" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
5234
+ size: "sm",
5235
+ variant: "primary",
5236
+ disabled: busy,
5237
+ onClick: () => void save(),
5238
+ children: busy ? t("cred.saving") : t("cred.save")
5239
+ }) : null]
5240
+ })
5241
+ ] }) : null
5242
+ ]
5243
+ });
5244
+ }
5245
+ //#endregion
4591
5246
  //#region src/client/git-graph.ts
4592
5247
  /** Lazily extend `lanes` so `index` exists. */
4593
5248
  function ensureLane(lanes, index) {
@@ -4772,8 +5427,11 @@ window.__ModuleLoader__.load({
4772
5427
  * author, and the absolute commit time. CLICKING a row expands the commit's
4773
5428
  * changed files INLINE below it (exactly one open at a time), loading them on
4774
5429
  * first open; RIGHT-CLICKING it opens the commit menu (copy hash, copy
4775
- * subject, new branch here, tag here), which is where the history card's two
4776
- * writing verbs live.
5430
+ * subject, new branch here, tag here, and — for every tag the commit already
5431
+ * carries — push that tag), which is where the history card's writing verbs
5432
+ * live. Pushing a tag is offered per EXISTING tag rather than folded into the
5433
+ * branch push: `git push` sends the branch alone, so a tag on an
5434
+ * already-pushed commit would otherwise have no way out of the local repo.
4777
5435
  *
4778
5436
  * The head carries the three filters the card asks for, in this order —
4779
5437
  * author, branch (a ref to pin the walk on, or every ref), and a time window
@@ -4941,7 +5599,7 @@ window.__ModuleLoader__.load({
4941
5599
  return `${commit.subject} · ${commit.authorName} <${commit.authorEmail}> · ${absolute} · ${commit.short}`;
4942
5600
  }
4943
5601
  function HistorySection(props) {
4944
- const { t, commits, loaded, loadError, loading, hasMore, loadingMore, expanded, filesByHash, filters, branches, busy, onLoadMore, onToggle, onFilters, onCreateBranch, onCreateTag, pushToast } = props;
5602
+ const { t, commits, loaded, loadError, loading, hasMore, loadingMore, expanded, filesByHash, filters, branches, busy, onLoadMore, onToggle, onFilters, onCreateBranch, onCreateTag, onPushTag, pushToast } = props;
4945
5603
  const [menu, setMenu] = (0, react.useState)(null);
4946
5604
  const [dialog, setDialog] = (0, react.useState)(null);
4947
5605
  const [draft, setDraft] = (0, react.useState)("");
@@ -5103,6 +5761,8 @@ window.__ModuleLoader__.load({
5103
5761
  })
5104
5762
  }, commit.hash));
5105
5763
  const dialogTitle = dialog === null ? "" : dialog.kind === "branch" ? t("createFlyAskFrom", { branch: dialog.commit.short }) : t("panel.tagTitle");
5764
+ /** Tags the right-clicked commit carries: one publish item each. */
5765
+ const menuTags = menu === null ? [] : commitTags(menu.commit.refs);
5106
5766
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
5107
5767
  className: `${GitHistoryPanel_module_default.section} ${GitHistoryPanel_module_default.sectionFill} ${GitHistoryPanel_module_default.sectionHistory}`,
5108
5768
  "aria-label": t("panel.history"),
@@ -5177,7 +5837,18 @@ window.__ModuleLoader__.load({
5177
5837
  disabled: busy,
5178
5838
  onClick: () => openDialog("tag", menu.commit),
5179
5839
  children: t("panel.branchTag")
5180
- })
5840
+ }),
5841
+ menuTags.map((tag) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
5842
+ type: "button",
5843
+ className: GitHistoryPanel_module_default.menuItem,
5844
+ role: "menuitem",
5845
+ disabled: busy,
5846
+ onClick: () => {
5847
+ setMenu(null);
5848
+ onPushTag(tag);
5849
+ },
5850
+ children: t("panel.tagPush", { tag })
5851
+ }, tag))
5181
5852
  ]
5182
5853
  }) : null,
5183
5854
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
@@ -5241,6 +5912,286 @@ window.__ModuleLoader__.load({
5241
5912
  });
5242
5913
  }
5243
5914
  //#endregion
5915
+ //#region src/client/panel/RemotesModal.tsx
5916
+ /**
5917
+ * RemotesModal: the repository's remote-address editor (behind the branch
5918
+ * section's 「管理远程」 button).
5919
+ *
5920
+ * A remote belongs to the REPOSITORY, not to a worktree, so this dialog
5921
+ * mutates `.git/config` through the plugin's remote routes and then reports
5922
+ * the fresh list back to the caller — the panel reloads its branch rows so
5923
+ * a new remote's branches show up without a manual refresh.
5924
+ *
5925
+ * Every mutation returns the complete list (the host half re-reads
5926
+ * `git remote -v` after writing), so the dialog never guesses what changed.
5927
+ * Names are immutable here: `git remote rename` also rewrites tracking
5928
+ * config, which this panel does not offer; the URL is the mutable part.
5929
+ *
5930
+ * The dialog follows the credential editor's interaction shape, so the two
5931
+ * modals read as one design: the list is the default view, a 「+」 in the
5932
+ * header row opens the editor, and the row's edit button re-opens it
5933
+ * prefilled. Cancel closes the editor only — the modal itself stays open.
5934
+ *
5935
+ * @module git-worktree/client/panel/RemotesModal
5936
+ */
5937
+ /** The remote editor dialog. */
5938
+ function RemotesModal(props) {
5939
+ const { repoPath, t, pushToast, onClose, onChanged } = props;
5940
+ const [remotes, setRemotes] = (0, react.useState)([]);
5941
+ const [loaded, setLoaded] = (0, react.useState)(false);
5942
+ const [loadError, setLoadError] = (0, react.useState)();
5943
+ const [name, setName] = (0, react.useState)("");
5944
+ const [url, setUrl] = (0, react.useState)("");
5945
+ /** The remote whose URL is being rewritten (undefined = adding a new one). */
5946
+ const [editing, setEditing] = (0, react.useState)();
5947
+ const [formError, setFormError] = (0, react.useState)();
5948
+ const [busy, setBusy] = (0, react.useState)(false);
5949
+ /** Two-step remove: the first click only arms the row. */
5950
+ const [confirmName, setConfirmName] = (0, react.useState)();
5951
+ /** The editor starts collapsed; the 「+」 in the header row opens it, and
5952
+ * the row's 修改地址 button re-opens it prefilled. Keeping the form out of
5953
+ * the DOM until it is asked for leaves the list as the modal's default —
5954
+ * the same shape the credential editor uses. */
5955
+ const [editorOpen, setEditorOpen] = (0, react.useState)(false);
5956
+ const load = (0, react.useCallback)(async () => {
5957
+ const result = await requestRemotes(repoPath);
5958
+ if (!result.ok) {
5959
+ setLoadError(result.error);
5960
+ setLoaded(true);
5961
+ return;
5962
+ }
5963
+ setRemotes(result.remotes);
5964
+ setLoadError(void 0);
5965
+ setLoaded(true);
5966
+ }, [repoPath]);
5967
+ (0, react.useEffect)(() => {
5968
+ load();
5969
+ }, [load]);
5970
+ const resetEditor = (0, react.useCallback)(() => {
5971
+ setEditing(void 0);
5972
+ setName("");
5973
+ setUrl("");
5974
+ setFormError(void 0);
5975
+ setConfirmName(void 0);
5976
+ setEditorOpen(false);
5977
+ }, []);
5978
+ /** The 「+」 button: a fresh, empty editor for a new remote. */
5979
+ const openNew = (0, react.useCallback)(() => {
5980
+ resetEditor();
5981
+ setEditorOpen(true);
5982
+ }, [resetEditor]);
5983
+ const startEdit = (0, react.useCallback)((entry) => {
5984
+ setEditing(entry.name);
5985
+ setName(entry.name);
5986
+ setUrl(entry.fetchUrl);
5987
+ setFormError(void 0);
5988
+ setConfirmName(void 0);
5989
+ setEditorOpen(true);
5990
+ }, []);
5991
+ const submit = (0, react.useCallback)(async () => {
5992
+ if (busy) return;
5993
+ if (name.trim() === "") {
5994
+ setFormError(t("remotes.nameEmpty"));
5995
+ return;
5996
+ }
5997
+ if (url.trim() === "") {
5998
+ setFormError(t("remotes.urlEmpty"));
5999
+ return;
6000
+ }
6001
+ setBusy(true);
6002
+ const result = await requestMutateRemote({
6003
+ repoPath,
6004
+ action: editing === void 0 ? "add" : "set-url",
6005
+ name: name.trim(),
6006
+ url: url.trim()
6007
+ });
6008
+ setBusy(false);
6009
+ if (!result.ok) {
6010
+ setFormError(result.error);
6011
+ return;
6012
+ }
6013
+ setRemotes(result.remotes);
6014
+ pushToast(t("remotes.saved", { name: name.trim() }), "success");
6015
+ resetEditor();
6016
+ onChanged?.();
6017
+ }, [
6018
+ busy,
6019
+ editing,
6020
+ name,
6021
+ onChanged,
6022
+ pushToast,
6023
+ repoPath,
6024
+ resetEditor,
6025
+ t,
6026
+ url
6027
+ ]);
6028
+ const remove = (0, react.useCallback)(async (target) => {
6029
+ if (busy) return;
6030
+ setBusy(true);
6031
+ const result = await requestMutateRemote({
6032
+ repoPath,
6033
+ action: "remove",
6034
+ name: target
6035
+ });
6036
+ setBusy(false);
6037
+ if (!result.ok) {
6038
+ setFormError(result.error);
6039
+ setConfirmName(void 0);
6040
+ return;
6041
+ }
6042
+ setRemotes(result.remotes);
6043
+ pushToast(t("remotes.removed", { name: target }), "success");
6044
+ setConfirmName(void 0);
6045
+ if (editing === target) resetEditor();
6046
+ onChanged?.();
6047
+ }, [
6048
+ busy,
6049
+ editing,
6050
+ onChanged,
6051
+ pushToast,
6052
+ repoPath,
6053
+ resetEditor,
6054
+ t
6055
+ ]);
6056
+ const editorTitle = editing === void 0 ? t("remotes.add") : `${t("remotes.edit")} · ${editing}`;
6057
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
6058
+ open: true,
6059
+ onClose,
6060
+ title: t("remotes.title"),
6061
+ closeLabel: t("remotes.cancel"),
6062
+ footer: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
6063
+ size: "sm",
6064
+ variant: "ghost",
6065
+ onClick: onClose,
6066
+ children: t("close")
6067
+ }),
6068
+ children: [
6069
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6070
+ className: GitHistoryPanel_module_default.credTop,
6071
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
6072
+ className: GitHistoryPanel_module_default.credHint,
6073
+ children: t("remotes.hint")
6074
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
6075
+ type: "button",
6076
+ className: GitHistoryPanel_module_default.credAdd,
6077
+ title: t("remotes.add"),
6078
+ "aria-label": t("remotes.add"),
6079
+ "aria-expanded": editorOpen,
6080
+ onClick: openNew,
6081
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6082
+ "aria-hidden": "true",
6083
+ children: "+"
6084
+ })
6085
+ })]
6086
+ }),
6087
+ loadError !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
6088
+ className: GitHistoryPanel_module_default.formError,
6089
+ children: loadError
6090
+ }) : null,
6091
+ loaded && loadError === void 0 && remotes.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
6092
+ className: GitHistoryPanel_module_default.empty,
6093
+ children: t("remotes.empty")
6094
+ }) : null,
6095
+ remotes.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
6096
+ className: GitHistoryPanel_module_default.credList,
6097
+ children: remotes.map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6098
+ className: GitHistoryPanel_module_default.credRow,
6099
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6100
+ className: GitHistoryPanel_module_default.credMain,
6101
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6102
+ className: GitHistoryPanel_module_default.credHost,
6103
+ children: entry.name
6104
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6105
+ className: GitHistoryPanel_module_default.credMeta,
6106
+ children: entry.fetchUrl === entry.pushUrl ? entry.fetchUrl : `${t("remotes.fetchUrl")}: ${entry.fetchUrl} · ${t("remotes.pushUrl")}: ${entry.pushUrl}`
6107
+ })]
6108
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6109
+ className: GitHistoryPanel_module_default.credActions,
6110
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
6111
+ size: "sm",
6112
+ variant: "ghost",
6113
+ onClick: () => startEdit(entry),
6114
+ children: t("remotes.edit")
6115
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
6116
+ size: "sm",
6117
+ variant: "ghost",
6118
+ disabled: busy,
6119
+ onClick: () => confirmName === entry.name ? void remove(entry.name) : setConfirmName(entry.name),
6120
+ children: confirmName === entry.name ? t("remotes.confirmRemove") : t("remotes.remove")
6121
+ })]
6122
+ })]
6123
+ }, entry.name))
6124
+ }) : null,
6125
+ editorOpen ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
6126
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: GitHistoryPanel_module_default.credDivider }),
6127
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
6128
+ className: GitHistoryPanel_module_default.credFormTitle,
6129
+ children: editorTitle
6130
+ }),
6131
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6132
+ className: GitHistoryPanel_module_default.credForm,
6133
+ children: [
6134
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6135
+ className: GitHistoryPanel_module_default.formField,
6136
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
6137
+ className: GitHistoryPanel_module_default.formLabel,
6138
+ htmlFor: "git-remote-name",
6139
+ children: t("remotes.nameLabel")
6140
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
6141
+ id: "git-remote-name",
6142
+ className: GitHistoryPanel_module_default.formInput,
6143
+ value: name,
6144
+ placeholder: t("remotes.namePlaceholder"),
6145
+ readOnly: editing !== void 0,
6146
+ "data-modal-autofocus": editing === void 0 ? true : void 0,
6147
+ onChange: (event) => setName(event.target.value)
6148
+ })]
6149
+ }),
6150
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6151
+ className: GitHistoryPanel_module_default.formField,
6152
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
6153
+ className: GitHistoryPanel_module_default.formLabel,
6154
+ htmlFor: "git-remote-url",
6155
+ children: t("remotes.urlLabel")
6156
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
6157
+ id: "git-remote-url",
6158
+ className: GitHistoryPanel_module_default.formInput,
6159
+ value: url,
6160
+ placeholder: t("remotes.urlPlaceholder"),
6161
+ onChange: (event) => setUrl(event.target.value),
6162
+ onKeyDown: (event) => {
6163
+ if (event.key !== "Enter") return;
6164
+ event.preventDefault();
6165
+ submit();
6166
+ }
6167
+ })]
6168
+ }),
6169
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
6170
+ className: GitHistoryPanel_module_default.formError,
6171
+ children: formError ?? ""
6172
+ })
6173
+ ]
6174
+ }),
6175
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6176
+ className: GitHistoryPanel_module_default.credFooter,
6177
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
6178
+ size: "sm",
6179
+ variant: "outline",
6180
+ onClick: resetEditor,
6181
+ children: t("cancel")
6182
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
6183
+ size: "sm",
6184
+ variant: "primary",
6185
+ disabled: busy,
6186
+ onClick: () => void submit(),
6187
+ children: busy ? t("remotes.saving") : t("remotes.save")
6188
+ })]
6189
+ })
6190
+ ] }) : null
6191
+ ]
6192
+ });
6193
+ }
6194
+ //#endregion
5244
6195
  //#region src/client/panel/UncommittedSection.tsx
5245
6196
  /**
5246
6197
  * UncommittedSection: the panel's first block — the staged/unstaged file
@@ -5542,6 +6493,8 @@ window.__ModuleLoader__.load({
5542
6493
  const [expanded, setExpanded] = (0, react.useState)(null);
5543
6494
  const [filesByHash, setFilesByHash] = (0, react.useState)({});
5544
6495
  const [sectionBusy, setSectionBusy] = (0, react.useState)(false);
6496
+ /** The remote-address editor (branch card's 「管理远程」 entry). */
6497
+ const [remotesOpen, setRemotesOpen] = (0, react.useState)(false);
5545
6498
  const [syncBusy, setSyncBusy] = (0, react.useState)(false);
5546
6499
  const [commitMenuOpen, setCommitMenuOpen] = (0, react.useState)(false);
5547
6500
  const [commitDraft, setCommitDraft] = (0, react.useState)("");
@@ -5600,21 +6553,28 @@ window.__ModuleLoader__.load({
5600
6553
  (0, react.useEffect)(() => onGitMutated(refresh), [refresh]);
5601
6554
  (0, react.useImperativeHandle)(ref, () => ({ refresh }), [refresh]);
5602
6555
  const repo = status !== null && status.repo ? status : null;
5603
- /** Run one mutating verb with the section-busy guard and error toast.
6556
+ /** Run one mutating verb with the error toast and the cross-surface notice.
5604
6557
  * Mutations refresh through `loadAll` so their re-read bumps the request
5605
6558
  * sequence — an older in-flight snapshot can never land over the fresh
5606
6559
  * post-mutation state. A SUCCESS also emits the cross-surface notice so
5607
6560
  * the composer chip drops the branch git just left (stage toggles ride
5608
- * along — one extra idempotent /status beats per-verb wiring). */
5609
- const runGuarded = (0, react.useCallback)(async (verb) => {
5610
- setSectionBusy(true);
6561
+ * along — one extra idempotent /status beats per-verb wiring).
6562
+ *
6563
+ * `scope` names what the verb touches: `'section'` (default) claims the
6564
+ * working-tree busy gate — the commit box's primary button reads
6565
+ * 「提交中…」 while it is on. A tag verb (create or push) passes `'repo'`:
6566
+ * it neither reads nor writes the working tree, so lighting that gate
6567
+ * would report a commit that is not happening. */
6568
+ const runGuarded = (0, react.useCallback)(async (verb, scope = "section") => {
6569
+ const section = scope === "section";
6570
+ if (section) setSectionBusy(true);
5611
6571
  try {
5612
6572
  await verb();
5613
6573
  emitGitMutated();
5614
6574
  } catch (error) {
5615
6575
  pushToast(t("errorGeneric", { message: failureText(error) }), "error");
5616
6576
  } finally {
5617
- setSectionBusy(false);
6577
+ if (section) setSectionBusy(false);
5618
6578
  }
5619
6579
  }, [pushToast, t]);
5620
6580
  const onToggleRow = (0, react.useCallback)((row) => {
@@ -5856,14 +6816,16 @@ window.__ModuleLoader__.load({
5856
6816
  ]);
5857
6817
  /** Tag one commit. Runs against the repository (a tag is a repository-wide
5858
6818
  * ref), so the history card can tag a commit that no checkout has. Only
5859
- * local: pushing tags is a release step, not a side effect of a right-click. */
6819
+ * local: pushing tags is a release step, not a side effect of a right-click.
6820
+ * `'repo'`: a tag write leaves the working tree untouched — it must not
6821
+ * turn the commit box's primary button into 「提交中…」. */
5860
6822
  const onCreateTag = (0, react.useCallback)((name, target, message) => {
5861
6823
  runGuarded(async () => {
5862
6824
  const result = await requestCreateTag(cwd, name, target, message);
5863
6825
  if (!result.ok) throw new Error(result.error);
5864
6826
  pushToast(t("panel.tagDone", { tag: result.tag }), "success");
5865
6827
  await loadAll();
5866
- });
6828
+ }, "repo");
5867
6829
  }, [
5868
6830
  cwd,
5869
6831
  loadAll,
@@ -5871,6 +6833,28 @@ window.__ModuleLoader__.load({
5871
6833
  runGuarded,
5872
6834
  t
5873
6835
  ]);
6836
+ /** Publish ONE local tag. Its own verb, apart from the branch push: `git
6837
+ * push` sends the branch alone, so a tag made on an already-pushed commit
6838
+ * would otherwise never reach the remote. Deliberately no refetch: tags are
6839
+ * local refs and a remote tag changes nothing the panel can see. */
6840
+ const onPushTag = (0, react.useCallback)((name) => {
6841
+ runGuarded(async () => {
6842
+ const result = await requestPushTag(cwd, name);
6843
+ if (!result.ok) throw new Error(result.error);
6844
+ pushToast(result.pushed ? t("panel.tagPushDone", {
6845
+ tag: result.tag,
6846
+ remote: result.remote
6847
+ }) : t("panel.tagPushUpToDate", {
6848
+ tag: result.tag,
6849
+ remote: result.remote
6850
+ }), "success");
6851
+ }, "repo");
6852
+ }, [
6853
+ cwd,
6854
+ pushToast,
6855
+ runGuarded,
6856
+ t
6857
+ ]);
5874
6858
  const onToggleCommit = (0, react.useCallback)((hash) => {
5875
6859
  if (expanded === hash) {
5876
6860
  setExpanded(null);
@@ -5981,7 +6965,8 @@ window.__ModuleLoader__.load({
5981
6965
  onCheckout,
5982
6966
  onRename,
5983
6967
  onDelete,
5984
- onCreate
6968
+ onCreate,
6969
+ onManageRemotes: () => setRemotesOpen(true)
5985
6970
  }),
5986
6971
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(HistorySection, {
5987
6972
  t,
@@ -6001,8 +6986,16 @@ window.__ModuleLoader__.load({
6001
6986
  onFilters: onHistoryFilters,
6002
6987
  onCreateBranch: onCreate,
6003
6988
  onCreateTag,
6989
+ onPushTag,
6004
6990
  pushToast
6005
- })
6991
+ }),
6992
+ remotesOpen ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RemotesModal, {
6993
+ repoPath: cwd,
6994
+ t,
6995
+ pushToast,
6996
+ onClose: () => setRemotesOpen(false),
6997
+ onChanged: () => void loadAll()
6998
+ }) : null
6006
6999
  ]
6007
7000
  });
6008
7001
  });
@@ -6030,6 +7023,7 @@ window.__ModuleLoader__.load({
6030
7023
  }, []);
6031
7024
  const accelState = (0, react.useSyncExternalStore)(accel.store.subscribe, accel.store.getSnapshot);
6032
7025
  const [accelOpen, setAccelOpen] = (0, react.useState)(false);
7026
+ const [credOpen, setCredOpen] = (0, react.useState)(false);
6033
7027
  const accelOn = (0, react.useCallback)(() => accel.store.getSnapshot().enabled, [accel]);
6034
7028
  const closeAccel = (0, react.useCallback)(() => {
6035
7029
  accel.discardAccel();
@@ -6048,46 +7042,66 @@ window.__ModuleLoader__.load({
6048
7042
  })
6049
7043
  }) : null, /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6050
7044
  className: GitHistoryPanel_module_default.topbarActions,
6051
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6052
- className: GitHistoryPanel_module_default.cnWrap,
6053
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
7045
+ children: [
7046
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7047
+ className: GitHistoryPanel_module_default.cnWrap,
7048
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
7049
+ type: "button",
7050
+ className: accelState.enabled ? GitHistoryPanel_module_default.cnBtnOn : GitHistoryPanel_module_default.cnBtn,
7051
+ title: t("panel.cnAccelerate"),
7052
+ "aria-label": t("panel.cnAccelerate"),
7053
+ "aria-expanded": accelOpen,
7054
+ onClick: () => accelOpen ? closeAccel() : setAccelOpen(true),
7055
+ children: [
7056
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
7057
+ "aria-hidden": "true",
7058
+ children: "⚡"
7059
+ }),
7060
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("panel.cnAccelerate") }),
7061
+ accelState.enabled ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
7062
+ className: GitHistoryPanel_module_default.cnDot,
7063
+ "aria-hidden": "true"
7064
+ }) : null
7065
+ ]
7066
+ }), accelOpen ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CnAcceleratePopover, {
7067
+ accel,
7068
+ t,
7069
+ pushToast,
7070
+ onClose: closeAccel
7071
+ }) : null]
7072
+ }),
7073
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
6054
7074
  type: "button",
6055
- className: accelState.enabled ? GitHistoryPanel_module_default.cnBtnOn : GitHistoryPanel_module_default.cnBtn,
6056
- title: t("panel.cnAccelerate"),
6057
- "aria-label": t("panel.cnAccelerate"),
6058
- "aria-expanded": accelOpen,
6059
- onClick: () => accelOpen ? closeAccel() : setAccelOpen(true),
6060
- children: [
6061
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6062
- "aria-hidden": "true",
6063
- children: "⚡"
6064
- }),
6065
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("panel.cnAccelerate") }),
6066
- accelState.enabled ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6067
- className: GitHistoryPanel_module_default.cnDot,
6068
- "aria-hidden": "true"
6069
- }) : null
6070
- ]
6071
- }), accelOpen ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CnAcceleratePopover, {
6072
- accel,
6073
- t,
6074
- pushToast,
6075
- onClose: closeAccel
6076
- }) : null]
6077
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
6078
- type: "button",
6079
- className: GitHistoryPanel_module_default.refreshBtn,
6080
- disabled: cwd === void 0,
6081
- title: t("panel.refresh"),
6082
- "aria-label": t("panel.refresh"),
6083
- onClick: () => dataRef.current?.refresh(),
6084
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconRefreshOutlineRegular, {
6085
- size: 14,
6086
- "aria-hidden": "true"
7075
+ className: credOpen ? GitHistoryPanel_module_default.cnBtnOn : GitHistoryPanel_module_default.cnBtn,
7076
+ title: t("panel.credentials"),
7077
+ "aria-label": t("panel.credentials"),
7078
+ "aria-expanded": credOpen,
7079
+ onClick: () => setCredOpen((open) => !open),
7080
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
7081
+ "aria-hidden": "true",
7082
+ children: "🔑"
7083
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("panel.credentials") })]
7084
+ }),
7085
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
7086
+ type: "button",
7087
+ className: GitHistoryPanel_module_default.refreshBtn,
7088
+ disabled: cwd === void 0,
7089
+ title: t("panel.refresh"),
7090
+ "aria-label": t("panel.refresh"),
7091
+ onClick: () => dataRef.current?.refresh(),
7092
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconRefreshOutlineRegular, {
7093
+ size: 14,
7094
+ "aria-hidden": "true"
7095
+ })
6087
7096
  })
6088
- })]
7097
+ ]
6089
7098
  })]
6090
7099
  }),
7100
+ credOpen ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CredentialModal, {
7101
+ t,
7102
+ pushToast,
7103
+ onClose: () => setCredOpen(false)
7104
+ }) : null,
6091
7105
  cwd === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
6092
7106
  className: GitHistoryPanel_module_default.empty,
6093
7107
  children: t("panel.noSession")
@@ -7389,6 +8403,9 @@ window.__ModuleLoader__.load({
7389
8403
  "panel.tagName": "Tag name",
7390
8404
  "panel.tagMessage": "Message (optional)",
7391
8405
  "panel.tagDone": "Tagged {tag}",
8406
+ "panel.tagPush": "Push tag {tag}",
8407
+ "panel.tagPushDone": "Pushed tag {tag} to {remote}",
8408
+ "panel.tagPushUpToDate": "Tag {tag} is already on {remote}",
7392
8409
  "panel.noCommits": "This repository has no commits yet",
7393
8410
  "panel.noMatches": "No matching commits",
7394
8411
  "panel.loadMore": "Load more",
@@ -7412,7 +8429,72 @@ window.__ModuleLoader__.load({
7412
8429
  "accel.saveFailed": "The change did not save. Try again.",
7413
8430
  "accel.savedOn": "China acceleration enabled: {url}",
7414
8431
  "accel.savedOff": "China acceleration disabled",
7415
- "accel.mirrorDownHint": "If the mirror is down, disable acceleration or change the URL"
8432
+ "accel.mirrorDownHint": "If the mirror is down, disable acceleration or change the URL",
8433
+ "panel.credentials": "Credentials",
8434
+ "cred.title": "Git credentials",
8435
+ "cred.hint": "Global, one entry per host: a token saved for github.com serves every repository. Stored only on this machine in git-credentials.json.",
8436
+ "cred.empty": "No credentials saved yet",
8437
+ "cred.hostLabel": "Host",
8438
+ "cred.hostPlaceholder": "github.com",
8439
+ "cred.presets": "Common hosts",
8440
+ "cred.customPreset": "Custom host",
8441
+ "cred.secretTokenLabel": "Token",
8442
+ "cred.secretKeep": "A secret is already saved — leave blank to keep it",
8443
+ "cred.secretPlaceholder": "Paste a personal access token",
8444
+ "cred.method": "Authentication",
8445
+ "cred.methodToken": "Token",
8446
+ "cred.methodBrowser": "Browser sign-in",
8447
+ "cred.methodPassword": "Password",
8448
+ "cred.getToken": "Get a token",
8449
+ "cred.add": "Add credential",
8450
+ "cred.edit": "Edit",
8451
+ "cred.save": "Save",
8452
+ "cred.saving": "Saving…",
8453
+ "cred.delete": "Delete",
8454
+ "cred.deleting": "Deleting…",
8455
+ "cred.confirmDelete": "Delete for good",
8456
+ "cred.cancel": "Cancel",
8457
+ "cred.saved": "Saved the credential for {host}",
8458
+ "cred.deleted": "Deleted the credential for {host}",
8459
+ "cred.loadFailed": "Could not read the credential store",
8460
+ "cred.hostInvalid": "Enter a host such as github.com",
8461
+ "cred.hostEmpty": "Enter a host",
8462
+ "cred.secretEmpty": "Enter a token for a new host",
8463
+ "cred.connect": "Sign in with browser",
8464
+ "cred.connecting": "Signing in…",
8465
+ "cred.browserHint": "Approve access in the browser window; the token is saved for this host automatically.",
8466
+ "cred.deviceCode": "Verification code",
8467
+ "cred.deviceHint": "Enter this code in the browser window that just opened",
8468
+ "cred.deviceWaiting": "Waiting for approval…",
8469
+ "cred.deviceExpired": "The code expired — start the sign-in again",
8470
+ "cred.deviceDenied": "The sign-in was denied",
8471
+ "cred.deviceFailed": "The browser sign-in failed: {error}",
8472
+ "cred.deviceNetwork": "Could not reach {host} — check your network or proxy and try again",
8473
+ "cred.createToken": "Create",
8474
+ "panel.remotes": "Remotes",
8475
+ "remotes.title": "Remote addresses",
8476
+ "remotes.hint": "Remotes belong to the whole repository and apply to every worktree of it.",
8477
+ "remotes.empty": "No remote is configured",
8478
+ "remotes.add": "Add remote",
8479
+ "remotes.nameLabel": "Name",
8480
+ "remotes.namePlaceholder": "origin",
8481
+ "remotes.urlLabel": "Address",
8482
+ "remotes.urlPlaceholder": "https://github.com/user/repo.git",
8483
+ "remotes.fetchUrl": "Fetch",
8484
+ "remotes.pushUrl": "Push",
8485
+ "remotes.save": "Save",
8486
+ "remotes.saving": "Saving…",
8487
+ "remotes.edit": "Change address",
8488
+ "remotes.remove": "Remove",
8489
+ "remotes.confirmRemove": "Remove for good",
8490
+ "remotes.cancel": "Cancel",
8491
+ "remotes.saved": "Updated the remote {name}",
8492
+ "remotes.removed": "Removed the remote {name}",
8493
+ "remotes.loadFailed": "Could not read the remotes",
8494
+ "remotes.nameEmpty": "Enter a name",
8495
+ "remotes.nameInvalid": "Only letters, digits, \".\", \"_\" and \"-\" are allowed",
8496
+ "remotes.urlEmpty": "Enter an address",
8497
+ "remotes.urlInvalid": "Enter a valid remote address"
7416
8498
  };
7417
8499
  /** 中文词典。 */
7418
8500
  const zh = {
@@ -7607,6 +8689,9 @@ window.__ModuleLoader__.load({
7607
8689
  "panel.tagName": "标签名称",
7608
8690
  "panel.tagMessage": "消息(可选)",
7609
8691
  "panel.tagDone": "已标记 {tag}",
8692
+ "panel.tagPush": "推送标签 {tag}",
8693
+ "panel.tagPushDone": "已推送标签 {tag} 到 {remote}",
8694
+ "panel.tagPushUpToDate": "标签 {tag} 已存在于 {remote}",
7610
8695
  "panel.noCommits": "仓库还没有提交",
7611
8696
  "panel.noMatches": "没有匹配的提交",
7612
8697
  "panel.loadMore": "加载更多",
@@ -7630,7 +8715,72 @@ window.__ModuleLoader__.load({
7630
8715
  "accel.saveFailed": "保存未生效,请重试。",
7631
8716
  "accel.savedOn": "已启用国内加速:{url}",
7632
8717
  "accel.savedOff": "已关闭国内加速",
7633
- "accel.mirrorDownHint": "若加速地址不可用,可停用国内加速或更换地址"
8718
+ "accel.mirrorDownHint": "若加速地址不可用,可停用国内加速或更换地址",
8719
+ "panel.credentials": "密钥配置",
8720
+ "cred.title": "Git 密钥配置",
8721
+ "cred.hint": "全局生效:同一域名只需配置一次,该域名的所有仓库共用。密钥只保存在本机 git-credentials.json 中。",
8722
+ "cred.empty": "还没有配置任何密钥",
8723
+ "cred.hostLabel": "域名",
8724
+ "cred.hostPlaceholder": "github.com",
8725
+ "cred.presets": "常用平台",
8726
+ "cred.customPreset": "自定义域名",
8727
+ "cred.secretTokenLabel": "令牌",
8728
+ "cred.secretKeep": "已保存密钥,留空表示不修改",
8729
+ "cred.secretPlaceholder": "粘贴个人访问令牌",
8730
+ "cred.method": "认证方式",
8731
+ "cred.methodToken": "令牌",
8732
+ "cred.methodBrowser": "浏览器登录",
8733
+ "cred.methodPassword": "密码",
8734
+ "cred.getToken": "获取令牌",
8735
+ "cred.add": "新增密钥",
8736
+ "cred.edit": "编辑",
8737
+ "cred.save": "保存",
8738
+ "cred.saving": "保存中…",
8739
+ "cred.delete": "删除",
8740
+ "cred.deleting": "删除中…",
8741
+ "cred.confirmDelete": "确认删除",
8742
+ "cred.cancel": "取消",
8743
+ "cred.saved": "已保存 {host} 的密钥",
8744
+ "cred.deleted": "已删除 {host} 的密钥",
8745
+ "cred.loadFailed": "读取密钥失败",
8746
+ "cred.hostInvalid": "请输入类似 github.com 的域名",
8747
+ "cred.hostEmpty": "请输入域名",
8748
+ "cred.secretEmpty": "新域名必须填写令牌",
8749
+ "cred.connect": "点击跳转登录",
8750
+ "cred.connecting": "正在跳转…",
8751
+ "cred.browserHint": "在浏览器窗口中完成授权,成功后会自动保存该域名的令牌。",
8752
+ "cred.deviceCode": "验证码",
8753
+ "cred.deviceHint": "在刚打开的浏览器窗口输入此验证码",
8754
+ "cred.deviceWaiting": "等待授权中…",
8755
+ "cred.deviceExpired": "验证码已过期,请重新登录",
8756
+ "cred.deviceDenied": "授权已被拒绝",
8757
+ "cred.deviceFailed": "浏览器授权失败:{error}",
8758
+ "cred.deviceNetwork": "无法连接 {host},请检查网络或代理后重试",
8759
+ "cred.createToken": "创建",
8760
+ "panel.remotes": "管理远程",
8761
+ "remotes.title": "远程地址",
8762
+ "remotes.hint": "远程地址属于整个仓库,对该仓库的所有工作树生效。",
8763
+ "remotes.empty": "还没有配置远程地址",
8764
+ "remotes.add": "新增远程",
8765
+ "remotes.nameLabel": "名称",
8766
+ "remotes.namePlaceholder": "origin",
8767
+ "remotes.urlLabel": "地址",
8768
+ "remotes.urlPlaceholder": "https://github.com/user/repo.git",
8769
+ "remotes.fetchUrl": "提取",
8770
+ "remotes.pushUrl": "推送",
8771
+ "remotes.save": "保存",
8772
+ "remotes.saving": "保存中…",
8773
+ "remotes.edit": "修改地址",
8774
+ "remotes.remove": "移除",
8775
+ "remotes.confirmRemove": "确认移除",
8776
+ "remotes.cancel": "取消",
8777
+ "remotes.saved": "已更新远程 {name}",
8778
+ "remotes.removed": "已移除远程 {name}",
8779
+ "remotes.loadFailed": "读取远程地址失败",
8780
+ "remotes.nameEmpty": "请输入名称",
8781
+ "remotes.nameInvalid": "仅允许字母、数字、\".\"、\"_\"、\"-\"",
8782
+ "remotes.urlEmpty": "请输入地址",
8783
+ "remotes.urlInvalid": "请输入有效的远程地址"
7634
8784
  };
7635
8785
  //#endregion
7636
8786
  //#region src/client/index.ts