dsh-remote-vs 0.9.2 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -11,7 +11,7 @@ window.__ModuleLoader__.load({
11
11
  let react_jsx_runtime = require("react/jsx-runtime");
12
12
  let react_dom = require("react-dom");
13
13
  //#region \0dsh-css:E:\work_base\dsh_remote_vs\src\client\vsc\vsc.css.mjs
14
- const css$3 = "/**\n * VS Code-style design system for the remote explorer. Every surface below\n * is scoped under `.rvs-root` so the sheet can never leak into the host GUI,\n * and every host token carries a literal fallback: the plugin also renders in\n * the native right-Sidebar fallback and on hosts whose alias tokens differ.\n *\n * Palette and metrics are aligned with the reference implementation\n * (dsh-solution-explorer): 13px/20px panel type, 5px/10px tree rows with a\n * 10px radius, a 30px activity button with the badge tucked into the icon's\n * bottom-right corner, GitHub-Dark token colors, and the\n * `--e2b714 / #4ec9b0 / #f14c4c` modified/added/deleted triple.\n */\n\n.rvs-root {\n --rvs-fg: var(--dsw-alias-label-primary, #e6e6e6);\n --rvs-fg2: var(--dsw-alias-label-secondary, #b9b9b9);\n --rvs-fg3: var(--dsw-alias-label-tertiary, #8b8b8b);\n --rvs-hover: var(--dsw-alias-interactive-bg-hover, rgba(127, 127, 127, .14));\n --rvs-active: var(--dsw-alias-interactive-bg-active, rgba(77, 107, 254, .22));\n --rvs-border: var(--dsw-alias-border-l1, rgba(127, 127, 127, .25));\n --rvs-border2: var(--dsw-alias-border-l2, rgba(127, 127, 127, .38));\n --rvs-input: var(--dsw-alias-bg-input, rgba(127, 127, 127, .12));\n --rvs-overlay: var(--dsw-alias-bg-overlay, #24262e);\n --rvs-layer3: var(--dsw-alias-bg-layer-3, rgba(127, 127, 127, .16));\n --rvs-brand: var(--dsw-alias-brand-primary, #4d6bfe);\n --rvs-accent: var(--dsw-alias-button-info-fill, #4d6bfe);\n --rvs-danger: var(--dsw-alias-state-error-primary, #e05555);\n --rvs-git-m: #e2b714;\n --rvs-git-a: #4ec9b0;\n --rvs-git-d: #f14c4c;\n --rvs-git-r: #4ec9b0;\n --rvs-git-c: #58a6ff;\n --rvs-git-u: #f14c4c;\n --rvs-git-x: #6e6e6e;\n --rvs-add-bg: rgba(78, 201, 176, .15);\n --rvs-del-bg: rgba(241, 76, 76, .15);\n /* Opaque equivalents, for elements that must not show what scrolls under\n them (the pinned diff line numbers). */\n --rvs-add-solid: #e4f6f1;\n --rvs-del-solid: #fdeaea;\n --rvs-mono: 'Cascadia Code', 'Fira Code', 'JetBrains Mono', Consolas, 'Courier New', monospace;\n /* Opaque code surfaces (Light+ default; the dark block below flips them). */\n --rvs-code-bg: #ffffff;\n --rvs-code-fg: #1f1f1f;\n --rvs-code-gutter-fg: #237893;\n\n display: flex;\n flex-direction: row;\n height: 100%;\n min-height: 0;\n overflow: hidden;\n color: var(--rvs-fg);\n font-size: 13px;\n line-height: 20px;\n}\n\n/**\n * Code surfaces are OPAQUE, in the editor's own palette rather than the DSH\n * skin: syntax colors need a definite background to stay readable, and the\n * app shell marks its resolved scheme on <body> (same signal theme.ts reads).\n */\nbody[data-ds-dark-theme] .rvs-root {\n --rvs-code-bg: #1e1e1e;\n --rvs-code-fg: #d4d4d4;\n --rvs-code-gutter-fg: #858585;\n --rvs-add-solid: #16332e;\n --rvs-del-solid: #3b2325;\n}\n\n.rvs-edge {\n background: var(--rvs-code-bg);\n color: var(--rvs-code-fg);\n}\n\n.rvs-root *,\n.rvs-portal * {\n box-sizing: border-box;\n}\n\n/* ------------------------------------------------------------ activity rail */\n\n.rvs-rail {\n flex: none;\n width: 38px;\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 4px;\n padding: 8px 0;\n border-right: 1px solid var(--rvs-border);\n background: var(--dsw-specific-sidebar-fill, transparent);\n}\n\n.rvs-rail-btn {\n position: relative;\n flex: none;\n width: 30px;\n height: 30px;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 0;\n border: none;\n border-radius: 6px;\n background: transparent;\n color: var(--rvs-fg3);\n cursor: pointer;\n}\n\n.rvs-rail-btn:hover {\n color: var(--rvs-fg);\n background: var(--rvs-hover);\n}\n\n.rvs-rail-btn.active {\n color: var(--rvs-fg);\n background: var(--rvs-hover);\n box-shadow: inset 2px 0 0 var(--rvs-brand);\n}\n\n.rvs-rail-btn.danger:hover {\n color: var(--rvs-danger);\n}\n\n.rvs-rail-badge {\n position: absolute;\n bottom: 1px;\n right: 1px;\n min-width: 14px;\n height: 14px;\n padding: 0 3px;\n border-radius: 7px;\n background: var(--rvs-accent);\n color: #fff;\n font-size: 9px;\n font-weight: 700;\n line-height: 14px;\n text-align: center;\n pointer-events: none;\n}\n\n.rvs-rail-sep {\n flex: none;\n width: 20px;\n height: 1px;\n margin: 4px 0;\n background: var(--rvs-border);\n}\n\n/* -------------------------------------------------------- panel + editor */\n\n.rvs-column {\n flex: 1;\n min-width: 0;\n min-height: 0;\n display: flex;\n flex-direction: column;\n}\n\n.rvs-pane {\n display: flex;\n flex-direction: column;\n min-height: 0;\n min-width: 0;\n overflow: hidden;\n}\n\n/**\n * One lazily-mounted panel slot: a visited panel stays mounted and is merely\n * hidden, so returning to the explorer (or the terminal) neither refetches nor\n * flashes a reload.\n */\n.rvs-viewslot {\n flex: 1;\n min-height: 0;\n min-width: 0;\n flex-direction: column;\n height: 100%;\n overflow: hidden;\n}\n\n/**\n * Padding for the legacy views (workspaces / terminal) that draw no chrome.\n *\n * It MUST be a flex column: the workspaces view scroller is its child and gets\n * its bounded height only from a flex parent (`flex:1; min-height:0`). As a\n * plain block it grew with its content instead, so the panel clipped at the\n * viewslot's `overflow:hidden` and the binding list could not scroll at all —\n * a seventh workspace (and the wizard under it) was simply unreachable.\n */\n.rvs-viewpad {\n display: flex;\n flex-direction: column;\n min-height: 0;\n padding: 10px 12px 14px;\n box-sizing: border-box;\n}\n\n.rvs-pane.grow {\n flex: 1;\n}\n\n.rvs-header {\n flex: none;\n display: flex;\n align-items: center;\n gap: 6px;\n height: 34px;\n padding: 0 8px 0 10px;\n border-bottom: 1px solid var(--rvs-border);\n}\n\n.rvs-header .rvs-title {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n font-size: 11px;\n font-weight: 700;\n letter-spacing: .04em;\n text-transform: uppercase;\n color: var(--rvs-fg3);\n}\n\n.rvs-hbtn {\n flex: none;\n width: 22px;\n height: 22px;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 0;\n border: none;\n border-radius: 5px;\n background: transparent;\n color: var(--rvs-fg3);\n cursor: pointer;\n}\n\n.rvs-hbtn:hover {\n color: var(--rvs-fg);\n background: var(--rvs-hover);\n}\n\n.rvs-hbtn.danger:hover {\n color: #fff;\n background: var(--rvs-danger);\n}\n\n.rvs-hbtn:disabled {\n opacity: .4;\n cursor: default;\n}\n\n.rvs-scroll {\n flex: 1;\n min-height: 0;\n overflow: auto;\n scrollbar-width: thin;\n}\n\n.rvs-globalbar {\n flex: none;\n display: flex;\n align-items: center;\n gap: 6px;\n padding: 4px 8px;\n border-bottom: 1px solid var(--rvs-border);\n font-size: 11px;\n}\n\n.rvs-scroll::-webkit-scrollbar {\n width: 8px;\n height: 8px;\n}\n\n.rvs-scroll::-webkit-scrollbar-thumb {\n background: var(--dsw-alias-scrollbar-bg-l2, rgba(127, 127, 127, .4));\n border-radius: 4px;\n}\n\n.rvs-scroll::-webkit-scrollbar-thumb:hover {\n background: var(--dsw-alias-scrollbar-hover-l2, rgba(127, 127, 127, .6));\n}\n\n.rvs-scroll::-webkit-scrollbar-track {\n background: transparent;\n}\n\n.rvs-vsplit-handle {\n flex: none;\n position: relative;\n height: 7px;\n cursor: row-resize;\n touch-action: none;\n}\n\n.rvs-vsplit-handle::after {\n content: '';\n position: absolute;\n left: 0;\n right: 0;\n top: 3px;\n height: 1px;\n background: var(--rvs-border);\n}\n\n.rvs-vsplit-handle:hover::after,\n.rvs-vsplit-handle[data-dragging='true']::after {\n top: 2px;\n height: 3px;\n border-radius: 2px;\n background: var(--rvs-brand);\n}\n\n/* ------------------------------------------------------------------ tree */\n\n.rvs-tree {\n padding: 4px 6px 12px;\n min-width: 0;\n}\n\n.rvs-node {\n display: flex;\n align-items: center;\n gap: 6px;\n min-width: 0;\n width: 100%;\n padding: 4px 8px;\n border: none;\n background: transparent;\n border-radius: 8px;\n color: var(--rvs-fg);\n font: inherit;\n font-size: 13px;\n line-height: 18px;\n text-align: left;\n cursor: pointer;\n}\n\n.rvs-node:hover {\n background: var(--rvs-hover);\n}\n\n.rvs-node.selected {\n background: var(--rvs-active);\n}\n\n.rvs-node.cut {\n opacity: .5;\n}\n\n.rvs-node.drop-target {\n background: var(--rvs-active);\n outline: 1px solid var(--rvs-brand);\n outline-offset: -1px;\n}\n\n/* Where a file drag will land: a slip must not silently drop a tree into the\n wrong directory. */\n.rvs-drop-hint {\n margin: 2px 8px 4px;\n padding: 3px 8px;\n border: 1px dashed var(--rvs-brand);\n border-radius: 3px;\n font-size: 11px;\n color: var(--rvs-brand);\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n direction: rtl;\n unicode-bidi: plaintext;\n}\n/* Upload progress: one line under the header. A multi-minute transfer with no\n feedback is indistinguishable from a hung explorer, so the row names the\n count and the file on the wire. */\n.rvs-upload-bar {\n flex: none;\n display: flex;\n align-items: center;\n gap: 6px;\n padding: 3px 10px 4px;\n font-size: 11px;\n color: var(--dsw-alias-text-secondary, rgba(127, 127, 127, .95));\n}\n\n.rvs-upload-text {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n direction: rtl;\n unicode-bidi: plaintext;\n}\n\n.rvs-node-icon {\n flex: none;\n width: 16px;\n height: 16px;\n display: flex;\n align-items: center;\n justify-content: center;\n color: var(--rvs-fg3);\n}\n\n.rvs-node-name {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.rvs-node-actions {\n flex: none;\n display: none;\n align-items: center;\n gap: 2px;\n}\n\n.rvs-node:hover .rvs-node-actions {\n display: flex;\n}\n\n.rvs-git-letter {\n flex: none;\n width: 14px;\n margin-left: 2px;\n font-size: 11px;\n font-weight: 700;\n text-align: center;\n}\n\n.rvs-git-M, .rvs-letter-M { color: var(--rvs-git-m); }\n.rvs-git-A, .rvs-letter-A { color: var(--rvs-git-a); }\n.rvs-git-D, .rvs-letter-D { color: var(--rvs-git-d); }\n.rvs-git-R, .rvs-letter-R { color: var(--rvs-git-r); }\n.rvs-git-C, .rvs-letter-C { color: var(--rvs-git-c); }\n.rvs-git-U, .rvs-letter-U { color: var(--rvs-git-u); }\n.rvs-git-q, .rvs-letter-q, .rvs-git-\\\\? { color: var(--rvs-git-a); }\n.rvs-git-x, .rvs-letter-x { color: var(--rvs-git-x); }\n.rvs-git-T, .rvs-letter-T { color: var(--rvs-git-m); }\n\n.rvs-rename-input {\n flex: 1;\n min-width: 0;\n padding: 0 4px;\n border: 1px solid var(--rvs-brand);\n border-radius: 4px;\n background: var(--rvs-input);\n color: var(--rvs-fg);\n font: inherit;\n font-size: 13px;\n line-height: 18px;\n outline: none;\n}\n\n.rvs-empty {\n padding: 18px 14px;\n color: var(--rvs-fg3);\n font-size: 12px;\n text-align: center;\n line-height: 1.7;\n}\n\n.rvs-banner {\n margin: 6px 8px;\n padding: 6px 9px;\n border-radius: 6px;\n font-size: 12px;\n line-height: 1.5;\n word-break: break-word;\n white-space: pre-wrap;\n}\n\n.rvs-banner.info {\n background: var(--rvs-hover);\n color: var(--rvs-fg2);\n}\n\n.rvs-banner.error {\n background: rgba(200, 60, 60, .16);\n color: var(--rvs-danger);\n}\n\n.rvs-banner.warn {\n background: rgba(226, 183, 20, .14);\n color: var(--rvs-git-m);\n}\n\n/* --------------------------------------------------------- source control */\n\n.rvs-scm-section {\n border-bottom: 1px solid var(--rvs-border);\n}\n\n.rvs-scm-head {\n display: flex;\n align-items: center;\n gap: 4px;\n height: 28px;\n padding: 0 6px 0 4px;\n cursor: pointer;\n user-select: none;\n font-size: 11px;\n font-weight: 700;\n letter-spacing: .04em;\n text-transform: uppercase;\n color: var(--rvs-fg2);\n}\n\n.rvs-scm-head:hover {\n background: var(--rvs-hover);\n}\n\n.rvs-scm-head .rvs-count {\n margin-left: 6px;\n font-size: 11px;\n font-weight: 600;\n letter-spacing: 0;\n color: var(--rvs-fg3);\n}\n\n.rvs-scm-head-actions {\n margin-left: auto;\n display: flex;\n align-items: center;\n gap: 2px;\n}\n\n.rvs-scm-section.collapsed > *:not(.rvs-scm-head) {\n display: none !important;\n}\n\n.rvs-chevron {\n flex: none;\n transition: transform .12s ease;\n}\n\n.rvs-chevron.open {\n transform: rotate(90deg);\n}\n\n.rvs-scm-item {\n display: flex;\n align-items: center;\n gap: 6px;\n width: 100%;\n padding: 3px 8px 3px 22px;\n border: none;\n background: transparent;\n color: var(--rvs-fg);\n font: inherit;\n font-size: 12.5px;\n line-height: 19px;\n text-align: left;\n cursor: pointer;\n}\n\n.rvs-scm-item:hover {\n background: var(--rvs-hover);\n}\n\n.rvs-scm-item.selected {\n background: var(--rvs-active);\n}\n\n.rvs-scm-path {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.rvs-scm-dir {\n flex: none;\n color: var(--rvs-fg3);\n}\n\n.rvs-scm-actions {\n flex: none;\n display: none;\n align-items: center;\n gap: 1px;\n}\n\n.rvs-scm-item:hover .rvs-scm-actions {\n display: flex;\n}\n\n.rvs-commit-box {\n flex: none;\n padding: 8px 10px;\n border-bottom: 1px solid var(--rvs-border);\n}\n\n.rvs-commit-input {\n width: 100%;\n min-height: 58px;\n max-height: 180px;\n padding: 6px 8px;\n border: 1px solid var(--rvs-border2);\n border-radius: 8px;\n background: var(--rvs-input);\n color: var(--rvs-fg);\n font: inherit;\n font-size: 12.5px;\n line-height: 18px;\n resize: vertical;\n outline: none;\n}\n\n.rvs-commit-input:focus {\n border-color: var(--rvs-brand);\n}\n\n.rvs-commit-row {\n display: flex;\n gap: 6px;\n margin-top: 6px;\n}\n\n.rvs-commit-btn {\n flex: 1;\n padding: 5px 8px;\n border: none;\n border-radius: 6px;\n background: var(--rvs-accent);\n color: #fff;\n font: inherit;\n font-size: 12.5px;\n font-weight: 600;\n cursor: pointer;\n}\n\n.rvs-commit-btn:disabled {\n opacity: .45;\n cursor: default;\n}\n\n.rvs-repo-item {\n display: flex;\n align-items: center;\n gap: 6px;\n padding: 5px 10px 5px 12px;\n cursor: pointer;\n font-size: 12.5px;\n}\n\n.rvs-repo-item:hover {\n background: var(--rvs-hover);\n}\n\n.rvs-repo-item.active {\n background: var(--rvs-active);\n}\n\n.rvs-repo-name {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.rvs-repo-branch {\n flex: none;\n font-size: 11px;\n color: var(--rvs-fg3);\n}\n\n.rvs-pill {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n padding: 1px 8px;\n border-radius: 999px;\n background: var(--rvs-hover);\n font-size: 11px;\n font-weight: 600;\n color: var(--rvs-fg);\n}\n\n.rvs-subpanel {\n margin: 6px 8px;\n padding: 8px;\n border: 1px solid var(--rvs-border2);\n border-radius: 8px;\n font-size: 12px;\n}\n\n.rvs-subpanel-head {\n display: flex;\n align-items: center;\n gap: 6px;\n margin-bottom: 6px;\n color: var(--rvs-fg2);\n font-weight: 600;\n}\n\n.rvs-chip {\n flex: none;\n padding: 2px 8px;\n border: 1px solid var(--rvs-border2);\n border-radius: 6px;\n background: transparent;\n color: var(--rvs-fg);\n font: inherit;\n font-size: 11.5px;\n cursor: pointer;\n}\n\n.rvs-chip:hover {\n background: var(--rvs-hover);\n}\n\n.rvs-chip.primary {\n background: var(--rvs-accent);\n border-color: transparent;\n color: #fff;\n}\n\n.rvs-chip.danger:hover {\n background: var(--rvs-danger);\n border-color: transparent;\n color: #fff;\n}\n\n.rvs-input {\n flex: 1;\n min-width: 0;\n padding: 4px 8px;\n border: 1px solid var(--rvs-border2);\n border-radius: 6px;\n background: var(--rvs-input);\n color: var(--rvs-fg);\n font: inherit;\n font-size: 12px;\n outline: none;\n}\n\n.rvs-input:focus {\n border-color: var(--rvs-brand);\n}\n\n.rvs-select {\n flex: 1;\n min-width: 0;\n padding: 3px 6px;\n border: 1px solid var(--rvs-border2);\n border-radius: 6px;\n background: var(--rvs-input);\n color: var(--rvs-fg);\n font: inherit;\n font-size: 12px;\n outline: none;\n color-scheme: light dark;\n}\n\n.rvs-select option {\n background: Canvas;\n color: CanvasText;\n}\n\n/* ------------------------------------------------- conversation-view mount */\n\n/**\n * Bound the center surface to the height the host gave the slot.\n *\n * `flex: 1 1 0` is the load-bearing declaration, and BOTH halves of it matter.\n * The host's own slot rules are:\n *\n * .wSkVaW_viewArea { flex: 1; min-height: 0; overflow: hidden }\n * [data-phase=active] .viewArea { flex: 1 0 auto; min-height: auto }\n * .wSkVaW_root { height: 100%; position: relative }\n *\n * While a conversation is active the slot's flex-basis is `auto`, so the slot's\n * height is driven by its child's INTRINSIC size. A full-height editor then\n * pushed it to 5510px for a 300-line file, the outer scroller took over, and\n * the tab strip and file info bar scrolled off the top with the code.\n *\n * `flex-basis: 0` breaks that cycle: the item stops contributing its content\n * height, so the slot stays the size the host intended and `.cm-scroller`\n * becomes the only scroller. Measured in the fixture (.spike/layout-fixture.html,\n * which mirrors the slot rules above) — only the bottom row fits:\n *\n * flex: 1 1 0 + min-height: 0 -> 736px OK\n * flex: 1 1 auto (i.e. `flex: 1`) -> 5510px broken\n * height: 0 + min-height: 100% -> 5510px broken\n * max-height: 100% -> 5510px broken\n * height: 100vh -> 5510px broken\n *\n * `height: 100%` is deliberately NOT set here any more. It resolves to `auto`\n * against a slot whose height is not definite — it reads like the fix but\n * measures as a no-op, which is how this regressed once already.\n *\n * NOT `position: absolute` either — that was another failed attempt: `.viewArea`\n * is not itself positioned, so `inset: 0` escaped to the host's `.wSkVaW_root`\n * (which is `position: relative`) and the plugin's global bar floated on top of\n * the host's 对话 / 轨迹 / 远程编辑器 tab row.\n *\n * The sidebar's `.rvs-viewslot` is deliberately not covered: it has a bounding\n * parent and this bug is specific to the conversation slot's `min-height: auto`\n * phase.\n */\n.rvs-center {\n flex: 1 1 0;\n display: flex;\n flex-direction: column;\n min-height: 0;\n min-width: 0;\n overflow: hidden;\n}\n\n/* ---------------------------------------------------------------- editor */\n\n.rvs-etabs {\n flex: none;\n display: flex;\n align-items: stretch;\n gap: 2px;\n height: 32px;\n padding: 0 4px;\n overflow-x: auto;\n overflow-y: hidden;\n border-bottom: 1px solid var(--rvs-border);\n scrollbar-width: none;\n}\n\n.rvs-etabs::-webkit-scrollbar {\n height: 0;\n}\n\n.rvs-etab {\n flex: none;\n display: flex;\n align-items: center;\n gap: 5px;\n max-width: 190px;\n margin: 4px 0;\n padding: 0 4px 0 8px;\n border: none;\n border-radius: 6px;\n background: transparent;\n color: var(--rvs-fg3);\n font: inherit;\n font-size: 12px;\n cursor: pointer;\n}\n\n.rvs-etab:hover {\n background: var(--rvs-hover);\n color: var(--rvs-fg);\n}\n\n.rvs-etab.active {\n background: var(--rvs-layer3);\n color: var(--rvs-fg);\n}\n\n.rvs-etab-name {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.rvs-etab-dot {\n flex: none;\n font-size: 9px;\n color: var(--rvs-git-m);\n}\n\n.rvs-etab-close {\n flex: none;\n width: 16px;\n height: 16px;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 0;\n border: none;\n border-radius: 4px;\n background: transparent;\n color: inherit;\n cursor: pointer;\n opacity: .65;\n}\n\n.rvs-etab-close:hover {\n background: var(--rvs-hover);\n opacity: 1;\n}\n\n.rvs-einfo {\n flex: none;\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 2px 8px 2px 10px;\n border-bottom: 1px solid var(--rvs-border);\n font-size: 11px;\n line-height: 18px;\n color: var(--rvs-fg3);\n}\n\n.rvs-einfo-path {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n /* Truncate from the LEFT (keep the tail) without reordering the text:\n `plaintext` gives the content its own LTR base direction, which is what\n stops a leading \"/\" from rendering as a trailing one. */\n direction: rtl;\n unicode-bidi: plaintext;\n text-align: left;\n}\n\n.rvs-einfo-spacer {\n margin-left: auto;\n}\n\n.rvs-status.dirty { color: var(--rvs-git-m); }\n.rvs-status.saved { color: var(--rvs-git-a); }\n.rvs-status.saving { color: var(--rvs-fg2); }\n\n.rvs-body {\n flex: 1;\n min-height: 0;\n min-width: 0;\n display: flex;\n flex-direction: column;\n overflow: hidden;\n /* Anchors portalled overlays (the find widget) outside every scroller. */\n position: relative;\n background: var(--rvs-code-bg);\n color: var(--rvs-code-fg);\n}\n\n.rvs-editor-host {\n flex: 1;\n min-height: 0;\n overflow: hidden;\n background: var(--rvs-code-bg);\n}\n\n/* Keep a panel mounted but out of layout (and out of the a11y tree). Used by\n the editor host in preview mode: unmounting it destroyed the buffer and the\n undo stack, hiding it preserves both. */\n.rvs-hidden {\n display: none !important;\n}\n\n/* Find-match marks. Painted by our own StateField (`findMatchHighlight`), and\n deliberately the SAME colours as the `.cm-searchMatch` rules below so the\n two paths cannot drift apart. */\n.rvs-find-match {\n background-color: rgba(226, 183, 20, .30);\n outline: 1px solid rgba(226, 183, 20, .55);\n border-radius: 2px;\n}\n\n.rvs-find-match.current {\n background-color: rgba(226, 183, 20, .48);\n}\n\n/* ------------------------------------------------------- preview surfaces */\n\n.rvs-preview-img {\n flex: 1;\n min-height: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n overflow: auto;\n background-color: var(--rvs-code-bg);\n background-image:\n linear-gradient(45deg, rgba(127, 127, 127, .08) 25%, transparent 25%),\n linear-gradient(-45deg, rgba(127, 127, 127, .08) 25%, transparent 25%),\n linear-gradient(45deg, transparent 75%, rgba(127, 127, 127, .08) 75%),\n linear-gradient(-45deg, transparent 75%, rgba(127, 127, 127, .08) 75%);\n background-size: 20px 20px, 20px 20px, 20px 20px, 20px 20px;\n background-position: 0 0, 0 10px, 10px -10px, -10px 0;\n}\n\n.rvs-preview-img img {\n max-width: 100%;\n max-height: 100%;\n object-fit: contain;\n image-rendering: auto;\n}\n\n.rvs-markdown {\n flex: 1;\n min-height: 0;\n overflow: auto;\n padding: 12px 14px 24px;\n font-size: 13px;\n line-height: 1.7;\n color: var(--rvs-code-fg);\n background: var(--rvs-code-bg);\n user-select: text;\n}\n\n.rvs-markdown h1, .rvs-markdown h2, .rvs-markdown h3, .rvs-markdown h4 {\n margin: 14px 0 6px;\n line-height: 1.35;\n}\n\n.rvs-markdown h1 { font-size: 19px; border-bottom: 1px solid var(--rvs-border); padding-bottom: 5px; }\n.rvs-markdown h2 { font-size: 16px; border-bottom: 1px solid var(--rvs-border); padding-bottom: 4px; }\n.rvs-markdown h3 { font-size: 14px; }\n.rvs-markdown p { margin: 6px 0; }\n.rvs-markdown ul, .rvs-markdown ol { margin: 6px 0; padding-left: 22px; }\n.rvs-markdown li { margin: 2px 0; }\n.rvs-markdown code {\n padding: 1px 5px;\n border-radius: 4px;\n background: var(--rvs-hover);\n font-family: var(--rvs-mono);\n font-size: 12px;\n}\n.rvs-markdown pre {\n margin: 8px 0;\n padding: 9px 11px;\n border-radius: 8px;\n background: var(--rvs-input);\n overflow-x: auto;\n}\n.rvs-markdown pre code { padding: 0; background: transparent; }\n.rvs-markdown blockquote {\n margin: 8px 0;\n padding: 2px 12px;\n border-left: 3px solid var(--rvs-border2);\n color: var(--rvs-fg2);\n}\n.rvs-markdown a { color: var(--dsw-alias-label-link, #58a6ff); text-decoration: none; }\n.rvs-markdown a:hover { text-decoration: underline; }\n.rvs-markdown table { border-collapse: collapse; margin: 8px 0; font-size: 12px; }\n.rvs-markdown th, .rvs-markdown td { border: 1px solid var(--rvs-border2); padding: 4px 8px; }\n.rvs-markdown hr { border: none; border-top: 1px solid var(--rvs-border); margin: 12px 0; }\n\n.rvs-html-frame {\n flex: 1;\n min-height: 0;\n width: 100%;\n border: none;\n background: #fff;\n}\n\n/* ------------------------------------------------------------------ diff */\n\n.rvs-diff {\n flex: 1;\n min-height: 0;\n display: flex;\n flex-direction: column;\n overflow: hidden;\n}\n\n.rvs-diff-toolbar {\n flex: none;\n display: flex;\n align-items: center;\n gap: 6px;\n padding: 3px 8px;\n border-bottom: 1px solid var(--rvs-border);\n font-size: 11px;\n color: var(--rvs-fg3);\n}\n\n/**\n * Prev/next change, in the toolbar's free space.\n *\n * Two chevrons around a tabular counter: the pair then reads as \"jump to change\n * n of N\" instead of two anonymous arrows, and the counter doubles as the \"where\n * am I\" cue while scrolling by hand.\n */\n.rvs-hunknav {\n display: inline-flex;\n align-items: center;\n gap: 1px;\n margin-right: 2px;\n}\n\n.rvs-hunknav-pos {\n min-width: 30px;\n text-align: center;\n font-size: 11px;\n font-variant-numeric: tabular-nums;\n color: var(--rvs-fg3);\n user-select: none;\n}\n\n.rvs-seg {\n display: inline-flex;\n border: 1px solid var(--rvs-border2);\n border-radius: 6px;\n overflow: hidden;\n}\n\n.rvs-seg button {\n padding: 2px 9px;\n border: none;\n background: transparent;\n color: var(--rvs-fg2);\n font: inherit;\n font-size: 11px;\n cursor: pointer;\n}\n\n.rvs-seg button.active {\n background: var(--rvs-active);\n color: var(--rvs-fg);\n}\n\n.rvs-seg button:hover:not(.active) {\n background: var(--rvs-hover);\n}\n\n.rvs-diff-grid {\n flex: 1;\n min-height: 0;\n overflow-y: auto;\n overflow-x: hidden;\n font-family: var(--rvs-mono);\n font-size: 12px;\n line-height: 18px;\n scrollbar-width: thin;\n background: var(--rvs-code-bg);\n color: var(--rvs-code-fg);\n}\n\n/**\n * Top horizontal scrollbar.\n *\n * The two panes scroll horizontally on their own — so a long line can never\n * widen the layout — and this proxy bar drives both of them from the TOP of\n * the view, instead of forcing a scroll to the bottom of the file to reach a\n * horizontal bar. Its inner spacer is sized to the widest line, and the\n * component only renders it when a line really is wider than the grid.\n *\n * Sized and coloured to be SEEN: as a 13 px band with `scrollbar-width: thin`\n * and a faint thumb, it read as \"there is no horizontal scrollbar at all\".\n */\n.rvs-hbar {\n flex: none;\n height: 15px;\n overflow-x: auto;\n overflow-y: hidden;\n border-bottom: 1px solid var(--rvs-border);\n background: var(--rvs-hover);\n}\n\n.rvs-hbar-inner {\n height: 1px;\n}\n\n.rvs-hbar::-webkit-scrollbar {\n height: 11px;\n}\n\n.rvs-hbar::-webkit-scrollbar-thumb {\n background: var(--dsw-alias-scrollbar-bg-l2, rgba(150, 160, 180, .75));\n border-radius: 6px;\n border: 2px solid transparent;\n background-clip: padding-box;\n}\n\n.rvs-hbar::-webkit-scrollbar-thumb:hover {\n background: var(--dsw-alias-scrollbar-hover-l2, rgba(170, 180, 200, .95));\n background-clip: padding-box;\n}\n\n.rvs-hbar::-webkit-scrollbar-track {\n background: transparent;\n}\n\n.rvs-diff-cols {\n display: flex;\n align-items: stretch;\n min-width: 0;\n}\n\n/**\n * One diff pane. `flex: 1 1 0` keeps the panes EXACTLY equal however long a\n * line is (the previous row markup let a long line widen its own pane, which\n * is what broke the symmetry). Each pane keeps its own horizontal scroll, and\n * its scrollbar is now VISIBLE: the top proxy remains the convenient control,\n * but the pane's own bar is what a user reaches for first, and it sits right\n * under the content when the file is shorter than the view.\n */\n.rvs-diff-col {\n flex: 1 1 0;\n min-width: 0;\n overflow-x: auto;\n overflow-y: hidden;\n}\n\n.rvs-diff-col::-webkit-scrollbar {\n height: 9px;\n width: 0;\n}\n\n.rvs-diff-col::-webkit-scrollbar-thumb {\n background: var(--dsw-alias-scrollbar-bg-l2, rgba(150, 160, 180, .6));\n border-radius: 5px;\n}\n\n.rvs-diff-col::-webkit-scrollbar-thumb:hover {\n background: var(--dsw-alias-scrollbar-hover-l2, rgba(170, 180, 200, .85));\n}\n\n.rvs-diff-col::-webkit-scrollbar-track {\n background: transparent;\n}\n\n.rvs-dline {\n display: flex;\n align-items: flex-start;\n height: 18px;\n line-height: 18px;\n white-space: pre;\n}\n\n.rvs-dline-num {\n /* Pinned while the line scrolls sideways, so a long line never takes the\n line numbers away with it. */\n position: sticky;\n left: 0;\n z-index: 1;\n flex: none;\n width: 3.4em;\n padding-right: 6px;\n text-align: right;\n color: var(--rvs-fg3);\n opacity: .85;\n user-select: none;\n background: var(--rvs-code-bg);\n}\n\n.rvs-row-del .rvs-dline-num {\n background: var(--rvs-del-solid);\n}\n\n.rvs-row-add .rvs-dline-num {\n background: var(--rvs-add-solid);\n}\n\n.rvs-dline-text {\n flex: 1;\n padding-right: 10px;\n user-select: text;\n}\n\n.rvs-gutter-col {\n flex: none;\n width: 30px;\n border-left: 1px solid var(--rvs-border);\n border-right: 1px solid var(--rvs-border);\n user-select: none;\n}\n\n.rvs-gutter-line {\n height: 18px;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n.rvs-diff-grid::-webkit-scrollbar {\n width: 9px;\n height: 0;\n}\n\n.rvs-diff-grid::-webkit-scrollbar-thumb {\n background: var(--dsw-alias-scrollbar-bg-l2, rgba(127, 127, 127, .4));\n border-radius: 4px;\n}\n\n.rvs-num {\n flex: none;\n display: inline-block;\n width: 3.2em;\n padding-right: 6px;\n text-align: right;\n color: var(--rvs-fg3);\n opacity: .6;\n user-select: none;\n}\n\n.rvs-row-del {\n background: var(--rvs-del-bg);\n}\n\n.rvs-row-del .rvs-dline-text,\n.rvs-row-del .rvs-dline-num,\n.rvs-row-del .rvs-num {\n color: #f14c4c;\n}\n\n.rvs-row-add {\n background: var(--rvs-add-bg);\n}\n\n.rvs-row-add .rvs-dline-text,\n.rvs-row-add .rvs-dline-num,\n.rvs-row-add .rvs-num {\n color: #4ec9b0;\n}\n\n.rvs-row-empty {\n color: var(--rvs-fg3);\n opacity: .35;\n}\n\n.rvs-gutter {\n flex: none;\n width: 30px;\n display: flex;\n align-items: center;\n justify-content: center;\n border-left: 1px solid var(--rvs-border);\n border-right: 1px solid var(--rvs-border);\n user-select: none;\n}\n\n.rvs-gutter-dot {\n width: 6px;\n height: 6px;\n border-radius: 50%;\n opacity: .7;\n}\n\n.rvs-hunk-btns {\n display: flex;\n gap: 2px;\n}\n\n.rvs-hunk-btn {\n width: 13px;\n height: 15px;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 0;\n border: none;\n border-radius: 3px;\n background: transparent;\n color: var(--rvs-fg3);\n font-size: 11px;\n line-height: 1;\n cursor: pointer;\n}\n\n.rvs-hunk-btn:hover {\n background: var(--rvs-hover);\n color: var(--rvs-fg);\n}\n\n.rvs-hunk-btn.stage:hover {\n color: var(--rvs-git-a);\n}\n\n.rvs-hunk-btn.revert:hover {\n color: var(--rvs-git-d);\n}\n\n/**\n * Token colors: VS Code Dark+ / Light+, matching the CodeMirror theme in\n * `vsc/editor-theme.ts`. Keeping ONE palette for both surfaces means a file\n * does not change colors when the diff is switched to its editable side.\n */\n.rvs-t-comment { color: #6a9955; }\n.rvs-t-string { color: #ce9178; }\n.rvs-t-keyword { color: #569cd6; }\n.rvs-t-number { color: #b5cea8; }\n.rvs-t-func { color: #dcdcaa; }\n.rvs-t-type { color: #4ec9b0; }\n.rvs-t-attr { color: #9cdcfe; }\n.rvs-t-variable { color: #9cdcfe; }\n.rvs-t-operator { color: #d4d4d4; }\n.rvs-t-punct { color: #d4d4d4; }\n.rvs-t-tag { color: #569cd6; }\n.rvs-t-meta { color: #c586c0; }\n.rvs-t-insert { color: #4ec9b0; }\n.rvs-t-delete { color: #f14c4c; }\n\nbody:not([data-ds-dark-theme]) .rvs-t-comment { color: #008000; }\nbody:not([data-ds-dark-theme]) .rvs-t-string { color: #a31515; }\nbody:not([data-ds-dark-theme]) .rvs-t-keyword { color: #0000ff; }\nbody:not([data-ds-dark-theme]) .rvs-t-number { color: #098658; }\nbody:not([data-ds-dark-theme]) .rvs-t-func { color: #795e26; }\nbody:not([data-ds-dark-theme]) .rvs-t-type { color: #267f99; }\nbody:not([data-ds-dark-theme]) .rvs-t-attr,\nbody:not([data-ds-dark-theme]) .rvs-t-variable { color: #001080; }\nbody:not([data-ds-dark-theme]) .rvs-t-operator,\nbody:not([data-ds-dark-theme]) .rvs-t-punct { color: #1f1f1f; }\nbody:not([data-ds-dark-theme]) .rvs-t-meta { color: #af00db; }\n\n/**\n * CodeMirror token colors, applied through FIXED class names emitted by the\n * highlight style (`vsc/editor-palette.ts`). Fixed names mean the colors come\n * from this stylesheet — the same one that paints the rest of the UI — so a\n * code file can never go monochrome because a generated class name changed or\n * an injected style failed. Same Dark+ / Light+ palette as the diff panes\n * above, so a file keeps its colors when switching between the two views.\n */\n.rvs-ck-comment { color: #6a9955; }\n.rvs-ck-keyword { color: #569cd6; }\n.rvs-ck-control { color: #c586c0; }\n.rvs-ck-string { color: #ce9178; }\n.rvs-ck-regexp { color: #d16969; }\n.rvs-ck-number { color: #b5cea8; }\n.rvs-ck-constant { color: #569cd6; }\n.rvs-ck-func { color: #dcdcaa; }\n.rvs-ck-type { color: #4ec9b0; }\n.rvs-ck-variable { color: #9cdcfe; }\n.rvs-ck-attr { color: #9cdcfe; }\n.rvs-ck-meta { color: #c586c0; }\n.rvs-ck-punct { color: #d4d4d4; }\n.rvs-ck-op { color: #d4d4d4; }\n.rvs-ck-heading { color: #569cd6; font-weight: 700; }\n.rvs-ck-em { font-style: italic; }\n.rvs-ck-link { color: #9cdcfe; text-decoration: underline; }\n.rvs-ck-strike { text-decoration: line-through; }\n.rvs-ck-invalid { color: #f44747; }\n\nbody:not([data-ds-dark-theme]) .rvs-ck-comment { color: #008000; }\nbody:not([data-ds-dark-theme]) .rvs-ck-keyword { color: #0000ff; }\nbody:not([data-ds-dark-theme]) .rvs-ck-control { color: #af00db; }\nbody:not([data-ds-dark-theme]) .rvs-ck-string { color: #a31515; }\nbody:not([data-ds-dark-theme]) .rvs-ck-regexp { color: #811f3f; }\nbody:not([data-ds-dark-theme]) .rvs-ck-number { color: #098658; }\nbody:not([data-ds-dark-theme]) .rvs-ck-constant { color: #0070c1; }\nbody:not([data-ds-dark-theme]) .rvs-ck-func { color: #795e26; }\nbody:not([data-ds-dark-theme]) .rvs-ck-type { color: #267f99; }\nbody:not([data-ds-dark-theme]) .rvs-ck-variable { color: #001080; }\nbody:not([data-ds-dark-theme]) .rvs-ck-attr { color: #e50000; }\nbody:not([data-ds-dark-theme]) .rvs-ck-meta { color: #0000ff; }\nbody:not([data-ds-dark-theme]) .rvs-ck-punct,\nbody:not([data-ds-dark-theme]) .rvs-ck-op { color: #1f1f1f; }\nbody:not([data-ds-dark-theme]) .rvs-ck-heading { color: #0000ff; }\nbody:not([data-ds-dark-theme]) .rvs-ck-link { color: #001080; }\nbody:not([data-ds-dark-theme]) .rvs-ck-invalid { color: #cd3131; }\n\n/**\n * Opaque editor surface, also as plain CSS: the CodeMirror theme paints it too\n * (following the scheme through a Compartment), but a transparent editor is\n * the one failure a user notices immediately, so the container states it as\n * well.\n */\n.rvs-edge .cm-editor,\n.rvs-edge .cm-scroller,\n.rvs-edge .cm-gutters {\n background-color: var(--rvs-code-bg);\n}\n\n.rvs-edge .cm-editor {\n color: var(--rvs-code-fg);\n}\n\n.rvs-edge .cm-gutters {\n color: var(--rvs-code-gutter-fg, var(--rvs-fg3));\n border-right: 1px solid var(--rvs-border);\n}\n\n.rvs-edge .cm-activeLine {\n background-color: rgba(127, 127, 127, .08);\n}\n\n.rvs-edge .cm-activeLineGutter {\n background-color: rgba(127, 127, 127, .12);\n}\n\n/**\n * The code editor's own scrollbars.\n *\n * These had NO rule at all: CodeMirror's scroller is `.cm-scroller`, which is\n * none of `.rvs-scroll` / `.rvs-diff-grid` / `.rvs-hbar`, so the editor fell\n * back to the browser default — too thin to hit with a mouse.\n *\n * Both notations are needed and must stay in step: Chromium honours\n * `scrollbar-width` over `::-webkit-scrollbar` when both apply, so setting\n * only the webkit half would silently have no effect. Keep the numbers equal.\n */\n.rvs-edge .cm-scroller {\n scrollbar-width: auto;\n}\n\n.rvs-edge .cm-scroller::-webkit-scrollbar {\n width: 14px;\n height: 14px;\n}\n\n.rvs-edge .cm-scroller::-webkit-scrollbar-track {\n background: transparent;\n}\n\n.rvs-edge .cm-scroller::-webkit-scrollbar-thumb {\n /* Opaque, not a translucent token: a thin wash over code is exactly the\n low-contrast target that is hard to grab. */\n background: rgba(121, 121, 121, .8);\n border: 3px solid transparent;\n border-radius: 7px;\n background-clip: padding-box;\n min-height: 36px;\n}\n\n.rvs-edge .cm-scroller::-webkit-scrollbar-thumb:hover {\n background: rgba(150, 150, 150, .95);\n border: 3px solid transparent;\n background-clip: padding-box;\n}\n\n.rvs-edge .cm-scroller::-webkit-scrollbar-corner {\n background: transparent;\n}\n\n/* ------------------------------------------------------------ find widget */\n\n/**\n * \"You landed here\": the transient highlight on the line a jump landed on\n * (search hit, @-reference).\n *\n * Deliberately NOT a warm/amber wash: over the dark editor that muddies into a\n * brown-black band that reads as a shadow and swallows annotation colors. This\n * is the accent blue instead — a tinted band with a bright edge on the left and\n * hairlines top/bottom, so it reads as a marked region while every token keeps\n * its own colour and contrast.\n */\n.rvs-reveal-line {\n background-color: rgba(88, 166, 255, .22);\n box-shadow:\n inset 3px 0 0 0 #58a6ff,\n inset 0 1px 0 0 rgba(88, 166, 255, .38),\n inset 0 -1px 0 0 rgba(88, 166, 255, .38);\n}\n\nbody:not([data-ds-dark-theme]) .rvs-reveal-line {\n background-color: rgba(77, 107, 254, .14);\n box-shadow:\n inset 3px 0 0 0 #4d6bfe,\n inset 0 1px 0 0 rgba(77, 107, 254, .35),\n inset 0 -1px 0 0 rgba(77, 107, 254, .35);\n}\n\n/* --------------------------------------------------------- session changes */\n\n/**\n * 「本会话改了什么」:代码行背景 + 左侧改动栏(Cursor 风格)。\n *\n * 三种形态必须**一眼分得开**,光靠绿/红两个色相不够(色弱用户看新增行和删除行\n * 会糊成同一片),所以每种的左侧色条宽度/花样的语义也不同:\n * - 新增:实心绿条;\n * - 修改:实心蓝条(不是绿:修改块里既有留下的行也有新写的行,用\"中性的已变更\"\n * 色不会误导读者以为整块都是新内容);\n * - 删除:**斜纹**红底 + 更粗的红条 —— 这一行其实已经不存在了,只在中止位置上\n * 留个锚点,斜纹是这一层\"虚\"的信息里最不容易被当成真内容的花样。\n *\n * 选择器带上 `.cm-line`:`.cm-activeLine` 由 CodeMirror 的主题注入(运行时\n * `<style>`,通常排在本文档之后),只写 `.rvs-change-*` 两者同权重时会输给它,\n * 光标所在的那一行改动就会消失。\n */\n.rvs-edge .cm-line.rvs-change-add {\n background-color: rgba(46, 160, 67, .17);\n box-shadow:\n inset 3px 0 0 0 #2ea043,\n inset 0 -1px 0 0 rgba(46, 160, 67, .28);\n}\n\n.rvs-edge .cm-line.rvs-change-mod {\n background-color: rgba(56, 139, 253, .15);\n box-shadow:\n inset 3px 0 0 0 #388bfd,\n inset 0 -1px 0 0 rgba(56, 139, 253, .28);\n}\n\n.rvs-edge .cm-line.rvs-change-del {\n background-color: rgba(248, 81, 73, .09);\n background-image: repeating-linear-gradient(135deg, rgba(248, 81, 73, .17) 0 6px, transparent 6px 12px);\n box-shadow: inset 4px 0 0 0 #f85149;\n}\n\n/* 改动栏本身:符号 + 一列与行背景同色的窄条,读起来才是一条\"改动边\"。 */\n.rvs-edge .cm-gutterElement.rvs-change-gutter-add {\n box-shadow: inset 2px 0 0 0 #2ea043;\n}\n\n.rvs-edge .cm-gutterElement.rvs-change-gutter-mod {\n box-shadow: inset 2px 0 0 0 #388bfd;\n}\n\n.rvs-edge .cm-gutterElement.rvs-change-gutter-del {\n box-shadow: inset 2px 0 0 0 #f85149;\n}\n\n.rvs-edge .rvs-change-mark {\n display: inline-block;\n width: 12px;\n font-family: var(--rvs-mono);\n font-size: 12px;\n line-height: 1;\n text-align: center;\n user-select: none;\n}\n\n.rvs-edge .rvs-change-mark-add { color: #2ea043; }\n.rvs-edge .rvs-change-mark-mod { color: #388bfd; }\n.rvs-edge .rvs-change-mark-del { color: #f85149; }\n\n/**\n * 行内(字符级)差异:上面那三种底色回答\"哪几行变了\",这两条回答\"那一行的哪几个\n * 字变了\"。只改一个词的编辑,整行同色时完全看不出改了什么。\n *\n * `rvs-change-word-add` 刻意**只给背景、不给 `color`**:代码有自己的语法着色,\n * 覆盖前景色会把关键字/字符串/注释染成同一种颜色,比不标还难读。透明度压在行\n * 底色之上一点点,叠起来仍读得出\"整块改动\"的边界,又能看出块内哪几个字是新的。\n */\n.rvs-edge .rvs-change-word-add {\n background-color: rgba(46, 160, 67, .22);\n border-radius: 2px;\n}\n\n/**\n * 被删掉、在新文件里已经不存在的内容:零宽 widget,红底 + 删除线。\n *\n * 删除线画在 chip 自己身上(chip 是独立元素,不会波及周围代码);`white-space:\n * pre` 保留被删文本自己的缩进 —— 截断/去空白都会让 chip 谎报\"删了什么\"。\n * 字号略小 + 降一点不透明度,是让它读起来像\"残留物\"而不是一行真代码。\n */\n.rvs-edge .rvs-change-word-del {\n padding: 0 3px;\n margin: 0 1px;\n border-radius: 3px;\n background-color: rgba(248, 81, 73, .18);\n font-size: .92em;\n text-decoration: line-through;\n opacity: .85;\n white-space: pre;\n}\n\n/**\n * 撑宽度的隐形符号:只用它占位,绝不显示(见 `change-marks.ts` 的 SpacerMarker,\n * CodeMirror 会给这个 spacer 元素加 `visibility: hidden`,这里不重复声明)。\n */\n.rvs-edge .rvs-change-spacer {\n opacity: 0;\n}\n\n/* 浅色主题:同样的色相,压暗 + 提高不透明度,在近白底上才看得清。 */\nbody:not([data-ds-dark-theme]) .rvs-edge .cm-line.rvs-change-add {\n background-color: rgba(31, 136, 61, .13);\n box-shadow:\n inset 3px 0 0 0 #1f883d,\n inset 0 -1px 0 0 rgba(31, 136, 61, .22);\n}\n\nbody:not([data-ds-dark-theme]) .rvs-edge .cm-line.rvs-change-mod {\n background-color: rgba(9, 105, 218, .12);\n box-shadow:\n inset 3px 0 0 0 #0969da,\n inset 0 -1px 0 0 rgba(9, 105, 218, .22);\n}\n\nbody:not([data-ds-dark-theme]) .rvs-edge .cm-line.rvs-change-del {\n background-color: rgba(207, 34, 46, .07);\n background-image: repeating-linear-gradient(135deg, rgba(207, 34, 46, .14) 0 6px, transparent 6px 12px);\n box-shadow: inset 4px 0 0 0 #cf222e;\n}\n\nbody:not([data-ds-dark-theme]) .rvs-edge .cm-gutterElement.rvs-change-gutter-add {\n box-shadow: inset 2px 0 0 0 #1f883d;\n}\n\nbody:not([data-ds-dark-theme]) .rvs-edge .cm-gutterElement.rvs-change-gutter-mod {\n box-shadow: inset 2px 0 0 0 #0969da;\n}\n\nbody:not([data-ds-dark-theme]) .rvs-edge .cm-gutterElement.rvs-change-gutter-del {\n box-shadow: inset 2px 0 0 0 #cf222e;\n}\n\nbody:not([data-ds-dark-theme]) .rvs-edge .rvs-change-mark-add { color: #1f883d; }\nbody:not([data-ds-dark-theme]) .rvs-edge .rvs-change-mark-mod { color: #0969da; }\nbody:not([data-ds-dark-theme]) .rvs-edge .rvs-change-mark-del { color: #cf222e; }\n\n/* 浅色主题的行内差异:同样的色相压暗,在近白底上才看得清(暗色那套会糊成一片)。 */\nbody:not([data-ds-dark-theme]) .rvs-edge .rvs-change-word-add {\n background-color: rgba(26, 127, 55, .18);\n}\n\nbody:not([data-ds-dark-theme]) .rvs-edge .rvs-change-word-del {\n background-color: rgba(207, 34, 46, .14);\n}\n\n/**\n * 改动跳转按钮:浮在编辑器右上角。\n *\n * 放在编辑器上而不是面板里 —— 文件打开后光标就在这里,而面板可能被收起、被滚走。\n * 容器 `.rvs-body` 已经是 `position: relative`(它本来用于挂 find 浮层),所以这里\n * 只做绝对定位,不引入新的定位上下文。`pointer-events` 只开在按钮上:这块区域压在\n * 编辑器文字上方,整块可点会把右上角那几行变成死区。\n */\n.rvs-change-nav {\n position: absolute;\n top: 8px;\n right: 14px;\n z-index: 5;\n display: flex;\n align-items: center;\n gap: 2px;\n padding: 3px 4px;\n border: 1px solid var(--rvs-border2);\n border-radius: 8px;\n background: Canvas;\n box-shadow: 0 2px 8px rgba(0, 0, 0, .28);\n pointer-events: none;\n}\n\n/* The counter is what makes this read as \"3 of 7 changes\" rather than two\n anonymous arrows — and it keeps the buttons from looking like decoration. */\n.rvs-change-nav-count {\n padding: 0 6px 0 4px;\n color: var(--rvs-fg2);\n font-size: 11px;\n font-variant-numeric: tabular-nums;\n line-height: 22px;\n white-space: nowrap;\n}\n\n.rvs-change-nav-btn {\n width: 26px;\n height: 22px;\n padding: 0;\n border: 1px solid transparent;\n border-radius: 5px;\n background: transparent;\n color: var(--rvs-fg);\n font-size: 13px;\n font-weight: 600;\n line-height: 20px;\n text-align: center;\n cursor: pointer;\n pointer-events: auto;\n}\n\n.rvs-change-nav-btn:hover {\n border-color: var(--rvs-border2);\n background: var(--rvs-hover);\n color: var(--rvs-brand);\n}\n\n.rvs-change-nav-btn:active { background: var(--rvs-active); }\n\n/**\n * 悬浮在改动行上出现的「撤销这处改动」按钮。\n *\n * 它**不在编辑器的 DOM 里**:`undo-affordance.ts` 把它当 `position: fixed` 的元素挂到\n * `document.body`(和「添加到对话」那个浮层同一套做法),所以这里的选择器不带\n * `.rvs-edge` —— 带上了就一条都不生效。固定定位 + 高 z-index 是刻意的:宿主容器有\n * `overflow: hidden`,挂在里面会被 CodeMirror 的滚动层裁掉。\n *\n * 颜色按\"危险动作\"给:默认红边红字、悬停实心红。底色不透明(不用半透明的 overlay\n * token),否则后面的代码文字会透出来。\n */\n.rvs-undo-affordance {\n position: fixed;\n z-index: 2100;\n display: inline-flex;\n align-items: center;\n gap: 4px;\n max-width: 220px;\n padding: 3px 9px;\n border: 1px solid #f85149;\n border-radius: 6px;\n background-color: #1e2229;\n color: #ffb4ae;\n /* 按钮挂在 body 上,没有 .rvs-edge 的字体上下文:跟随宿主页面字体。 */\n font-family: inherit;\n font-size: 12px;\n line-height: 1.5;\n white-space: nowrap;\n cursor: pointer;\n box-shadow: 0 6px 18px rgba(0, 0, 0, .45);\n /* 纵向以悬停行的中线对齐(横向坐标由 JS 按行尾算好)。 */\n transform: translateY(-50%);\n}\n\n.rvs-undo-affordance:hover {\n background-color: #f85149;\n border-color: #f85149;\n color: #fff;\n}\n\n.rvs-undo-affordance:active {\n transform: translateY(-50%) scale(.97);\n}\n\n.rvs-undo-affordance:focus-visible {\n outline: 2px solid #58a6ff;\n outline-offset: 1px;\n}\n\n/* 删除锚点:这一行其实已经不在了,虚线边框把它和\"真实存在的行\"分开。 */\n.rvs-undo-affordance[data-kind=\"del\"] {\n border-style: dashed;\n}\n\nbody:not([data-ds-dark-theme]) .rvs-undo-affordance {\n border-color: #cf222e;\n background-color: #ffffff;\n color: #a40e26;\n box-shadow: 0 6px 18px rgba(31, 35, 40, .18);\n}\n\nbody:not([data-ds-dark-theme]) .rvs-undo-affordance:hover {\n background-color: #cf222e;\n border-color: #cf222e;\n color: #fff;\n}\n\nbody:not([data-ds-dark-theme]) .rvs-undo-affordance:focus-visible {\n outline-color: #4d6bfe;\n}\n\n/**\n * CodeMirror's own search decorations, also stated here: the editor's theme is\n * injected by the library, and these must stay readable even when only this\n * stylesheet is in play.\n */\n.rvs-edge .cm-searchMatch {\n background-color: rgba(226, 183, 20, .30);\n outline: 1px solid rgba(226, 183, 20, .55);\n}\n\n.rvs-edge .cm-searchMatch-selected {\n background-color: rgba(226, 183, 20, .48);\n}\n\n.rvs-edge .cm-selectionBackground,\n.rvs-edge .cm-content ::selection {\n background-color: rgba(58, 110, 165, .55) !important;\n}\n\n/** Build stamp in the editor info bar: identifies the loaded client bundle. */\n.rvs-build {\n flex: none;\n padding: 0 5px;\n border: 1px solid var(--rvs-border);\n border-radius: 4px;\n color: var(--rvs-fg3);\n font-size: 10px;\n font-family: var(--rvs-mono);\n opacity: .85;\n user-select: text;\n}\n\n.rvs-find {\n position: absolute;\n top: 6px;\n right: 18px;\n z-index: 30;\n display: flex;\n align-items: center;\n gap: 3px;\n padding: 3px 4px 3px 7px;\n border: 1px solid var(--rvs-border2);\n border-radius: 8px;\n background-color: Canvas;\n background-image: linear-gradient(var(--dsw-alias-bg-overlay, transparent), var(--dsw-alias-bg-overlay, transparent));\n color: var(--rvs-fg);\n box-shadow: 0 4px 14px rgba(0, 0, 0, .28);\n font-size: 12px;\n}\n\n.rvs-find-input {\n width: 170px;\n padding: 2px 6px;\n border: 1px solid var(--rvs-border2);\n border-radius: 5px;\n background: var(--rvs-input);\n color: var(--rvs-fg);\n font: inherit;\n font-size: 12px;\n outline: none;\n}\n\n.rvs-find-input:focus {\n border-color: var(--rvs-brand);\n}\n\n.rvs-find-count {\n flex: none;\n min-width: 54px;\n text-align: center;\n color: var(--rvs-fg3);\n font-size: 11px;\n white-space: nowrap;\n}\n\n.rvs-find-count.none {\n color: var(--rvs-git-d);\n}\n\n.rvs-code {\n flex: 1;\n min-height: 0;\n overflow: auto;\n margin: 0;\n padding: 6px 0 16px;\n font-family: var(--rvs-mono);\n font-size: 12.5px;\n line-height: 18px;\n white-space: pre;\n scrollbar-width: thin;\n background: var(--rvs-code-bg);\n color: var(--rvs-code-fg);\n}\n\n.rvs-code-line {\n display: flex;\n align-items: flex-start;\n min-width: max-content;\n}\n\n.rvs-code-line:hover {\n background: var(--rvs-hover);\n}\n\n.rvs-code-text {\n flex: 1;\n padding-right: 10px;\n white-space: pre-wrap;\n word-break: break-word;\n user-select: text;\n}\n\n/* ---------------------------------------------------------- history list */\n\n.rvs-commit-item {\n display: flex;\n align-items: center;\n gap: 6px;\n width: 100%;\n padding: 4px 8px 4px 4px;\n border: none;\n background: transparent;\n color: var(--rvs-fg);\n font: inherit;\n font-size: 12.5px;\n line-height: 18px;\n text-align: left;\n cursor: pointer;\n}\n\n.rvs-commit-item:hover {\n background: var(--rvs-hover);\n}\n\n.rvs-commit-item.selected {\n background: var(--rvs-active);\n}\n\n.rvs-graph-svg {\n flex: none;\n display: block;\n}\n\n.rvs-commit-hash {\n flex: none;\n font-family: var(--rvs-mono);\n font-size: 11px;\n color: var(--rvs-fg3);\n}\n\n.rvs-commit-msg {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.rvs-commit-date {\n flex: none;\n font-size: 11px;\n color: var(--rvs-fg3);\n}\n\n.rvs-ref-badge {\n flex: none;\n padding: 0 5px;\n border-radius: 8px;\n border: 1px solid var(--rvs-border2);\n font-size: 10px;\n line-height: 15px;\n color: var(--rvs-fg2);\n}\n\n.rvs-ref-badge.head {\n border-color: var(--rvs-brand);\n background: rgba(77, 107, 254, .2);\n color: var(--rvs-fg);\n}\n\n.rvs-ref-badge.tag {\n border-color: rgba(226, 183, 20, .55);\n background: rgba(226, 183, 20, .16);\n color: var(--rvs-git-m);\n}\n\n.rvs-detail {\n margin: 0 6px 8px;\n padding: 8px 10px;\n border: 1px solid var(--rvs-border2);\n border-left: 3px solid var(--rvs-accent);\n border-radius: 8px;\n background: var(--rvs-layer3);\n font-size: 12px;\n}\n\n.rvs-detail-meta {\n color: var(--rvs-fg3);\n font-size: 11px;\n margin-bottom: 6px;\n white-space: pre-wrap;\n}\n\n.rvs-file-row {\n display: flex;\n align-items: center;\n gap: 6px;\n padding: 2px 4px;\n border-radius: 5px;\n cursor: pointer;\n}\n\n.rvs-file-row:hover {\n background: var(--rvs-hover);\n}\n\n.rvs-file-path {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n /* Same tail-first truncation as the info bar; `plaintext` keeps the path's\n own LTR order (see .rvs-einfo-path). */\n direction: rtl;\n unicode-bidi: plaintext;\n text-align: left;\n}\n\n.rvs-file-status {\n flex: none;\n width: 16px;\n text-align: center;\n font-weight: 700;\n font-size: 11px;\n}\n\n/* ---------------------------------------------------- floating editor window */\n\n/**\n * The floating editor lives in the host's frame-wide overlay layer\n * (`position: absolute; inset: 0; z-index: 20`, click-through, children opt\n * back into pointer events). It is placed with viewport coordinates, so it\n * floats above every column without joining any of their stacking contexts.\n *\n * `.rvs-root` carries the whole design-system token set and is row-flex by\n * default; this frame overrides the direction to a column and takes its size\n * from the inline rect the component keeps.\n */\n.rvs-float-window {\n position: fixed;\n flex-direction: column;\n /* The rect maths (clamp/persist) is in border-box terms; without this the\n painted border would sit one pixel outside the computed rectangle. */\n box-sizing: border-box;\n min-width: 360px;\n min-height: 220px;\n overflow: hidden;\n border: 1px solid var(--rvs-border2);\n border-radius: 10px;\n background: var(--rvs-code-bg);\n color: var(--rvs-code-fg);\n box-shadow: 0 18px 60px rgba(0, 0, 0, .45), 0 2px 8px rgba(0, 0, 0, .28);\n z-index: 30;\n /* The overlay layer itself is click-through; the window opts back in even if\n the host wraps each overlay entry in its own element. */\n pointer-events: auto;\n}\n\n/* Minimized: the frame IS the ball (see `.rvs-float-mini`); the body stays\n mounted but hidden, and it must not impose a minimum height. */\n.rvs-float-window[data-rvs-float=\"collapsed\"] {\n min-height: 0;\n}\n\n/**\n * Minimized window = floating ball.\n *\n * A full-width 34px bar was too long: it read as a broken window and covered a\n * whole strip. The ball is a small circle at the window's own corner, draggable\n * anywhere, restored by a click (or Enter/Space). It keeps the dirty dot, so\n * \"the editor is out and has unsaved edits\" is still one glance away.\n */\n.rvs-float-window.rvs-float-mini {\n min-width: 0;\n min-height: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n border-radius: 50%;\n cursor: move;\n touch-action: none;\n}\n\n.rvs-float-ball {\n flex: 1;\n /* Stretch to the circle: the frame is a COLUMN flex box with centred items,\n so without this the hit area (and the absolutely positioned dot) would be\n sized by the 16px glyph instead of the 44px ball. */\n width: 100%;\n height: 100%;\n box-sizing: border-box;\n display: flex;\n align-items: center;\n justify-content: center;\n position: relative;\n border-radius: 50%;\n color: var(--rvs-brand);\n font-size: 16px;\n line-height: 1;\n cursor: move;\n outline: none;\n}\n\n.rvs-float-ball:hover,\n.rvs-float-ball:focus-visible {\n background: var(--rvs-hover);\n}\n\n.rvs-float-ball-glyph {\n pointer-events: none;\n}\n\n/* Unsaved edits stay visible while minimized. */\n.rvs-float-ball-dot {\n position: absolute;\n top: 8px;\n right: 8px;\n width: 8px;\n height: 8px;\n border-radius: 50%;\n background: var(--rvs-git-m);\n box-shadow: 0 0 0 2px var(--rvs-code-bg);\n pointer-events: none;\n}\n\n/* While a drag/resize gesture runs the pointer must not select editor text. */\n.rvs-float-window.dragging {\n user-select: none;\n}\n\n.rvs-float-titlebar {\n flex: none;\n display: flex;\n align-items: center;\n gap: 7px;\n height: 34px;\n padding: 0 6px 0 10px;\n border-bottom: 1px solid var(--rvs-border);\n background: var(--rvs-layer3);\n cursor: move;\n user-select: none;\n touch-action: none;\n}\n\n.rvs-float-glyph {\n color: var(--rvs-brand);\n font-size: 11px;\n}\n\n.rvs-float-title {\n flex: none;\n font-size: 12px;\n font-weight: 600;\n color: var(--rvs-fg2);\n}\n\n.rvs-float-file {\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n font-size: 11.5px;\n color: var(--rvs-fg3);\n direction: rtl;\n unicode-bidi: plaintext;\n text-align: left;\n}\n\n.rvs-float-spacer {\n flex: 1;\n}\n\n.rvs-float-btn {\n flex: none;\n width: 22px;\n height: 22px;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 0;\n border: none;\n border-radius: 5px;\n background: transparent;\n color: var(--rvs-fg3);\n font: inherit;\n font-size: 12px;\n line-height: 1;\n cursor: pointer;\n}\n\n.rvs-float-btn:hover {\n background: var(--rvs-hover);\n color: var(--rvs-fg);\n}\n\n.rvs-float-close:hover {\n background: rgba(241, 76, 76, .18);\n color: var(--rvs-danger);\n}\n\n.rvs-float-body {\n flex: 1 1 0;\n min-height: 0;\n min-width: 0;\n display: flex;\n overflow: hidden;\n background: var(--rvs-code-bg);\n}\n\n/* Keep the height chain pinned: the editor pane grows inside this box. */\n.rvs-float-body > .rvs-pane {\n flex: 1 1 0;\n min-height: 0;\n min-width: 0;\n}\n\n/* Eight grips, placed INSIDE the border box: the window clips overflow (rounded\n corners), so grips hanging outside would lose half their hit area. Corners\n are larger so they can actually be hit. */\n.rvs-float-resize {\n position: absolute;\n z-index: 3;\n}\n\n.rvs-float-resize-n { top: 0; left: 10px; right: 10px; height: 6px; cursor: ns-resize; }\n.rvs-float-resize-s { bottom: 0; left: 10px; right: 10px; height: 6px; cursor: ns-resize; }\n.rvs-float-resize-e { top: 10px; bottom: 10px; right: 0; width: 6px; cursor: ew-resize; }\n.rvs-float-resize-w { top: 10px; bottom: 10px; left: 0; width: 6px; cursor: ew-resize; }\n.rvs-float-resize-ne { top: 0; right: 0; width: 12px; height: 12px; cursor: nesw-resize; }\n.rvs-float-resize-nw { top: 0; left: 0; width: 12px; height: 12px; cursor: nwse-resize; }\n.rvs-float-resize-se { bottom: 0; right: 0; width: 12px; height: 12px; cursor: nwse-resize; }\n.rvs-float-resize-sw { bottom: 0; left: 0; width: 12px; height: 12px; cursor: nesw-resize; }\n\n/* Shown by whichever surface the single editor left behind (centre tab or the\n sidebar's editor panel). */\n.rvs-float-placeholder {\n flex: 1;\n min-height: 0;\n height: 100%;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 10px;\n padding: 24px;\n text-align: center;\n color: var(--rvs-fg3);\n font-size: 12.5px;\n}\n\n.rvs-float-placeholder-title {\n color: var(--rvs-fg2);\n font-size: 13px;\n font-weight: 600;\n}\n\n.rvs-float-placeholder-text {\n max-width: 360px;\n line-height: 1.6;\n}\n\n/* --------------------------------------------------------- context menu */\n\n.rvs-portal {\n font-family: inherit;\n}\n\n/**\n * Floating surfaces (menu / modal / toast) paint an OPAQUE base first and then\n * lay the skin's overlay token on top. The token is frequently translucent, so\n * using it alone let the panel's own text bleed through the menu and overlap\n * the entries; `Canvas` follows the resolved light/dark scheme, so the base is\n * correct in either theme.\n */\n.rvs-menu,\n.rvs-modal-box,\n.rvs-toast {\n background-color: Canvas;\n background-image: linear-gradient(var(--dsw-alias-bg-overlay, transparent), var(--dsw-alias-bg-overlay, transparent));\n color: var(--rvs-fg);\n}\n\n.rvs-menu {\n position: fixed;\n z-index: 1000;\n min-width: 172px;\n max-height: calc(100vh - 16px);\n overflow-y: auto;\n padding: 4px;\n border: 1px solid var(--rvs-border2);\n border-radius: 8px;\n box-shadow: 0 6px 20px rgba(0, 0, 0, .35);\n font-size: 12.5px;\n line-height: 18px;\n}\n\n.rvs-menu-sep {\n height: 1px;\n margin: 4px 6px;\n background: var(--rvs-border);\n}\n\n.rvs-menu-item {\n display: flex;\n align-items: center;\n gap: 8px;\n width: 100%;\n padding: 5px 10px;\n border: none;\n border-radius: 6px;\n background: transparent;\n color: inherit;\n font: inherit;\n text-align: left;\n cursor: pointer;\n}\n\n.rvs-menu-item:hover {\n background: var(--rvs-hover);\n}\n\n.rvs-menu-item.danger {\n color: var(--vsc-danger, #f14c4c);\n}\n\n.rvs-menu-item.danger:hover {\n background: rgba(241, 76, 76, .16);\n}\n\n.rvs-menu-item:disabled {\n opacity: .45;\n cursor: default;\n}\n\n.rvs-menu-item:disabled:hover {\n background: transparent;\n}\n\n.rvs-menu-hint {\n margin-left: auto;\n font-size: 11px;\n opacity: .6;\n}\n\n/* ------------------------------------------------------- modal + toast */\n\n.rvs-modal-mask {\n position: fixed;\n inset: 0;\n z-index: 100000;\n display: flex;\n align-items: center;\n justify-content: center;\n background: rgba(0, 0, 0, .45);\n backdrop-filter: blur(2px);\n animation: rvsFade .12s ease;\n}\n\n.rvs-modal-box {\n width: min(420px, calc(100vw - 40px));\n padding: 16px 18px;\n border: 1px solid var(--rvs-border2);\n border-radius: 10px;\n color: var(--rvs-fg);\n box-shadow: 0 12px 40px rgba(0, 0, 0, .38);\n font-size: 13px;\n animation: rvsPop .14s ease;\n}\n\n.rvs-modal-title {\n margin-bottom: 8px;\n font-size: 14px;\n font-weight: 600;\n}\n\n.rvs-modal-text {\n margin-bottom: 12px;\n color: var(--rvs-fg2);\n font-size: 12.5px;\n line-height: 1.6;\n word-break: break-word;\n white-space: pre-wrap;\n}\n\n.rvs-modal-row {\n display: flex;\n justify-content: flex-end;\n gap: 8px;\n margin-top: 14px;\n}\n\n.rvs-modal-btn {\n padding: 5px 14px;\n border: 1px solid var(--rvs-border2);\n border-radius: 7px;\n background: transparent;\n color: var(--rvs-fg);\n font: inherit;\n font-size: 12.5px;\n cursor: pointer;\n}\n\n.rvs-modal-btn:hover {\n background: var(--rvs-hover);\n}\n\n.rvs-modal-btn.primary {\n border-color: transparent;\n background: var(--rvs-accent);\n color: #fff;\n}\n\n.rvs-modal-btn.danger {\n border-color: transparent;\n background: var(--rvs-danger);\n color: #fff;\n}\n\n.rvs-toasts {\n position: fixed;\n right: 16px;\n bottom: 18px;\n z-index: 100001;\n display: flex;\n flex-direction: column;\n gap: 8px;\n align-items: flex-end;\n pointer-events: none;\n}\n\n.rvs-toast {\n max-width: 340px;\n padding: 7px 12px;\n border: 1px solid var(--rvs-border2);\n border-radius: 8px;\n color: var(--rvs-fg);\n box-shadow: 0 6px 20px rgba(0, 0, 0, .35);\n font-size: 12.5px;\n line-height: 1.5;\n word-break: break-word;\n white-space: pre-wrap;\n animation: rvsPop .14s ease;\n}\n\n.rvs-toast.error {\n border-color: rgba(241, 76, 76, .6);\n background-image: linear-gradient(rgba(58, 31, 34, .92), rgba(58, 31, 34, .92));\n}\n\n/* ------------------------------------------------------------- terminal */\n\n.rvs-term-tabs {\n flex: none;\n display: flex;\n align-items: stretch;\n gap: 2px;\n height: 32px;\n padding: 0 4px;\n overflow-x: auto;\n overflow-y: hidden;\n border-bottom: 1px solid var(--rvs-border);\n scrollbar-width: none;\n}\n\n.rvs-term-tabs::-webkit-scrollbar {\n height: 0;\n}\n\n.rvs-term-tab {\n flex: none;\n display: flex;\n align-items: center;\n gap: 6px;\n max-width: 180px;\n margin: 4px 0;\n padding: 0 6px 0 9px;\n border: none;\n border-radius: 6px;\n background: transparent;\n color: var(--rvs-fg3);\n font: inherit;\n font-size: 12px;\n cursor: pointer;\n}\n\n/* Wrapper that pairs a tab with its close button. The two are siblings so the\n ✕ can be a real focusable <button> (a nested one is invalid HTML and left\n keyboard users no way to close a tab). */\n.rvs-term-tabwrap {\n position: relative;\n flex: none;\n display: flex;\n align-items: center;\n}\n\n.rvs-term-tabwrap .rvs-term-tab {\n padding-right: 22px;\n}\n\n.rvs-term-tabwrap .rvs-etab-close {\n position: absolute;\n right: 3px;\n opacity: 0;\n}\n\n.rvs-term-tabwrap:hover .rvs-etab-close,\n.rvs-term-tabwrap:focus-within .rvs-etab-close {\n opacity: .8;\n}\n\n.rvs-term-tab:hover {\n background: var(--rvs-hover);\n color: var(--rvs-fg);\n}\n\n.rvs-term-tab.active {\n background: var(--rvs-layer3);\n color: var(--rvs-fg);\n}\n\n.rvs-term-tab.dead {\n color: var(--rvs-git-d);\n}\n\n.rvs-term-name {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.rvs-term-body {\n flex: 1;\n min-height: 0;\n position: relative;\n overflow: hidden;\n}\n\n.rvs-term-slot {\n position: absolute;\n inset: 0;\n}\n\n.rvs-term-dead {\n height: 100%;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 8px;\n background: var(--rvs-code-bg);\n color: var(--rvs-code-fg);\n}\n\n/* `.rvs-toast.error` is styled with the overlay form further up, so the popup\n opacity convention (an opaque Canvas base plus a token layer) still governs\n it. A flat `background: #3a1f22` shorthand sat here and overrode that chain —\n same opaque result, but outside the convention. */\n\n@keyframes rvsFade {\n from { opacity: 0; }\n to { opacity: 1; }\n}\n\n@keyframes rvsPop {\n from { opacity: 0; transform: scale(.97) translateY(3px); }\n to { opacity: 1; transform: none; }\n}\n\n.rvs-search-row {\n display: flex;\n gap: 6px;\n padding: 6px 8px;\n border-bottom: 1px solid var(--rvs-border);\n}\n\n/* --------------------------------------------------------- search options */\n\n.rvs-toggles {\n flex: none;\n display: inline-flex;\n border: 1px solid var(--rvs-border2);\n border-radius: 6px;\n overflow: hidden;\n}\n\n.rvs-toggle {\n min-width: 26px;\n height: 22px;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 0 5px;\n border: none;\n border-right: 1px solid var(--rvs-border);\n background: transparent;\n color: var(--rvs-fg3);\n font: inherit;\n cursor: pointer;\n}\n\n.rvs-toggle:last-child {\n border-right: none;\n}\n\n.rvs-toggle:hover {\n background: var(--rvs-hover);\n color: var(--rvs-fg);\n}\n\n.rvs-toggle.active {\n background: var(--rvs-active);\n color: var(--rvs-fg);\n box-shadow: inset 0 0 0 1px var(--rvs-brand);\n}\n\n.rvs-glyph {\n font-family: var(--rvs-mono);\n font-weight: 600;\n line-height: 1;\n letter-spacing: -0.02em;\n}\n\n.rvs-glyph-underline {\n text-decoration: underline;\n}\n\n.rvs-scope-label {\n flex: none;\n align-self: center;\n font-size: 11px;\n color: var(--rvs-fg3);\n white-space: nowrap;\n}\n\n/* --------------------------------------------------------- search results */\n\n.rvs-hit {\n display: flex;\n align-items: center;\n gap: 6px;\n width: 100%;\n padding: 3px 10px;\n border: none;\n background: transparent;\n color: var(--rvs-fg);\n font: inherit;\n font-size: 12.5px;\n text-align: left;\n cursor: pointer;\n}\n\n.rvs-hit:hover {\n background: var(--rvs-hover);\n}\n\n.rvs-hit-file {\n font-weight: 600;\n}\n\n.rvs-hit-line {\n padding-left: 30px;\n align-items: flex-start;\n font-family: var(--rvs-mono);\n font-size: 12px;\n color: var(--rvs-fg2);\n}\n\n.rvs-hit-count {\n flex: none;\n min-width: 18px;\n padding: 0 5px;\n border-radius: 9px;\n background: var(--rvs-hover);\n font-size: 10.5px;\n line-height: 15px;\n text-align: center;\n color: var(--rvs-fg3);\n}\n\n.rvs-hit-lineno {\n flex: none;\n width: 3.2em;\n text-align: right;\n color: var(--rvs-fg3);\n opacity: .8;\n font-size: 11px;\n}\n\n.rvs-hit-text {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: pre;\n direction: ltr;\n}\n\n.rvs-mark {\n padding: 0 1px;\n border-radius: 2px;\n background: rgba(226, 183, 20, .3);\n color: inherit;\n}\n\n.rvs-hit-dir {\n flex: none;\n color: var(--rvs-fg3);\n font-size: 11px;\n}\n\n.rvs-hit-name {\n flex: none;\n font-weight: 600;\n}\n\n.rvs-kbd {\n padding: 0 4px;\n border: 1px solid var(--rvs-border2);\n border-radius: 4px;\n font-size: 10px;\n color: var(--rvs-fg3);\n}\n\n/* --- SCM: repository directory row (git runs in THIS directory) ------------- */\n\n.rvs-repo-bar {\n display: flex;\n align-items: center;\n gap: 4px;\n padding: 4px 8px 4px 10px;\n border-bottom: 1px solid var(--rvs-border);\n background: var(--rvs-bg2, var(--dsw-alias-bg-layer-1, #1b1b1b));\n font-size: 11px;\n flex: none;\n}\n\n.rvs-repo-bar-label {\n color: var(--rvs-fg3);\n white-space: nowrap;\n}\n\n.rvs-repo-bar-path {\n flex: 1 1 auto;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n direction: rtl;\n unicode-bidi: plaintext;\n text-align: left;\n padding: 2px 6px;\n border: 1px solid var(--rvs-border2);\n border-radius: 4px;\n background: var(--rvs-bg3, rgba(255, 255, 255, .04));\n color: var(--rvs-fg);\n font-family: var(--rvs-mono, ui-monospace, Consolas, monospace);\n font-size: 11px;\n cursor: pointer;\n}\n\n.rvs-repo-bar-path:hover {\n border-color: var(--rvs-brand);\n color: var(--rvs-brand-fg, var(--rvs-fg));\n}\n\n.rvs-repo-bar-fix {\n margin-top: 10px;\n display: flex;\n justify-content: center;\n}\n\n/* --- SCM: repository directory picker -------------------------------------- */\n\n.rvs-repopick {\n width: min(560px, calc(100vw - 40px));\n display: flex;\n flex-direction: column;\n gap: 8px;\n}\n\n.rvs-repopick .rvs-modal-title {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 8px;\n}\n\n.rvs-repopick-bar {\n display: flex;\n align-items: center;\n flex-wrap: wrap;\n gap: 2px;\n min-height: 22px;\n font-family: var(--rvs-mono, ui-monospace, Consolas, monospace);\n font-size: 12px;\n}\n\n.rvs-crumb-wrap {\n display: inline-flex;\n align-items: center;\n gap: 2px;\n color: var(--rvs-fg3);\n}\n\n.rvs-crumb {\n border: none;\n background: none;\n padding: 1px 3px;\n border-radius: 3px;\n color: var(--rvs-brand-fg, var(--rvs-fg));\n font: inherit;\n cursor: pointer;\n}\n\n.rvs-crumb:hover {\n background: var(--rvs-row-hover, rgba(255, 255, 255, .07));\n}\n\n.rvs-repopick-probe {\n padding: 5px 8px;\n border-radius: 4px;\n font-size: 11.5px;\n border: 1px solid var(--rvs-border2);\n background: var(--rvs-bg3, rgba(255, 255, 255, .04));\n color: var(--rvs-fg2);\n}\n\n.rvs-repopick-probe.root {\n border-color: var(--rvs-ok, #2ea043);\n color: var(--rvs-ok, #2ea043);\n}\n\n.rvs-repopick-probe.inside {\n border-color: var(--rvs-brand);\n color: var(--rvs-brand-fg, var(--rvs-fg));\n}\n\n.rvs-repopick-found {\n display: flex;\n align-items: center;\n flex-wrap: wrap;\n gap: 4px;\n padding-bottom: 2px;\n}\n\n.rvs-repopick-found-label {\n color: var(--rvs-fg3);\n font-size: 11px;\n white-space: nowrap;\n}\n\n.rvs-repopick-list {\n flex: 1 1 auto;\n min-height: 150px;\n max-height: 42vh;\n overflow: auto;\n border: 1px solid var(--rvs-border);\n border-radius: 5px;\n background: var(--rvs-bg-layer, rgba(0, 0, 0, .18));\n padding: 2px;\n}\n\n.rvs-repopick-row {\n display: flex;\n align-items: center;\n gap: 6px;\n width: 100%;\n padding: 3px 6px;\n border: none;\n border-radius: 3px;\n background: none;\n color: var(--rvs-fg);\n font: inherit;\n font-size: 12px;\n text-align: left;\n cursor: pointer;\n}\n\n.rvs-repopick-row:hover {\n background: var(--rvs-row-hover, rgba(255, 255, 255, .07));\n}\n\n.rvs-repopick-row span {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.rvs-repopick-dim {\n color: var(--rvs-fg3);\n font-size: 11.5px;\n padding: 6px;\n}\n\n.rvs-repopick-type {\n display: flex;\n gap: 6px;\n align-items: center;\n}\n\n.rvs-repopick-type .rvs-input {\n flex: 1 1 auto;\n}\n\n.rvs-repopick-error {\n color: var(--rvs-error, #f85149);\n font-size: 11.5px;\n}\n";
14
+ const css$3 = "/**\n * VS Code-style design system for the remote explorer. Every surface below\n * is scoped under `.rvs-root` so the sheet can never leak into the host GUI,\n * and every host token carries a literal fallback: the plugin also renders in\n * the native right-Sidebar fallback and on hosts whose alias tokens differ.\n *\n * Palette and metrics are aligned with the reference implementation\n * (dsh-solution-explorer): 13px/20px panel type, 5px/10px tree rows with a\n * 10px radius, a 30px activity button with the badge tucked into the icon's\n * bottom-right corner, GitHub-Dark token colors, and the\n * `--e2b714 / #4ec9b0 / #f14c4c` modified/added/deleted triple.\n */\n\n.rvs-root {\n --rvs-fg: var(--dsw-alias-label-primary, #e6e6e6);\n --rvs-fg2: var(--dsw-alias-label-secondary, #b9b9b9);\n --rvs-fg3: var(--dsw-alias-label-tertiary, #8b8b8b);\n --rvs-hover: var(--dsw-alias-interactive-bg-hover, rgba(127, 127, 127, .14));\n --rvs-active: var(--dsw-alias-interactive-bg-active, rgba(77, 107, 254, .22));\n --rvs-border: var(--dsw-alias-border-l1, rgba(127, 127, 127, .25));\n --rvs-border2: var(--dsw-alias-border-l2, rgba(127, 127, 127, .38));\n --rvs-input: var(--dsw-alias-bg-input, rgba(127, 127, 127, .12));\n --rvs-overlay: var(--dsw-alias-bg-overlay, #24262e);\n --rvs-layer3: var(--dsw-alias-bg-layer-3, rgba(127, 127, 127, .16));\n --rvs-brand: var(--dsw-alias-brand-primary, #4d6bfe);\n --rvs-accent: var(--dsw-alias-button-info-fill, #4d6bfe);\n --rvs-danger: var(--dsw-alias-state-error-primary, #e05555);\n --rvs-git-m: #e2b714;\n --rvs-git-a: #4ec9b0;\n --rvs-git-d: #f14c4c;\n --rvs-git-r: #4ec9b0;\n --rvs-git-c: #58a6ff;\n --rvs-git-u: #f14c4c;\n --rvs-git-x: #6e6e6e;\n --rvs-add-bg: rgba(78, 201, 176, .15);\n --rvs-del-bg: rgba(241, 76, 76, .15);\n /* Opaque equivalents, for elements that must not show what scrolls under\n them (the pinned diff line numbers). */\n --rvs-add-solid: #e4f6f1;\n --rvs-del-solid: #fdeaea;\n --rvs-mono: 'Cascadia Code', 'Fira Code', 'JetBrains Mono', Consolas, 'Courier New', monospace;\n /* Opaque code surfaces (Light+ default; the dark block below flips them). */\n --rvs-code-bg: #ffffff;\n --rvs-code-fg: #1f1f1f;\n --rvs-code-gutter-fg: #237893;\n\n display: flex;\n flex-direction: row;\n height: 100%;\n min-height: 0;\n overflow: hidden;\n color: var(--rvs-fg);\n font-size: 13px;\n line-height: 20px;\n}\n\n/**\n * Code surfaces are OPAQUE, in the editor's own palette rather than the DSH\n * skin: syntax colors need a definite background to stay readable, and the\n * app shell marks its resolved scheme on <body> (same signal theme.ts reads).\n */\nbody[data-ds-dark-theme] .rvs-root {\n --rvs-code-bg: #1e1e1e;\n --rvs-code-fg: #d4d4d4;\n --rvs-code-gutter-fg: #858585;\n --rvs-add-solid: #16332e;\n --rvs-del-solid: #3b2325;\n}\n\n.rvs-edge {\n background: var(--rvs-code-bg);\n color: var(--rvs-code-fg);\n}\n\n.rvs-root *,\n.rvs-portal * {\n box-sizing: border-box;\n}\n\n/* ------------------------------------------------------------ activity rail */\n\n.rvs-rail {\n flex: none;\n width: 38px;\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 4px;\n padding: 8px 0;\n border-right: 1px solid var(--rvs-border);\n background: var(--dsw-specific-sidebar-fill, transparent);\n}\n\n.rvs-rail-btn {\n position: relative;\n flex: none;\n width: 30px;\n height: 30px;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 0;\n border: none;\n border-radius: 6px;\n background: transparent;\n color: var(--rvs-fg3);\n cursor: pointer;\n}\n\n.rvs-rail-btn:hover {\n color: var(--rvs-fg);\n background: var(--rvs-hover);\n}\n\n.rvs-rail-btn.active {\n color: var(--rvs-fg);\n background: var(--rvs-hover);\n box-shadow: inset 2px 0 0 var(--rvs-brand);\n}\n\n.rvs-rail-btn.danger:hover {\n color: var(--rvs-danger);\n}\n\n.rvs-rail-badge {\n position: absolute;\n bottom: 1px;\n right: 1px;\n min-width: 14px;\n height: 14px;\n padding: 0 3px;\n border-radius: 7px;\n background: var(--rvs-accent);\n color: #fff;\n font-size: 9px;\n font-weight: 700;\n line-height: 14px;\n text-align: center;\n pointer-events: none;\n}\n\n.rvs-rail-sep {\n flex: none;\n width: 20px;\n height: 1px;\n margin: 4px 0;\n background: var(--rvs-border);\n}\n\n/* -------------------------------------------------------- panel + editor */\n\n.rvs-column {\n flex: 1;\n min-width: 0;\n min-height: 0;\n display: flex;\n flex-direction: column;\n}\n\n.rvs-pane {\n display: flex;\n flex-direction: column;\n min-height: 0;\n min-width: 0;\n overflow: hidden;\n}\n\n/**\n * One lazily-mounted panel slot: a visited panel stays mounted and is merely\n * hidden, so returning to the explorer (or the terminal) neither refetches nor\n * flashes a reload.\n */\n.rvs-viewslot {\n flex: 1;\n min-height: 0;\n min-width: 0;\n flex-direction: column;\n height: 100%;\n overflow: hidden;\n}\n\n/**\n * Padding for the legacy views (workspaces / terminal) that draw no chrome.\n *\n * It MUST be a flex column: the workspaces view scroller is its child and gets\n * its bounded height only from a flex parent (`flex:1; min-height:0`). As a\n * plain block it grew with its content instead, so the panel clipped at the\n * viewslot's `overflow:hidden` and the binding list could not scroll at all —\n * a seventh workspace (and the wizard under it) was simply unreachable.\n */\n.rvs-viewpad {\n display: flex;\n flex-direction: column;\n min-height: 0;\n padding: 10px 12px 14px;\n box-sizing: border-box;\n}\n\n.rvs-pane.grow {\n flex: 1;\n}\n\n.rvs-header {\n flex: none;\n display: flex;\n align-items: center;\n gap: 6px;\n height: 34px;\n padding: 0 8px 0 10px;\n border-bottom: 1px solid var(--rvs-border);\n}\n\n.rvs-header .rvs-title {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n font-size: 11px;\n font-weight: 700;\n letter-spacing: .04em;\n text-transform: uppercase;\n color: var(--rvs-fg3);\n}\n\n.rvs-hbtn {\n flex: none;\n width: 22px;\n height: 22px;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 0;\n border: none;\n border-radius: 5px;\n background: transparent;\n color: var(--rvs-fg3);\n cursor: pointer;\n}\n\n.rvs-hbtn:hover {\n color: var(--rvs-fg);\n background: var(--rvs-hover);\n}\n\n.rvs-hbtn.danger:hover {\n color: #fff;\n background: var(--rvs-danger);\n}\n\n.rvs-hbtn:disabled {\n opacity: .4;\n cursor: default;\n}\n\n.rvs-scroll {\n flex: 1;\n min-height: 0;\n overflow: auto;\n scrollbar-width: thin;\n}\n\n.rvs-globalbar {\n flex: none;\n display: flex;\n align-items: center;\n gap: 6px;\n padding: 4px 8px;\n border-bottom: 1px solid var(--rvs-border);\n font-size: 11px;\n}\n\n.rvs-scroll::-webkit-scrollbar {\n width: 8px;\n height: 8px;\n}\n\n.rvs-scroll::-webkit-scrollbar-thumb {\n background: var(--dsw-alias-scrollbar-bg-l2, rgba(127, 127, 127, .4));\n border-radius: 4px;\n}\n\n.rvs-scroll::-webkit-scrollbar-thumb:hover {\n background: var(--dsw-alias-scrollbar-hover-l2, rgba(127, 127, 127, .6));\n}\n\n.rvs-scroll::-webkit-scrollbar-track {\n background: transparent;\n}\n\n.rvs-vsplit-handle {\n flex: none;\n position: relative;\n height: 7px;\n cursor: row-resize;\n touch-action: none;\n}\n\n.rvs-vsplit-handle::after {\n content: '';\n position: absolute;\n left: 0;\n right: 0;\n top: 3px;\n height: 1px;\n background: var(--rvs-border);\n}\n\n.rvs-vsplit-handle:hover::after,\n.rvs-vsplit-handle[data-dragging='true']::after {\n top: 2px;\n height: 3px;\n border-radius: 2px;\n background: var(--rvs-brand);\n}\n\n/* ------------------------------------------------------------------ tree */\n\n.rvs-tree {\n padding: 4px 6px 12px;\n min-width: 0;\n}\n\n.rvs-node {\n display: flex;\n align-items: center;\n gap: 6px;\n min-width: 0;\n width: 100%;\n padding: 4px 8px;\n border: none;\n background: transparent;\n border-radius: 8px;\n color: var(--rvs-fg);\n font: inherit;\n font-size: 13px;\n line-height: 18px;\n text-align: left;\n cursor: pointer;\n}\n\n.rvs-node:hover {\n background: var(--rvs-hover);\n}\n\n.rvs-node.selected {\n background: var(--rvs-active);\n}\n\n.rvs-node.cut {\n opacity: .5;\n}\n\n.rvs-node.drop-target {\n background: var(--rvs-active);\n outline: 1px solid var(--rvs-brand);\n outline-offset: -1px;\n}\n\n/* Where a file drag will land: a slip must not silently drop a tree into the\n wrong directory. */\n.rvs-drop-hint {\n margin: 2px 8px 4px;\n padding: 3px 8px;\n border: 1px dashed var(--rvs-brand);\n border-radius: 3px;\n font-size: 11px;\n color: var(--rvs-brand);\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n direction: rtl;\n unicode-bidi: plaintext;\n}\n/* Upload progress: one line under the header. A multi-minute transfer with no\n feedback is indistinguishable from a hung explorer, so the row names the\n count and the file on the wire. */\n.rvs-upload-bar {\n flex: none;\n display: flex;\n align-items: center;\n gap: 6px;\n padding: 3px 10px 4px;\n font-size: 11px;\n color: var(--dsw-alias-text-secondary, rgba(127, 127, 127, .95));\n}\n\n.rvs-upload-text {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n direction: rtl;\n unicode-bidi: plaintext;\n}\n\n.rvs-node-icon {\n flex: none;\n width: 16px;\n height: 16px;\n display: flex;\n align-items: center;\n justify-content: center;\n color: var(--rvs-fg3);\n}\n\n.rvs-node-name {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.rvs-node-actions {\n flex: none;\n display: none;\n align-items: center;\n gap: 2px;\n}\n\n.rvs-node:hover .rvs-node-actions {\n display: flex;\n}\n\n.rvs-git-letter {\n flex: none;\n width: 14px;\n margin-left: 2px;\n font-size: 11px;\n font-weight: 700;\n text-align: center;\n}\n\n.rvs-git-M, .rvs-letter-M { color: var(--rvs-git-m); }\n.rvs-git-A, .rvs-letter-A { color: var(--rvs-git-a); }\n.rvs-git-D, .rvs-letter-D { color: var(--rvs-git-d); }\n.rvs-git-R, .rvs-letter-R { color: var(--rvs-git-r); }\n.rvs-git-C, .rvs-letter-C { color: var(--rvs-git-c); }\n.rvs-git-U, .rvs-letter-U { color: var(--rvs-git-u); }\n.rvs-git-q, .rvs-letter-q, .rvs-git-\\\\? { color: var(--rvs-git-a); }\n.rvs-git-x, .rvs-letter-x { color: var(--rvs-git-x); }\n.rvs-git-T, .rvs-letter-T { color: var(--rvs-git-m); }\n\n.rvs-rename-input {\n flex: 1;\n min-width: 0;\n padding: 0 4px;\n border: 1px solid var(--rvs-brand);\n border-radius: 4px;\n background: var(--rvs-input);\n color: var(--rvs-fg);\n font: inherit;\n font-size: 13px;\n line-height: 18px;\n outline: none;\n}\n\n.rvs-empty {\n padding: 18px 14px;\n color: var(--rvs-fg3);\n font-size: 12px;\n text-align: center;\n line-height: 1.7;\n}\n\n.rvs-banner {\n margin: 6px 8px;\n padding: 6px 9px;\n border-radius: 6px;\n font-size: 12px;\n line-height: 1.5;\n word-break: break-word;\n white-space: pre-wrap;\n}\n\n.rvs-banner.info {\n background: var(--rvs-hover);\n color: var(--rvs-fg2);\n}\n\n.rvs-banner.error {\n background: rgba(200, 60, 60, .16);\n color: var(--rvs-danger);\n}\n\n.rvs-banner.warn {\n background: rgba(226, 183, 20, .14);\n color: var(--rvs-git-m);\n}\n\n/* --------------------------------------------------------- source control */\n\n.rvs-scm-section {\n border-bottom: 1px solid var(--rvs-border);\n}\n\n.rvs-scm-head {\n display: flex;\n align-items: center;\n gap: 4px;\n height: 28px;\n padding: 0 6px 0 4px;\n cursor: pointer;\n user-select: none;\n font-size: 11px;\n font-weight: 700;\n letter-spacing: .04em;\n text-transform: uppercase;\n color: var(--rvs-fg2);\n}\n\n.rvs-scm-head:hover {\n background: var(--rvs-hover);\n}\n\n.rvs-scm-head .rvs-count {\n margin-left: 6px;\n font-size: 11px;\n font-weight: 600;\n letter-spacing: 0;\n color: var(--rvs-fg3);\n}\n\n.rvs-scm-head-actions {\n margin-left: auto;\n display: flex;\n align-items: center;\n gap: 2px;\n}\n\n.rvs-scm-section.collapsed > *:not(.rvs-scm-head) {\n display: none !important;\n}\n\n.rvs-chevron {\n flex: none;\n transition: transform .12s ease;\n}\n\n.rvs-chevron.open {\n transform: rotate(90deg);\n}\n\n.rvs-scm-item {\n display: flex;\n align-items: center;\n gap: 6px;\n width: 100%;\n padding: 3px 8px 3px 22px;\n border: none;\n background: transparent;\n color: var(--rvs-fg);\n font: inherit;\n font-size: 12.5px;\n line-height: 19px;\n text-align: left;\n cursor: pointer;\n}\n\n.rvs-scm-item:hover {\n background: var(--rvs-hover);\n}\n\n.rvs-scm-item.selected {\n background: var(--rvs-active);\n}\n\n.rvs-scm-path {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.rvs-scm-dir {\n flex: none;\n color: var(--rvs-fg3);\n}\n\n.rvs-scm-actions {\n flex: none;\n display: none;\n align-items: center;\n gap: 1px;\n}\n\n.rvs-scm-item:hover .rvs-scm-actions {\n display: flex;\n}\n\n.rvs-commit-box {\n flex: none;\n padding: 8px 10px;\n border-bottom: 1px solid var(--rvs-border);\n}\n\n.rvs-commit-input {\n width: 100%;\n min-height: 58px;\n max-height: 180px;\n padding: 6px 8px;\n border: 1px solid var(--rvs-border2);\n border-radius: 8px;\n background: var(--rvs-input);\n color: var(--rvs-fg);\n font: inherit;\n font-size: 12.5px;\n line-height: 18px;\n resize: vertical;\n outline: none;\n}\n\n.rvs-commit-input:focus {\n border-color: var(--rvs-brand);\n}\n\n.rvs-commit-row {\n display: flex;\n gap: 6px;\n margin-top: 6px;\n}\n\n.rvs-commit-btn {\n flex: 1;\n padding: 5px 8px;\n border: none;\n border-radius: 6px;\n background: var(--rvs-accent);\n color: #fff;\n font: inherit;\n font-size: 12.5px;\n font-weight: 600;\n cursor: pointer;\n}\n\n.rvs-commit-btn:disabled {\n opacity: .45;\n cursor: default;\n}\n\n.rvs-repo-item {\n display: flex;\n align-items: center;\n gap: 6px;\n padding: 5px 10px 5px 12px;\n cursor: pointer;\n font-size: 12.5px;\n}\n\n.rvs-repo-item:hover {\n background: var(--rvs-hover);\n}\n\n.rvs-repo-item.active {\n background: var(--rvs-active);\n}\n\n.rvs-repo-name {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.rvs-repo-branch {\n flex: none;\n font-size: 11px;\n color: var(--rvs-fg3);\n}\n\n.rvs-pill {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n padding: 1px 8px;\n border-radius: 999px;\n background: var(--rvs-hover);\n font-size: 11px;\n font-weight: 600;\n color: var(--rvs-fg);\n}\n\n/* \"cached · refreshing\" badge: quieter than a pill that carries information the\n user asked for (a branch), because it is a qualifier on the list beside it. */\n.rvs-pill-cached {\n flex: none;\n font-weight: 500;\n color: var(--rvs-fg3);\n border: 1px solid var(--rvs-border2);\n background: transparent;\n}\n\n.rvs-subpanel {\n margin: 6px 8px;\n padding: 8px;\n border: 1px solid var(--rvs-border2);\n border-radius: 8px;\n font-size: 12px;\n}\n\n.rvs-subpanel-head {\n display: flex;\n align-items: center;\n gap: 6px;\n margin-bottom: 6px;\n color: var(--rvs-fg2);\n font-weight: 600;\n}\n\n.rvs-chip {\n flex: none;\n padding: 2px 8px;\n border: 1px solid var(--rvs-border2);\n border-radius: 6px;\n background: transparent;\n color: var(--rvs-fg);\n font: inherit;\n font-size: 11.5px;\n cursor: pointer;\n}\n\n.rvs-chip:hover {\n background: var(--rvs-hover);\n}\n\n.rvs-chip.primary {\n background: var(--rvs-accent);\n border-color: transparent;\n color: #fff;\n}\n\n.rvs-chip.danger:hover {\n background: var(--rvs-danger);\n border-color: transparent;\n color: #fff;\n}\n\n.rvs-input {\n flex: 1;\n min-width: 0;\n padding: 4px 8px;\n border: 1px solid var(--rvs-border2);\n border-radius: 6px;\n background: var(--rvs-input);\n color: var(--rvs-fg);\n font: inherit;\n font-size: 12px;\n outline: none;\n}\n\n.rvs-input:focus {\n border-color: var(--rvs-brand);\n}\n\n.rvs-select {\n flex: 1;\n min-width: 0;\n padding: 3px 6px;\n border: 1px solid var(--rvs-border2);\n border-radius: 6px;\n background: var(--rvs-input);\n color: var(--rvs-fg);\n font: inherit;\n font-size: 12px;\n outline: none;\n color-scheme: light dark;\n}\n\n.rvs-select option {\n background: Canvas;\n color: CanvasText;\n}\n\n/* ------------------------------------------------- conversation-view mount */\n\n/**\n * Bound the center surface to the height the host gave the slot.\n *\n * `flex: 1 1 0` is the load-bearing declaration, and BOTH halves of it matter.\n * The host's own slot rules are:\n *\n * .wSkVaW_viewArea { flex: 1; min-height: 0; overflow: hidden }\n * [data-phase=active] .viewArea { flex: 1 0 auto; min-height: auto }\n * .wSkVaW_root { height: 100%; position: relative }\n *\n * While a conversation is active the slot's flex-basis is `auto`, so the slot's\n * height is driven by its child's INTRINSIC size. A full-height editor then\n * pushed it to 5510px for a 300-line file, the outer scroller took over, and\n * the tab strip and file info bar scrolled off the top with the code.\n *\n * `flex-basis: 0` breaks that cycle: the item stops contributing its content\n * height, so the slot stays the size the host intended and `.cm-scroller`\n * becomes the only scroller. Measured in the fixture (.spike/layout-fixture.html,\n * which mirrors the slot rules above) — only the bottom row fits:\n *\n * flex: 1 1 0 + min-height: 0 -> 736px OK\n * flex: 1 1 auto (i.e. `flex: 1`) -> 5510px broken\n * height: 0 + min-height: 100% -> 5510px broken\n * max-height: 100% -> 5510px broken\n * height: 100vh -> 5510px broken\n *\n * `height: 100%` is deliberately NOT set here any more. It resolves to `auto`\n * against a slot whose height is not definite — it reads like the fix but\n * measures as a no-op, which is how this regressed once already.\n *\n * NOT `position: absolute` either — that was another failed attempt: `.viewArea`\n * is not itself positioned, so `inset: 0` escaped to the host's `.wSkVaW_root`\n * (which is `position: relative`) and the plugin's global bar floated on top of\n * the host's 对话 / 轨迹 / 远程编辑器 tab row.\n *\n * The sidebar's `.rvs-viewslot` is deliberately not covered: it has a bounding\n * parent and this bug is specific to the conversation slot's `min-height: auto`\n * phase.\n */\n.rvs-center {\n flex: 1 1 0;\n display: flex;\n flex-direction: column;\n min-height: 0;\n min-width: 0;\n overflow: hidden;\n}\n\n/* ---------------------------------------------------------------- editor */\n\n.rvs-etabs {\n flex: none;\n display: flex;\n align-items: stretch;\n gap: 2px;\n height: 32px;\n padding: 0 4px;\n overflow-x: auto;\n overflow-y: hidden;\n border-bottom: 1px solid var(--rvs-border);\n scrollbar-width: none;\n}\n\n.rvs-etabs::-webkit-scrollbar {\n height: 0;\n}\n\n.rvs-etab {\n flex: none;\n display: flex;\n align-items: center;\n gap: 5px;\n max-width: 190px;\n margin: 4px 0;\n padding: 0 4px 0 8px;\n border: none;\n border-radius: 6px;\n background: transparent;\n color: var(--rvs-fg3);\n font: inherit;\n font-size: 12px;\n cursor: pointer;\n}\n\n.rvs-etab:hover {\n background: var(--rvs-hover);\n color: var(--rvs-fg);\n}\n\n.rvs-etab.active {\n background: var(--rvs-layer3);\n color: var(--rvs-fg);\n}\n\n.rvs-etab-name {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.rvs-etab-dot {\n flex: none;\n font-size: 9px;\n color: var(--rvs-git-m);\n}\n\n.rvs-etab-close {\n flex: none;\n width: 16px;\n height: 16px;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 0;\n border: none;\n border-radius: 4px;\n background: transparent;\n color: inherit;\n cursor: pointer;\n opacity: .65;\n}\n\n.rvs-etab-close:hover {\n background: var(--rvs-hover);\n opacity: 1;\n}\n\n.rvs-einfo {\n flex: none;\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 2px 8px 2px 10px;\n border-bottom: 1px solid var(--rvs-border);\n font-size: 11px;\n line-height: 18px;\n color: var(--rvs-fg3);\n}\n\n.rvs-einfo-path {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n /* Truncate from the LEFT (keep the tail) without reordering the text:\n `plaintext` gives the content its own LTR base direction, which is what\n stops a leading \"/\" from rendering as a trailing one. */\n direction: rtl;\n unicode-bidi: plaintext;\n text-align: left;\n}\n\n.rvs-einfo-spacer {\n margin-left: auto;\n}\n\n.rvs-status.dirty { color: var(--rvs-git-m); }\n.rvs-status.saved { color: var(--rvs-git-a); }\n.rvs-status.saving { color: var(--rvs-fg2); }\n\n.rvs-body {\n flex: 1;\n min-height: 0;\n min-width: 0;\n display: flex;\n flex-direction: column;\n overflow: hidden;\n /* Anchors portalled overlays (the find widget) outside every scroller. */\n position: relative;\n background: var(--rvs-code-bg);\n color: var(--rvs-code-fg);\n}\n\n.rvs-editor-host {\n flex: 1;\n min-height: 0;\n overflow: hidden;\n background: var(--rvs-code-bg);\n}\n\n/* Keep a panel mounted but out of layout (and out of the a11y tree). Used by\n the editor host in preview mode: unmounting it destroyed the buffer and the\n undo stack, hiding it preserves both. */\n.rvs-hidden {\n display: none !important;\n}\n\n/* Find-match marks. Painted by our own StateField (`findMatchHighlight`), and\n deliberately the SAME colours as the `.cm-searchMatch` rules below so the\n two paths cannot drift apart. */\n.rvs-find-match {\n background-color: rgba(226, 183, 20, .30);\n outline: 1px solid rgba(226, 183, 20, .55);\n border-radius: 2px;\n}\n\n.rvs-find-match.current {\n background-color: rgba(226, 183, 20, .48);\n}\n\n/* ------------------------------------------------------- preview surfaces */\n\n.rvs-preview-img {\n flex: 1;\n min-height: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n overflow: auto;\n background-color: var(--rvs-code-bg);\n background-image:\n linear-gradient(45deg, rgba(127, 127, 127, .08) 25%, transparent 25%),\n linear-gradient(-45deg, rgba(127, 127, 127, .08) 25%, transparent 25%),\n linear-gradient(45deg, transparent 75%, rgba(127, 127, 127, .08) 75%),\n linear-gradient(-45deg, transparent 75%, rgba(127, 127, 127, .08) 75%);\n background-size: 20px 20px, 20px 20px, 20px 20px, 20px 20px;\n background-position: 0 0, 0 10px, 10px -10px, -10px 0;\n}\n\n.rvs-preview-img img {\n max-width: 100%;\n max-height: 100%;\n object-fit: contain;\n image-rendering: auto;\n}\n\n.rvs-markdown {\n flex: 1;\n min-height: 0;\n overflow: auto;\n padding: 12px 14px 24px;\n font-size: 13px;\n line-height: 1.7;\n color: var(--rvs-code-fg);\n background: var(--rvs-code-bg);\n user-select: text;\n}\n\n.rvs-markdown h1, .rvs-markdown h2, .rvs-markdown h3, .rvs-markdown h4 {\n margin: 14px 0 6px;\n line-height: 1.35;\n}\n\n.rvs-markdown h1 { font-size: 19px; border-bottom: 1px solid var(--rvs-border); padding-bottom: 5px; }\n.rvs-markdown h2 { font-size: 16px; border-bottom: 1px solid var(--rvs-border); padding-bottom: 4px; }\n.rvs-markdown h3 { font-size: 14px; }\n.rvs-markdown p { margin: 6px 0; }\n.rvs-markdown ul, .rvs-markdown ol { margin: 6px 0; padding-left: 22px; }\n.rvs-markdown li { margin: 2px 0; }\n.rvs-markdown code {\n padding: 1px 5px;\n border-radius: 4px;\n background: var(--rvs-hover);\n font-family: var(--rvs-mono);\n font-size: 12px;\n}\n.rvs-markdown pre {\n margin: 8px 0;\n padding: 9px 11px;\n border-radius: 8px;\n background: var(--rvs-input);\n overflow-x: auto;\n}\n.rvs-markdown pre code { padding: 0; background: transparent; }\n.rvs-markdown blockquote {\n margin: 8px 0;\n padding: 2px 12px;\n border-left: 3px solid var(--rvs-border2);\n color: var(--rvs-fg2);\n}\n.rvs-markdown a { color: var(--dsw-alias-label-link, #58a6ff); text-decoration: none; }\n.rvs-markdown a:hover { text-decoration: underline; }\n.rvs-markdown table { border-collapse: collapse; margin: 8px 0; font-size: 12px; }\n.rvs-markdown th, .rvs-markdown td { border: 1px solid var(--rvs-border2); padding: 4px 8px; }\n.rvs-markdown hr { border: none; border-top: 1px solid var(--rvs-border); margin: 12px 0; }\n\n.rvs-html-frame {\n flex: 1;\n min-height: 0;\n width: 100%;\n border: none;\n background: #fff;\n}\n\n/* ------------------------------------------------------------------ diff */\n\n.rvs-diff {\n flex: 1;\n min-height: 0;\n display: flex;\n flex-direction: column;\n overflow: hidden;\n}\n\n.rvs-diff-toolbar {\n flex: none;\n display: flex;\n align-items: center;\n gap: 6px;\n padding: 3px 8px;\n border-bottom: 1px solid var(--rvs-border);\n font-size: 11px;\n color: var(--rvs-fg3);\n}\n\n/**\n * Prev/next change, in the toolbar's free space.\n *\n * Two chevrons around a tabular counter: the pair then reads as \"jump to change\n * n of N\" instead of two anonymous arrows, and the counter doubles as the \"where\n * am I\" cue while scrolling by hand.\n */\n.rvs-hunknav {\n display: inline-flex;\n align-items: center;\n gap: 1px;\n margin-right: 2px;\n}\n\n.rvs-hunknav-pos {\n min-width: 30px;\n text-align: center;\n font-size: 11px;\n font-variant-numeric: tabular-nums;\n color: var(--rvs-fg3);\n user-select: none;\n}\n\n.rvs-seg {\n display: inline-flex;\n border: 1px solid var(--rvs-border2);\n border-radius: 6px;\n overflow: hidden;\n}\n\n.rvs-seg button {\n padding: 2px 9px;\n border: none;\n background: transparent;\n color: var(--rvs-fg2);\n font: inherit;\n font-size: 11px;\n cursor: pointer;\n}\n\n.rvs-seg button.active {\n background: var(--rvs-active);\n color: var(--rvs-fg);\n}\n\n.rvs-seg button:hover:not(.active) {\n background: var(--rvs-hover);\n}\n\n.rvs-diff-grid {\n flex: 1;\n min-height: 0;\n overflow-y: auto;\n overflow-x: hidden;\n font-family: var(--rvs-mono);\n font-size: 12px;\n line-height: 18px;\n scrollbar-width: thin;\n background: var(--rvs-code-bg);\n color: var(--rvs-code-fg);\n}\n\n/**\n * Top horizontal scrollbar.\n *\n * The two panes scroll horizontally on their own — so a long line can never\n * widen the layout — and this proxy bar drives both of them from the TOP of\n * the view, instead of forcing a scroll to the bottom of the file to reach a\n * horizontal bar. Its inner spacer is sized to the widest line, and the\n * component only renders it when a line really is wider than the grid.\n *\n * Sized and coloured to be SEEN: as a 13 px band with `scrollbar-width: thin`\n * and a faint thumb, it read as \"there is no horizontal scrollbar at all\".\n */\n.rvs-hbar {\n flex: none;\n height: 15px;\n overflow-x: auto;\n overflow-y: hidden;\n border-bottom: 1px solid var(--rvs-border);\n background: var(--rvs-hover);\n}\n\n.rvs-hbar-inner {\n height: 1px;\n}\n\n.rvs-hbar::-webkit-scrollbar {\n height: 11px;\n}\n\n.rvs-hbar::-webkit-scrollbar-thumb {\n background: var(--dsw-alias-scrollbar-bg-l2, rgba(150, 160, 180, .75));\n border-radius: 6px;\n border: 2px solid transparent;\n background-clip: padding-box;\n}\n\n.rvs-hbar::-webkit-scrollbar-thumb:hover {\n background: var(--dsw-alias-scrollbar-hover-l2, rgba(170, 180, 200, .95));\n background-clip: padding-box;\n}\n\n.rvs-hbar::-webkit-scrollbar-track {\n background: transparent;\n}\n\n.rvs-diff-cols {\n display: flex;\n align-items: stretch;\n min-width: 0;\n}\n\n/**\n * One diff pane. `flex: 1 1 0` keeps the panes EXACTLY equal however long a\n * line is (the previous row markup let a long line widen its own pane, which\n * is what broke the symmetry). Each pane keeps its own horizontal scroll, and\n * its scrollbar is now VISIBLE: the top proxy remains the convenient control,\n * but the pane's own bar is what a user reaches for first, and it sits right\n * under the content when the file is shorter than the view.\n */\n.rvs-diff-col {\n flex: 1 1 0;\n min-width: 0;\n overflow-x: auto;\n overflow-y: hidden;\n}\n\n.rvs-diff-col::-webkit-scrollbar {\n height: 9px;\n width: 0;\n}\n\n.rvs-diff-col::-webkit-scrollbar-thumb {\n background: var(--dsw-alias-scrollbar-bg-l2, rgba(150, 160, 180, .6));\n border-radius: 5px;\n}\n\n.rvs-diff-col::-webkit-scrollbar-thumb:hover {\n background: var(--dsw-alias-scrollbar-hover-l2, rgba(170, 180, 200, .85));\n}\n\n.rvs-diff-col::-webkit-scrollbar-track {\n background: transparent;\n}\n\n.rvs-dline {\n display: flex;\n align-items: flex-start;\n height: 18px;\n line-height: 18px;\n white-space: pre;\n}\n\n.rvs-dline-num {\n /* Pinned while the line scrolls sideways, so a long line never takes the\n line numbers away with it. */\n position: sticky;\n left: 0;\n z-index: 1;\n flex: none;\n width: 3.4em;\n padding-right: 6px;\n text-align: right;\n color: var(--rvs-fg3);\n opacity: .85;\n user-select: none;\n background: var(--rvs-code-bg);\n}\n\n.rvs-row-del .rvs-dline-num {\n background: var(--rvs-del-solid);\n}\n\n.rvs-row-add .rvs-dline-num {\n background: var(--rvs-add-solid);\n}\n\n.rvs-dline-text {\n flex: 1;\n padding-right: 10px;\n user-select: text;\n}\n\n.rvs-gutter-col {\n flex: none;\n width: 30px;\n border-left: 1px solid var(--rvs-border);\n border-right: 1px solid var(--rvs-border);\n user-select: none;\n}\n\n.rvs-gutter-line {\n height: 18px;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n.rvs-diff-grid::-webkit-scrollbar {\n width: 9px;\n height: 0;\n}\n\n.rvs-diff-grid::-webkit-scrollbar-thumb {\n background: var(--dsw-alias-scrollbar-bg-l2, rgba(127, 127, 127, .4));\n border-radius: 4px;\n}\n\n.rvs-num {\n flex: none;\n display: inline-block;\n width: 3.2em;\n padding-right: 6px;\n text-align: right;\n color: var(--rvs-fg3);\n opacity: .6;\n user-select: none;\n}\n\n.rvs-row-del {\n background: var(--rvs-del-bg);\n}\n\n.rvs-row-del .rvs-dline-text,\n.rvs-row-del .rvs-dline-num,\n.rvs-row-del .rvs-num {\n color: #f14c4c;\n}\n\n.rvs-row-add {\n background: var(--rvs-add-bg);\n}\n\n.rvs-row-add .rvs-dline-text,\n.rvs-row-add .rvs-dline-num,\n.rvs-row-add .rvs-num {\n color: #4ec9b0;\n}\n\n.rvs-row-empty {\n color: var(--rvs-fg3);\n opacity: .35;\n}\n\n.rvs-gutter {\n flex: none;\n width: 30px;\n display: flex;\n align-items: center;\n justify-content: center;\n border-left: 1px solid var(--rvs-border);\n border-right: 1px solid var(--rvs-border);\n user-select: none;\n}\n\n.rvs-gutter-dot {\n width: 6px;\n height: 6px;\n border-radius: 50%;\n opacity: .7;\n}\n\n.rvs-hunk-btns {\n display: flex;\n gap: 2px;\n}\n\n.rvs-hunk-btn {\n width: 13px;\n height: 15px;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 0;\n border: none;\n border-radius: 3px;\n background: transparent;\n color: var(--rvs-fg3);\n font-size: 11px;\n line-height: 1;\n cursor: pointer;\n}\n\n.rvs-hunk-btn:hover {\n background: var(--rvs-hover);\n color: var(--rvs-fg);\n}\n\n.rvs-hunk-btn.stage:hover {\n color: var(--rvs-git-a);\n}\n\n.rvs-hunk-btn.revert:hover {\n color: var(--rvs-git-d);\n}\n\n/**\n * Token colors: VS Code Dark+ / Light+, matching the CodeMirror theme in\n * `vsc/editor-theme.ts`. Keeping ONE palette for both surfaces means a file\n * does not change colors when the diff is switched to its editable side.\n */\n.rvs-t-comment { color: #6a9955; }\n.rvs-t-string { color: #ce9178; }\n.rvs-t-keyword { color: #569cd6; }\n.rvs-t-number { color: #b5cea8; }\n.rvs-t-func { color: #dcdcaa; }\n.rvs-t-type { color: #4ec9b0; }\n.rvs-t-attr { color: #9cdcfe; }\n.rvs-t-variable { color: #9cdcfe; }\n.rvs-t-operator { color: #d4d4d4; }\n.rvs-t-punct { color: #d4d4d4; }\n.rvs-t-tag { color: #569cd6; }\n.rvs-t-meta { color: #c586c0; }\n.rvs-t-insert { color: #4ec9b0; }\n.rvs-t-delete { color: #f14c4c; }\n\nbody:not([data-ds-dark-theme]) .rvs-t-comment { color: #008000; }\nbody:not([data-ds-dark-theme]) .rvs-t-string { color: #a31515; }\nbody:not([data-ds-dark-theme]) .rvs-t-keyword { color: #0000ff; }\nbody:not([data-ds-dark-theme]) .rvs-t-number { color: #098658; }\nbody:not([data-ds-dark-theme]) .rvs-t-func { color: #795e26; }\nbody:not([data-ds-dark-theme]) .rvs-t-type { color: #267f99; }\nbody:not([data-ds-dark-theme]) .rvs-t-attr,\nbody:not([data-ds-dark-theme]) .rvs-t-variable { color: #001080; }\nbody:not([data-ds-dark-theme]) .rvs-t-operator,\nbody:not([data-ds-dark-theme]) .rvs-t-punct { color: #1f1f1f; }\nbody:not([data-ds-dark-theme]) .rvs-t-meta { color: #af00db; }\n\n/**\n * CodeMirror token colors, applied through FIXED class names emitted by the\n * highlight style (`vsc/editor-palette.ts`). Fixed names mean the colors come\n * from this stylesheet — the same one that paints the rest of the UI — so a\n * code file can never go monochrome because a generated class name changed or\n * an injected style failed. Same Dark+ / Light+ palette as the diff panes\n * above, so a file keeps its colors when switching between the two views.\n */\n.rvs-ck-comment { color: #6a9955; }\n.rvs-ck-keyword { color: #569cd6; }\n.rvs-ck-control { color: #c586c0; }\n.rvs-ck-string { color: #ce9178; }\n.rvs-ck-regexp { color: #d16969; }\n.rvs-ck-number { color: #b5cea8; }\n.rvs-ck-constant { color: #569cd6; }\n.rvs-ck-func { color: #dcdcaa; }\n.rvs-ck-type { color: #4ec9b0; }\n.rvs-ck-variable { color: #9cdcfe; }\n.rvs-ck-attr { color: #9cdcfe; }\n.rvs-ck-meta { color: #c586c0; }\n.rvs-ck-punct { color: #d4d4d4; }\n.rvs-ck-op { color: #d4d4d4; }\n.rvs-ck-heading { color: #569cd6; font-weight: 700; }\n.rvs-ck-em { font-style: italic; }\n.rvs-ck-link { color: #9cdcfe; text-decoration: underline; }\n.rvs-ck-strike { text-decoration: line-through; }\n.rvs-ck-invalid { color: #f44747; }\n\nbody:not([data-ds-dark-theme]) .rvs-ck-comment { color: #008000; }\nbody:not([data-ds-dark-theme]) .rvs-ck-keyword { color: #0000ff; }\nbody:not([data-ds-dark-theme]) .rvs-ck-control { color: #af00db; }\nbody:not([data-ds-dark-theme]) .rvs-ck-string { color: #a31515; }\nbody:not([data-ds-dark-theme]) .rvs-ck-regexp { color: #811f3f; }\nbody:not([data-ds-dark-theme]) .rvs-ck-number { color: #098658; }\nbody:not([data-ds-dark-theme]) .rvs-ck-constant { color: #0070c1; }\nbody:not([data-ds-dark-theme]) .rvs-ck-func { color: #795e26; }\nbody:not([data-ds-dark-theme]) .rvs-ck-type { color: #267f99; }\nbody:not([data-ds-dark-theme]) .rvs-ck-variable { color: #001080; }\nbody:not([data-ds-dark-theme]) .rvs-ck-attr { color: #e50000; }\nbody:not([data-ds-dark-theme]) .rvs-ck-meta { color: #0000ff; }\nbody:not([data-ds-dark-theme]) .rvs-ck-punct,\nbody:not([data-ds-dark-theme]) .rvs-ck-op { color: #1f1f1f; }\nbody:not([data-ds-dark-theme]) .rvs-ck-heading { color: #0000ff; }\nbody:not([data-ds-dark-theme]) .rvs-ck-link { color: #001080; }\nbody:not([data-ds-dark-theme]) .rvs-ck-invalid { color: #cd3131; }\n\n/**\n * Opaque editor surface, also as plain CSS: the CodeMirror theme paints it too\n * (following the scheme through a Compartment), but a transparent editor is\n * the one failure a user notices immediately, so the container states it as\n * well.\n */\n.rvs-edge .cm-editor,\n.rvs-edge .cm-scroller,\n.rvs-edge .cm-gutters {\n background-color: var(--rvs-code-bg);\n}\n\n.rvs-edge .cm-editor {\n color: var(--rvs-code-fg);\n}\n\n.rvs-edge .cm-gutters {\n color: var(--rvs-code-gutter-fg, var(--rvs-fg3));\n border-right: 1px solid var(--rvs-border);\n}\n\n.rvs-edge .cm-activeLine {\n background-color: rgba(127, 127, 127, .08);\n}\n\n.rvs-edge .cm-activeLineGutter {\n background-color: rgba(127, 127, 127, .12);\n}\n\n/**\n * The code editor's own scrollbars.\n *\n * These had NO rule at all: CodeMirror's scroller is `.cm-scroller`, which is\n * none of `.rvs-scroll` / `.rvs-diff-grid` / `.rvs-hbar`, so the editor fell\n * back to the browser default — too thin to hit with a mouse.\n *\n * Both notations are needed and must stay in step: Chromium honours\n * `scrollbar-width` over `::-webkit-scrollbar` when both apply, so setting\n * only the webkit half would silently have no effect. Keep the numbers equal.\n */\n.rvs-edge .cm-scroller {\n scrollbar-width: auto;\n}\n\n.rvs-edge .cm-scroller::-webkit-scrollbar {\n width: 14px;\n height: 14px;\n}\n\n.rvs-edge .cm-scroller::-webkit-scrollbar-track {\n background: transparent;\n}\n\n.rvs-edge .cm-scroller::-webkit-scrollbar-thumb {\n /* Opaque, not a translucent token: a thin wash over code is exactly the\n low-contrast target that is hard to grab. */\n background: rgba(121, 121, 121, .8);\n border: 3px solid transparent;\n border-radius: 7px;\n background-clip: padding-box;\n min-height: 36px;\n}\n\n.rvs-edge .cm-scroller::-webkit-scrollbar-thumb:hover {\n background: rgba(150, 150, 150, .95);\n border: 3px solid transparent;\n background-clip: padding-box;\n}\n\n.rvs-edge .cm-scroller::-webkit-scrollbar-corner {\n background: transparent;\n}\n\n/* ------------------------------------------------------------ find widget */\n\n/**\n * \"You landed here\": the transient highlight on the line a jump landed on\n * (search hit, @-reference).\n *\n * Deliberately NOT a warm/amber wash: over the dark editor that muddies into a\n * brown-black band that reads as a shadow and swallows annotation colors. This\n * is the accent blue instead — a tinted band with a bright edge on the left and\n * hairlines top/bottom, so it reads as a marked region while every token keeps\n * its own colour and contrast.\n */\n.rvs-reveal-line {\n background-color: rgba(88, 166, 255, .22);\n box-shadow:\n inset 3px 0 0 0 #58a6ff,\n inset 0 1px 0 0 rgba(88, 166, 255, .38),\n inset 0 -1px 0 0 rgba(88, 166, 255, .38);\n}\n\nbody:not([data-ds-dark-theme]) .rvs-reveal-line {\n background-color: rgba(77, 107, 254, .14);\n box-shadow:\n inset 3px 0 0 0 #4d6bfe,\n inset 0 1px 0 0 rgba(77, 107, 254, .35),\n inset 0 -1px 0 0 rgba(77, 107, 254, .35);\n}\n\n/* --------------------------------------------------------- session changes */\n\n/**\n * 「本会话改了什么」:代码行背景 + 左侧改动栏(Cursor 风格)。\n *\n * 三种形态必须**一眼分得开**,光靠绿/红两个色相不够(色弱用户看新增行和删除行\n * 会糊成同一片),所以每种的左侧色条宽度/花样的语义也不同:\n * - 新增:实心绿条;\n * - 修改:实心蓝条(不是绿:修改块里既有留下的行也有新写的行,用\"中性的已变更\"\n * 色不会误导读者以为整块都是新内容);\n * - 删除:**斜纹**红底 + 更粗的红条 —— 这一行其实已经不存在了,只在中止位置上\n * 留个锚点,斜纹是这一层\"虚\"的信息里最不容易被当成真内容的花样。\n *\n * 选择器带上 `.cm-line`:`.cm-activeLine` 由 CodeMirror 的主题注入(运行时\n * `<style>`,通常排在本文档之后),只写 `.rvs-change-*` 两者同权重时会输给它,\n * 光标所在的那一行改动就会消失。\n */\n.rvs-edge .cm-line.rvs-change-add {\n background-color: rgba(46, 160, 67, .17);\n box-shadow:\n inset 3px 0 0 0 #2ea043,\n inset 0 -1px 0 0 rgba(46, 160, 67, .28);\n}\n\n.rvs-edge .cm-line.rvs-change-mod {\n background-color: rgba(56, 139, 253, .15);\n box-shadow:\n inset 3px 0 0 0 #388bfd,\n inset 0 -1px 0 0 rgba(56, 139, 253, .28);\n}\n\n.rvs-edge .cm-line.rvs-change-del {\n background-color: rgba(248, 81, 73, .09);\n background-image: repeating-linear-gradient(135deg, rgba(248, 81, 73, .17) 0 6px, transparent 6px 12px);\n box-shadow: inset 4px 0 0 0 #f85149;\n}\n\n/* 改动栏本身:符号 + 一列与行背景同色的窄条,读起来才是一条\"改动边\"。 */\n.rvs-edge .cm-gutterElement.rvs-change-gutter-add {\n box-shadow: inset 2px 0 0 0 #2ea043;\n}\n\n.rvs-edge .cm-gutterElement.rvs-change-gutter-mod {\n box-shadow: inset 2px 0 0 0 #388bfd;\n}\n\n.rvs-edge .cm-gutterElement.rvs-change-gutter-del {\n box-shadow: inset 2px 0 0 0 #f85149;\n}\n\n.rvs-edge .rvs-change-mark {\n display: inline-block;\n width: 12px;\n font-family: var(--rvs-mono);\n font-size: 12px;\n line-height: 1;\n text-align: center;\n user-select: none;\n}\n\n.rvs-edge .rvs-change-mark-add { color: #2ea043; }\n.rvs-edge .rvs-change-mark-mod { color: #388bfd; }\n.rvs-edge .rvs-change-mark-del { color: #f85149; }\n\n/**\n * 行内(字符级)差异:上面那三种底色回答\"哪几行变了\",这两条回答\"那一行的哪几个\n * 字变了\"。只改一个词的编辑,整行同色时完全看不出改了什么。\n *\n * `rvs-change-word-add` 刻意**只给背景、不给 `color`**:代码有自己的语法着色,\n * 覆盖前景色会把关键字/字符串/注释染成同一种颜色,比不标还难读。透明度压在行\n * 底色之上一点点,叠起来仍读得出\"整块改动\"的边界,又能看出块内哪几个字是新的。\n */\n.rvs-edge .rvs-change-word-add {\n background-color: rgba(46, 160, 67, .22);\n border-radius: 2px;\n}\n\n/**\n * 被删掉、在新文件里已经不存在的内容:零宽 widget,红底 + 删除线。\n *\n * 删除线画在 chip 自己身上(chip 是独立元素,不会波及周围代码);`white-space:\n * pre` 保留被删文本自己的缩进 —— 截断/去空白都会让 chip 谎报\"删了什么\"。\n * 字号略小 + 降一点不透明度,是让它读起来像\"残留物\"而不是一行真代码。\n */\n.rvs-edge .rvs-change-word-del {\n padding: 0 3px;\n margin: 0 1px;\n border-radius: 3px;\n background-color: rgba(248, 81, 73, .18);\n font-size: .92em;\n text-decoration: line-through;\n opacity: .85;\n white-space: pre;\n}\n\n/**\n * 撑宽度的隐形符号:只用它占位,绝不显示(见 `change-marks.ts` 的 SpacerMarker,\n * CodeMirror 会给这个 spacer 元素加 `visibility: hidden`,这里不重复声明)。\n */\n.rvs-edge .rvs-change-spacer {\n opacity: 0;\n}\n\n/* 浅色主题:同样的色相,压暗 + 提高不透明度,在近白底上才看得清。 */\nbody:not([data-ds-dark-theme]) .rvs-edge .cm-line.rvs-change-add {\n background-color: rgba(31, 136, 61, .13);\n box-shadow:\n inset 3px 0 0 0 #1f883d,\n inset 0 -1px 0 0 rgba(31, 136, 61, .22);\n}\n\nbody:not([data-ds-dark-theme]) .rvs-edge .cm-line.rvs-change-mod {\n background-color: rgba(9, 105, 218, .12);\n box-shadow:\n inset 3px 0 0 0 #0969da,\n inset 0 -1px 0 0 rgba(9, 105, 218, .22);\n}\n\nbody:not([data-ds-dark-theme]) .rvs-edge .cm-line.rvs-change-del {\n background-color: rgba(207, 34, 46, .07);\n background-image: repeating-linear-gradient(135deg, rgba(207, 34, 46, .14) 0 6px, transparent 6px 12px);\n box-shadow: inset 4px 0 0 0 #cf222e;\n}\n\nbody:not([data-ds-dark-theme]) .rvs-edge .cm-gutterElement.rvs-change-gutter-add {\n box-shadow: inset 2px 0 0 0 #1f883d;\n}\n\nbody:not([data-ds-dark-theme]) .rvs-edge .cm-gutterElement.rvs-change-gutter-mod {\n box-shadow: inset 2px 0 0 0 #0969da;\n}\n\nbody:not([data-ds-dark-theme]) .rvs-edge .cm-gutterElement.rvs-change-gutter-del {\n box-shadow: inset 2px 0 0 0 #cf222e;\n}\n\nbody:not([data-ds-dark-theme]) .rvs-edge .rvs-change-mark-add { color: #1f883d; }\nbody:not([data-ds-dark-theme]) .rvs-edge .rvs-change-mark-mod { color: #0969da; }\nbody:not([data-ds-dark-theme]) .rvs-edge .rvs-change-mark-del { color: #cf222e; }\n\n/* 浅色主题的行内差异:同样的色相压暗,在近白底上才看得清(暗色那套会糊成一片)。 */\nbody:not([data-ds-dark-theme]) .rvs-edge .rvs-change-word-add {\n background-color: rgba(26, 127, 55, .18);\n}\n\nbody:not([data-ds-dark-theme]) .rvs-edge .rvs-change-word-del {\n background-color: rgba(207, 34, 46, .14);\n}\n\n/**\n * 改动跳转按钮:浮在编辑器右上角。\n *\n * 放在编辑器上而不是面板里 —— 文件打开后光标就在这里,而面板可能被收起、被滚走。\n * 容器 `.rvs-body` 已经是 `position: relative`(它本来用于挂 find 浮层),所以这里\n * 只做绝对定位,不引入新的定位上下文。`pointer-events` 只开在按钮上:这块区域压在\n * 编辑器文字上方,整块可点会把右上角那几行变成死区。\n */\n.rvs-change-nav {\n position: absolute;\n top: 8px;\n right: 14px;\n z-index: 5;\n display: flex;\n align-items: center;\n gap: 2px;\n padding: 3px 4px;\n border: 1px solid var(--rvs-border2);\n border-radius: 8px;\n background: Canvas;\n box-shadow: 0 2px 8px rgba(0, 0, 0, .28);\n pointer-events: none;\n}\n\n/* The counter is what makes this read as \"3 of 7 changes\" rather than two\n anonymous arrows — and it keeps the buttons from looking like decoration. */\n.rvs-change-nav-count {\n padding: 0 6px 0 4px;\n color: var(--rvs-fg2);\n font-size: 11px;\n font-variant-numeric: tabular-nums;\n line-height: 22px;\n white-space: nowrap;\n}\n\n.rvs-change-nav-btn {\n width: 26px;\n height: 22px;\n padding: 0;\n border: 1px solid transparent;\n border-radius: 5px;\n background: transparent;\n color: var(--rvs-fg);\n font-size: 13px;\n font-weight: 600;\n line-height: 20px;\n text-align: center;\n cursor: pointer;\n pointer-events: auto;\n}\n\n.rvs-change-nav-btn:hover {\n border-color: var(--rvs-border2);\n background: var(--rvs-hover);\n color: var(--rvs-brand);\n}\n\n.rvs-change-nav-btn:active { background: var(--rvs-active); }\n\n/**\n * 悬浮在改动行上出现的「撤销这处改动」按钮。\n *\n * 它**不在编辑器的 DOM 里**:`undo-affordance.ts` 把它当 `position: fixed` 的元素挂到\n * `document.body`(和「添加到对话」那个浮层同一套做法),所以这里的选择器不带\n * `.rvs-edge` —— 带上了就一条都不生效。固定定位 + 高 z-index 是刻意的:宿主容器有\n * `overflow: hidden`,挂在里面会被 CodeMirror 的滚动层裁掉。\n *\n * 颜色按\"危险动作\"给:默认红边红字、悬停实心红。底色不透明(不用半透明的 overlay\n * token),否则后面的代码文字会透出来。\n */\n.rvs-undo-affordance {\n position: fixed;\n z-index: 2100;\n display: inline-flex;\n align-items: center;\n gap: 4px;\n max-width: 220px;\n padding: 3px 9px;\n border: 1px solid #f85149;\n border-radius: 6px;\n background-color: #1e2229;\n color: #ffb4ae;\n /* 按钮挂在 body 上,没有 .rvs-edge 的字体上下文:跟随宿主页面字体。 */\n font-family: inherit;\n font-size: 12px;\n line-height: 1.5;\n white-space: nowrap;\n cursor: pointer;\n box-shadow: 0 6px 18px rgba(0, 0, 0, .45);\n /* 纵向以悬停行的中线对齐(横向坐标由 JS 按行尾算好)。 */\n transform: translateY(-50%);\n}\n\n.rvs-undo-affordance:hover {\n background-color: #f85149;\n border-color: #f85149;\n color: #fff;\n}\n\n.rvs-undo-affordance:active {\n transform: translateY(-50%) scale(.97);\n}\n\n.rvs-undo-affordance:focus-visible {\n outline: 2px solid #58a6ff;\n outline-offset: 1px;\n}\n\n/* 删除锚点:这一行其实已经不在了,虚线边框把它和\"真实存在的行\"分开。 */\n.rvs-undo-affordance[data-kind=\"del\"] {\n border-style: dashed;\n}\n\nbody:not([data-ds-dark-theme]) .rvs-undo-affordance {\n border-color: #cf222e;\n background-color: #ffffff;\n color: #a40e26;\n box-shadow: 0 6px 18px rgba(31, 35, 40, .18);\n}\n\nbody:not([data-ds-dark-theme]) .rvs-undo-affordance:hover {\n background-color: #cf222e;\n border-color: #cf222e;\n color: #fff;\n}\n\nbody:not([data-ds-dark-theme]) .rvs-undo-affordance:focus-visible {\n outline-color: #4d6bfe;\n}\n\n/**\n * CodeMirror's own search decorations, also stated here: the editor's theme is\n * injected by the library, and these must stay readable even when only this\n * stylesheet is in play.\n */\n.rvs-edge .cm-searchMatch {\n background-color: rgba(226, 183, 20, .30);\n outline: 1px solid rgba(226, 183, 20, .55);\n}\n\n.rvs-edge .cm-searchMatch-selected {\n background-color: rgba(226, 183, 20, .48);\n}\n\n.rvs-edge .cm-selectionBackground,\n.rvs-edge .cm-content ::selection {\n background-color: rgba(58, 110, 165, .55) !important;\n}\n\n/** Build stamp in the editor info bar: identifies the loaded client bundle. */\n.rvs-build {\n flex: none;\n padding: 0 5px;\n border: 1px solid var(--rvs-border);\n border-radius: 4px;\n color: var(--rvs-fg3);\n font-size: 10px;\n font-family: var(--rvs-mono);\n opacity: .85;\n user-select: text;\n}\n\n.rvs-find {\n position: absolute;\n top: 6px;\n right: 18px;\n z-index: 30;\n display: flex;\n align-items: center;\n gap: 3px;\n padding: 3px 4px 3px 7px;\n border: 1px solid var(--rvs-border2);\n border-radius: 8px;\n background-color: Canvas;\n background-image: linear-gradient(var(--dsw-alias-bg-overlay, transparent), var(--dsw-alias-bg-overlay, transparent));\n color: var(--rvs-fg);\n box-shadow: 0 4px 14px rgba(0, 0, 0, .28);\n font-size: 12px;\n}\n\n.rvs-find-input {\n width: 170px;\n padding: 2px 6px;\n border: 1px solid var(--rvs-border2);\n border-radius: 5px;\n background: var(--rvs-input);\n color: var(--rvs-fg);\n font: inherit;\n font-size: 12px;\n outline: none;\n}\n\n.rvs-find-input:focus {\n border-color: var(--rvs-brand);\n}\n\n.rvs-find-count {\n flex: none;\n min-width: 54px;\n text-align: center;\n color: var(--rvs-fg3);\n font-size: 11px;\n white-space: nowrap;\n}\n\n.rvs-find-count.none {\n color: var(--rvs-git-d);\n}\n\n.rvs-code {\n flex: 1;\n min-height: 0;\n overflow: auto;\n margin: 0;\n padding: 6px 0 16px;\n font-family: var(--rvs-mono);\n font-size: 12.5px;\n line-height: 18px;\n white-space: pre;\n scrollbar-width: thin;\n background: var(--rvs-code-bg);\n color: var(--rvs-code-fg);\n}\n\n.rvs-code-line {\n display: flex;\n align-items: flex-start;\n min-width: max-content;\n}\n\n.rvs-code-line:hover {\n background: var(--rvs-hover);\n}\n\n.rvs-code-text {\n flex: 1;\n padding-right: 10px;\n white-space: pre-wrap;\n word-break: break-word;\n user-select: text;\n}\n\n/* ---------------------------------------------------------- history list */\n\n.rvs-commit-item {\n display: flex;\n align-items: center;\n gap: 6px;\n width: 100%;\n padding: 4px 8px 4px 4px;\n border: none;\n background: transparent;\n color: var(--rvs-fg);\n font: inherit;\n font-size: 12.5px;\n line-height: 18px;\n text-align: left;\n cursor: pointer;\n}\n\n.rvs-commit-item:hover {\n background: var(--rvs-hover);\n}\n\n.rvs-commit-item.selected {\n background: var(--rvs-active);\n}\n\n.rvs-graph-svg {\n flex: none;\n display: block;\n}\n\n.rvs-commit-hash {\n flex: none;\n font-family: var(--rvs-mono);\n font-size: 11px;\n color: var(--rvs-fg3);\n}\n\n.rvs-commit-msg {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.rvs-commit-date {\n flex: none;\n font-size: 11px;\n color: var(--rvs-fg3);\n}\n\n.rvs-ref-badge {\n flex: none;\n padding: 0 5px;\n border-radius: 8px;\n border: 1px solid var(--rvs-border2);\n font-size: 10px;\n line-height: 15px;\n color: var(--rvs-fg2);\n}\n\n.rvs-ref-badge.head {\n border-color: var(--rvs-brand);\n background: rgba(77, 107, 254, .2);\n color: var(--rvs-fg);\n}\n\n.rvs-ref-badge.tag {\n border-color: rgba(226, 183, 20, .55);\n background: rgba(226, 183, 20, .16);\n color: var(--rvs-git-m);\n}\n\n.rvs-detail {\n margin: 0 6px 8px;\n padding: 8px 10px;\n border: 1px solid var(--rvs-border2);\n border-left: 3px solid var(--rvs-accent);\n border-radius: 8px;\n background: var(--rvs-layer3);\n font-size: 12px;\n}\n\n.rvs-detail-meta {\n color: var(--rvs-fg3);\n font-size: 11px;\n margin-bottom: 6px;\n white-space: pre-wrap;\n}\n\n.rvs-file-row {\n display: flex;\n align-items: center;\n gap: 6px;\n padding: 2px 4px;\n border-radius: 5px;\n cursor: pointer;\n}\n\n.rvs-file-row:hover {\n background: var(--rvs-hover);\n}\n\n.rvs-file-path {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n /* Same tail-first truncation as the info bar; `plaintext` keeps the path's\n own LTR order (see .rvs-einfo-path). */\n direction: rtl;\n unicode-bidi: plaintext;\n text-align: left;\n}\n\n.rvs-file-status {\n flex: none;\n width: 16px;\n text-align: center;\n font-weight: 700;\n font-size: 11px;\n}\n\n/* ---------------------------------------------------- floating editor window */\n\n/**\n * The floating editor lives in the host's frame-wide overlay layer\n * (`position: absolute; inset: 0; z-index: 20`, click-through, children opt\n * back into pointer events). It is placed with viewport coordinates, so it\n * floats above every column without joining any of their stacking contexts.\n *\n * `.rvs-root` carries the whole design-system token set and is row-flex by\n * default; this frame overrides the direction to a column and takes its size\n * from the inline rect the component keeps.\n */\n.rvs-float-window {\n position: fixed;\n flex-direction: column;\n /* The rect maths (clamp/persist) is in border-box terms; without this the\n painted border would sit one pixel outside the computed rectangle. */\n box-sizing: border-box;\n min-width: 360px;\n min-height: 220px;\n overflow: hidden;\n border: 1px solid var(--rvs-border2);\n border-radius: 10px;\n background: var(--rvs-code-bg);\n color: var(--rvs-code-fg);\n box-shadow: 0 18px 60px rgba(0, 0, 0, .45), 0 2px 8px rgba(0, 0, 0, .28);\n z-index: 30;\n /* The overlay layer itself is click-through; the window opts back in even if\n the host wraps each overlay entry in its own element. */\n pointer-events: auto;\n}\n\n/* Minimized: the frame IS the ball (see `.rvs-float-mini`); the body stays\n mounted but hidden, and it must not impose a minimum height. */\n.rvs-float-window[data-rvs-float=\"collapsed\"] {\n min-height: 0;\n}\n\n/**\n * Minimized window = floating ball.\n *\n * A full-width 34px bar was too long: it read as a broken window and covered a\n * whole strip. The ball is a small circle at the window's own corner, draggable\n * anywhere, restored by a click (or Enter/Space). It keeps the dirty dot, so\n * \"the editor is out and has unsaved edits\" is still one glance away.\n */\n.rvs-float-window.rvs-float-mini {\n min-width: 0;\n min-height: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n border-radius: 50%;\n cursor: move;\n touch-action: none;\n}\n\n.rvs-float-ball {\n flex: 1;\n /* Stretch to the circle: the frame is a COLUMN flex box with centred items,\n so without this the hit area (and the absolutely positioned dot) would be\n sized by the 16px glyph instead of the 44px ball. */\n width: 100%;\n height: 100%;\n box-sizing: border-box;\n display: flex;\n align-items: center;\n justify-content: center;\n position: relative;\n border-radius: 50%;\n color: var(--rvs-brand);\n font-size: 16px;\n line-height: 1;\n cursor: move;\n outline: none;\n}\n\n.rvs-float-ball:hover,\n.rvs-float-ball:focus-visible {\n background: var(--rvs-hover);\n}\n\n.rvs-float-ball-glyph {\n pointer-events: none;\n}\n\n/* Unsaved edits stay visible while minimized. */\n.rvs-float-ball-dot {\n position: absolute;\n top: 8px;\n right: 8px;\n width: 8px;\n height: 8px;\n border-radius: 50%;\n background: var(--rvs-git-m);\n box-shadow: 0 0 0 2px var(--rvs-code-bg);\n pointer-events: none;\n}\n\n/* While a drag/resize gesture runs the pointer must not select editor text. */\n.rvs-float-window.dragging {\n user-select: none;\n}\n\n.rvs-float-titlebar {\n flex: none;\n display: flex;\n align-items: center;\n gap: 7px;\n height: 34px;\n padding: 0 6px 0 10px;\n border-bottom: 1px solid var(--rvs-border);\n background: var(--rvs-layer3);\n cursor: move;\n user-select: none;\n touch-action: none;\n}\n\n.rvs-float-glyph {\n color: var(--rvs-brand);\n font-size: 11px;\n}\n\n.rvs-float-title {\n flex: none;\n font-size: 12px;\n font-weight: 600;\n color: var(--rvs-fg2);\n}\n\n.rvs-float-file {\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n font-size: 11.5px;\n color: var(--rvs-fg3);\n direction: rtl;\n unicode-bidi: plaintext;\n text-align: left;\n}\n\n.rvs-float-spacer {\n flex: 1;\n}\n\n.rvs-float-btn {\n flex: none;\n width: 22px;\n height: 22px;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 0;\n border: none;\n border-radius: 5px;\n background: transparent;\n color: var(--rvs-fg3);\n font: inherit;\n font-size: 12px;\n line-height: 1;\n cursor: pointer;\n}\n\n.rvs-float-btn:hover {\n background: var(--rvs-hover);\n color: var(--rvs-fg);\n}\n\n.rvs-float-close:hover {\n background: rgba(241, 76, 76, .18);\n color: var(--rvs-danger);\n}\n\n.rvs-float-body {\n flex: 1 1 0;\n min-height: 0;\n min-width: 0;\n display: flex;\n overflow: hidden;\n background: var(--rvs-code-bg);\n}\n\n/* Keep the height chain pinned: the editor pane grows inside this box. */\n.rvs-float-body > .rvs-pane {\n flex: 1 1 0;\n min-height: 0;\n min-width: 0;\n}\n\n/* Eight grips, placed INSIDE the border box: the window clips overflow (rounded\n corners), so grips hanging outside would lose half their hit area. Corners\n are larger so they can actually be hit. */\n.rvs-float-resize {\n position: absolute;\n z-index: 3;\n}\n\n.rvs-float-resize-n { top: 0; left: 10px; right: 10px; height: 6px; cursor: ns-resize; }\n.rvs-float-resize-s { bottom: 0; left: 10px; right: 10px; height: 6px; cursor: ns-resize; }\n.rvs-float-resize-e { top: 10px; bottom: 10px; right: 0; width: 6px; cursor: ew-resize; }\n.rvs-float-resize-w { top: 10px; bottom: 10px; left: 0; width: 6px; cursor: ew-resize; }\n.rvs-float-resize-ne { top: 0; right: 0; width: 12px; height: 12px; cursor: nesw-resize; }\n.rvs-float-resize-nw { top: 0; left: 0; width: 12px; height: 12px; cursor: nwse-resize; }\n.rvs-float-resize-se { bottom: 0; right: 0; width: 12px; height: 12px; cursor: nwse-resize; }\n.rvs-float-resize-sw { bottom: 0; left: 0; width: 12px; height: 12px; cursor: nesw-resize; }\n\n/* Shown by whichever surface the single editor left behind (centre tab or the\n sidebar's editor panel). */\n.rvs-float-placeholder {\n flex: 1;\n min-height: 0;\n height: 100%;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 10px;\n padding: 24px;\n text-align: center;\n color: var(--rvs-fg3);\n font-size: 12.5px;\n}\n\n.rvs-float-placeholder-title {\n color: var(--rvs-fg2);\n font-size: 13px;\n font-weight: 600;\n}\n\n.rvs-float-placeholder-text {\n max-width: 360px;\n line-height: 1.6;\n}\n\n/* --------------------------------------------------------- context menu */\n\n.rvs-portal {\n font-family: inherit;\n}\n\n/**\n * Floating surfaces (menu / modal / toast) paint an OPAQUE base first and then\n * lay the skin's overlay token on top. The token is frequently translucent, so\n * using it alone let the panel's own text bleed through the menu and overlap\n * the entries; `Canvas` follows the resolved light/dark scheme, so the base is\n * correct in either theme.\n */\n.rvs-menu,\n.rvs-modal-box,\n.rvs-toast {\n background-color: Canvas;\n background-image: linear-gradient(var(--dsw-alias-bg-overlay, transparent), var(--dsw-alias-bg-overlay, transparent));\n color: var(--rvs-fg);\n}\n\n.rvs-menu {\n position: fixed;\n z-index: 1000;\n min-width: 172px;\n max-height: calc(100vh - 16px);\n overflow-y: auto;\n padding: 4px;\n border: 1px solid var(--rvs-border2);\n border-radius: 8px;\n box-shadow: 0 6px 20px rgba(0, 0, 0, .35);\n font-size: 12.5px;\n line-height: 18px;\n}\n\n.rvs-menu-sep {\n height: 1px;\n margin: 4px 6px;\n background: var(--rvs-border);\n}\n\n.rvs-menu-item {\n display: flex;\n align-items: center;\n gap: 8px;\n width: 100%;\n padding: 5px 10px;\n border: none;\n border-radius: 6px;\n background: transparent;\n color: inherit;\n font: inherit;\n text-align: left;\n cursor: pointer;\n}\n\n.rvs-menu-item:hover {\n background: var(--rvs-hover);\n}\n\n.rvs-menu-item.danger {\n color: var(--vsc-danger, #f14c4c);\n}\n\n.rvs-menu-item.danger:hover {\n background: rgba(241, 76, 76, .16);\n}\n\n.rvs-menu-item:disabled {\n opacity: .45;\n cursor: default;\n}\n\n.rvs-menu-item:disabled:hover {\n background: transparent;\n}\n\n.rvs-menu-hint {\n margin-left: auto;\n font-size: 11px;\n opacity: .6;\n}\n\n/* ------------------------------------------------------- modal + toast */\n\n.rvs-modal-mask {\n position: fixed;\n inset: 0;\n z-index: 100000;\n display: flex;\n align-items: center;\n justify-content: center;\n background: rgba(0, 0, 0, .45);\n backdrop-filter: blur(2px);\n animation: rvsFade .12s ease;\n}\n\n.rvs-modal-box {\n width: min(420px, calc(100vw - 40px));\n padding: 16px 18px;\n border: 1px solid var(--rvs-border2);\n border-radius: 10px;\n color: var(--rvs-fg);\n box-shadow: 0 12px 40px rgba(0, 0, 0, .38);\n font-size: 13px;\n animation: rvsPop .14s ease;\n}\n\n.rvs-modal-title {\n margin-bottom: 8px;\n font-size: 14px;\n font-weight: 600;\n}\n\n.rvs-modal-text {\n margin-bottom: 12px;\n color: var(--rvs-fg2);\n font-size: 12.5px;\n line-height: 1.6;\n word-break: break-word;\n white-space: pre-wrap;\n}\n\n.rvs-modal-row {\n display: flex;\n justify-content: flex-end;\n gap: 8px;\n margin-top: 14px;\n}\n\n.rvs-modal-btn {\n padding: 5px 14px;\n border: 1px solid var(--rvs-border2);\n border-radius: 7px;\n background: transparent;\n color: var(--rvs-fg);\n font: inherit;\n font-size: 12.5px;\n cursor: pointer;\n}\n\n.rvs-modal-btn:hover {\n background: var(--rvs-hover);\n}\n\n.rvs-modal-btn.primary {\n border-color: transparent;\n background: var(--rvs-accent);\n color: #fff;\n}\n\n.rvs-modal-btn.danger {\n border-color: transparent;\n background: var(--rvs-danger);\n color: #fff;\n}\n\n.rvs-toasts {\n position: fixed;\n right: 16px;\n bottom: 18px;\n z-index: 100001;\n display: flex;\n flex-direction: column;\n gap: 8px;\n align-items: flex-end;\n pointer-events: none;\n}\n\n.rvs-toast {\n max-width: 340px;\n padding: 7px 12px;\n border: 1px solid var(--rvs-border2);\n border-radius: 8px;\n color: var(--rvs-fg);\n box-shadow: 0 6px 20px rgba(0, 0, 0, .35);\n font-size: 12.5px;\n line-height: 1.5;\n word-break: break-word;\n white-space: pre-wrap;\n animation: rvsPop .14s ease;\n}\n\n.rvs-toast.error {\n border-color: rgba(241, 76, 76, .6);\n background-image: linear-gradient(rgba(58, 31, 34, .92), rgba(58, 31, 34, .92));\n}\n\n/* ------------------------------------------------------------- terminal */\n\n.rvs-term-tabs {\n flex: none;\n display: flex;\n align-items: stretch;\n gap: 2px;\n height: 32px;\n padding: 0 4px;\n overflow-x: auto;\n overflow-y: hidden;\n border-bottom: 1px solid var(--rvs-border);\n scrollbar-width: none;\n}\n\n.rvs-term-tabs::-webkit-scrollbar {\n height: 0;\n}\n\n.rvs-term-tab {\n flex: none;\n display: flex;\n align-items: center;\n gap: 6px;\n max-width: 180px;\n margin: 4px 0;\n padding: 0 6px 0 9px;\n border: none;\n border-radius: 6px;\n background: transparent;\n color: var(--rvs-fg3);\n font: inherit;\n font-size: 12px;\n cursor: pointer;\n}\n\n/* Wrapper that pairs a tab with its close button. The two are siblings so the\n ✕ can be a real focusable <button> (a nested one is invalid HTML and left\n keyboard users no way to close a tab). */\n.rvs-term-tabwrap {\n position: relative;\n flex: none;\n display: flex;\n align-items: center;\n}\n\n.rvs-term-tabwrap .rvs-term-tab {\n padding-right: 22px;\n}\n\n.rvs-term-tabwrap .rvs-etab-close {\n position: absolute;\n right: 3px;\n opacity: 0;\n}\n\n.rvs-term-tabwrap:hover .rvs-etab-close,\n.rvs-term-tabwrap:focus-within .rvs-etab-close {\n opacity: .8;\n}\n\n.rvs-term-tab:hover {\n background: var(--rvs-hover);\n color: var(--rvs-fg);\n}\n\n.rvs-term-tab.active {\n background: var(--rvs-layer3);\n color: var(--rvs-fg);\n}\n\n.rvs-term-tab.dead {\n color: var(--rvs-git-d);\n}\n\n.rvs-term-name {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.rvs-term-body {\n flex: 1;\n min-height: 0;\n position: relative;\n overflow: hidden;\n}\n\n.rvs-term-slot {\n position: absolute;\n inset: 0;\n}\n\n.rvs-term-dead {\n height: 100%;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 8px;\n background: var(--rvs-code-bg);\n color: var(--rvs-code-fg);\n}\n\n/* `.rvs-toast.error` is styled with the overlay form further up, so the popup\n opacity convention (an opaque Canvas base plus a token layer) still governs\n it. A flat `background: #3a1f22` shorthand sat here and overrode that chain —\n same opaque result, but outside the convention. */\n\n@keyframes rvsFade {\n from { opacity: 0; }\n to { opacity: 1; }\n}\n\n@keyframes rvsPop {\n from { opacity: 0; transform: scale(.97) translateY(3px); }\n to { opacity: 1; transform: none; }\n}\n\n.rvs-search-row {\n display: flex;\n gap: 6px;\n padding: 6px 8px;\n border-bottom: 1px solid var(--rvs-border);\n}\n\n/* --------------------------------------------------------- search options */\n\n.rvs-toggles {\n flex: none;\n display: inline-flex;\n border: 1px solid var(--rvs-border2);\n border-radius: 6px;\n overflow: hidden;\n}\n\n.rvs-toggle {\n min-width: 26px;\n height: 22px;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 0 5px;\n border: none;\n border-right: 1px solid var(--rvs-border);\n background: transparent;\n color: var(--rvs-fg3);\n font: inherit;\n cursor: pointer;\n}\n\n.rvs-toggle:last-child {\n border-right: none;\n}\n\n.rvs-toggle:hover {\n background: var(--rvs-hover);\n color: var(--rvs-fg);\n}\n\n.rvs-toggle.active {\n background: var(--rvs-active);\n color: var(--rvs-fg);\n box-shadow: inset 0 0 0 1px var(--rvs-brand);\n}\n\n.rvs-glyph {\n font-family: var(--rvs-mono);\n font-weight: 600;\n line-height: 1;\n letter-spacing: -0.02em;\n}\n\n.rvs-glyph-underline {\n text-decoration: underline;\n}\n\n.rvs-scope-label {\n flex: none;\n align-self: center;\n font-size: 11px;\n color: var(--rvs-fg3);\n white-space: nowrap;\n}\n\n/* --------------------------------------------------------- search results */\n\n.rvs-hit {\n display: flex;\n align-items: center;\n gap: 6px;\n width: 100%;\n padding: 3px 10px;\n border: none;\n background: transparent;\n color: var(--rvs-fg);\n font: inherit;\n font-size: 12.5px;\n text-align: left;\n cursor: pointer;\n}\n\n.rvs-hit:hover {\n background: var(--rvs-hover);\n}\n\n.rvs-hit-file {\n font-weight: 600;\n}\n\n.rvs-hit-line {\n padding-left: 30px;\n align-items: flex-start;\n font-family: var(--rvs-mono);\n font-size: 12px;\n color: var(--rvs-fg2);\n}\n\n.rvs-hit-count {\n flex: none;\n min-width: 18px;\n padding: 0 5px;\n border-radius: 9px;\n background: var(--rvs-hover);\n font-size: 10.5px;\n line-height: 15px;\n text-align: center;\n color: var(--rvs-fg3);\n}\n\n.rvs-hit-lineno {\n flex: none;\n width: 3.2em;\n text-align: right;\n color: var(--rvs-fg3);\n opacity: .8;\n font-size: 11px;\n}\n\n.rvs-hit-text {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: pre;\n direction: ltr;\n}\n\n.rvs-mark {\n padding: 0 1px;\n border-radius: 2px;\n background: rgba(226, 183, 20, .3);\n color: inherit;\n}\n\n.rvs-hit-dir {\n flex: none;\n color: var(--rvs-fg3);\n font-size: 11px;\n}\n\n.rvs-hit-name {\n flex: none;\n font-weight: 600;\n}\n\n.rvs-kbd {\n padding: 0 4px;\n border: 1px solid var(--rvs-border2);\n border-radius: 4px;\n font-size: 10px;\n color: var(--rvs-fg3);\n}\n\n/* --- SCM: repository directory row (git runs in THIS directory) ------------- */\n\n.rvs-repo-bar {\n display: flex;\n align-items: center;\n gap: 4px;\n padding: 4px 8px 4px 10px;\n border-bottom: 1px solid var(--rvs-border);\n background: var(--rvs-bg2, var(--dsw-alias-bg-layer-1, #1b1b1b));\n font-size: 11px;\n flex: none;\n}\n\n.rvs-repo-bar-label {\n color: var(--rvs-fg3);\n white-space: nowrap;\n}\n\n.rvs-repo-bar-path {\n flex: 1 1 auto;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n direction: rtl;\n unicode-bidi: plaintext;\n text-align: left;\n padding: 2px 6px;\n border: 1px solid var(--rvs-border2);\n border-radius: 4px;\n background: var(--rvs-bg3, rgba(255, 255, 255, .04));\n color: var(--rvs-fg);\n font-family: var(--rvs-mono, ui-monospace, Consolas, monospace);\n font-size: 11px;\n cursor: pointer;\n}\n\n.rvs-repo-bar-path:hover {\n border-color: var(--rvs-brand);\n color: var(--rvs-brand-fg, var(--rvs-fg));\n}\n\n.rvs-repo-bar-fix {\n margin-top: 10px;\n display: flex;\n justify-content: center;\n}\n\n/* --- SCM: repository directory picker -------------------------------------- */\n\n.rvs-repopick {\n width: min(560px, calc(100vw - 40px));\n display: flex;\n flex-direction: column;\n gap: 8px;\n}\n\n.rvs-repopick .rvs-modal-title {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 8px;\n}\n\n.rvs-repopick-bar {\n display: flex;\n align-items: center;\n flex-wrap: wrap;\n gap: 2px;\n min-height: 22px;\n font-family: var(--rvs-mono, ui-monospace, Consolas, monospace);\n font-size: 12px;\n}\n\n.rvs-crumb-wrap {\n display: inline-flex;\n align-items: center;\n gap: 2px;\n color: var(--rvs-fg3);\n}\n\n.rvs-crumb {\n border: none;\n background: none;\n padding: 1px 3px;\n border-radius: 3px;\n color: var(--rvs-brand-fg, var(--rvs-fg));\n font: inherit;\n cursor: pointer;\n}\n\n.rvs-crumb:hover {\n background: var(--rvs-row-hover, rgba(255, 255, 255, .07));\n}\n\n.rvs-repopick-probe {\n padding: 5px 8px;\n border-radius: 4px;\n font-size: 11.5px;\n border: 1px solid var(--rvs-border2);\n background: var(--rvs-bg3, rgba(255, 255, 255, .04));\n color: var(--rvs-fg2);\n}\n\n.rvs-repopick-probe.root {\n border-color: var(--rvs-ok, #2ea043);\n color: var(--rvs-ok, #2ea043);\n}\n\n.rvs-repopick-probe.inside {\n border-color: var(--rvs-brand);\n color: var(--rvs-brand-fg, var(--rvs-fg));\n}\n\n.rvs-repopick-found {\n display: flex;\n align-items: center;\n flex-wrap: wrap;\n gap: 4px;\n padding-bottom: 2px;\n}\n\n.rvs-repopick-found-label {\n color: var(--rvs-fg3);\n font-size: 11px;\n white-space: nowrap;\n}\n\n.rvs-repopick-list {\n flex: 1 1 auto;\n min-height: 150px;\n max-height: 42vh;\n overflow: auto;\n border: 1px solid var(--rvs-border);\n border-radius: 5px;\n background: var(--rvs-bg-layer, rgba(0, 0, 0, .18));\n padding: 2px;\n}\n\n.rvs-repopick-row {\n display: flex;\n align-items: center;\n gap: 6px;\n width: 100%;\n padding: 3px 6px;\n border: none;\n border-radius: 3px;\n background: none;\n color: var(--rvs-fg);\n font: inherit;\n font-size: 12px;\n text-align: left;\n cursor: pointer;\n}\n\n.rvs-repopick-row:hover {\n background: var(--rvs-row-hover, rgba(255, 255, 255, .07));\n}\n\n.rvs-repopick-row span {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.rvs-repopick-dim {\n color: var(--rvs-fg3);\n font-size: 11.5px;\n padding: 6px;\n}\n\n.rvs-repopick-type {\n display: flex;\n gap: 6px;\n align-items: center;\n}\n\n.rvs-repopick-type .rvs-input {\n flex: 1 1 auto;\n}\n\n.rvs-repopick-error {\n color: var(--rvs-error, #f85149);\n font-size: 11.5px;\n}\n";
15
15
  const key$2 = "E:\\work_base\\dsh_remote_vs\\src\\client\\vsc\\vsc.css";
16
16
  const injected$2 = typeof document === "undefined" ? void 0 : Array.prototype.find.call(document.querySelectorAll("style[data-rvs-css]"), (tag) => tag.dataset.rvsCss === key$2);
17
17
  if (typeof document !== "undefined" && injected$2 === void 0) {
@@ -1332,6 +1332,10 @@ window.__ModuleLoader__.load({
1332
1332
  "tab.git": "Git",
1333
1333
  "git.hint": "请选择远端路径为 git 仓库的工作区。",
1334
1334
  "scm.loadingSlow": "正在读取仓库状态…大仓库首次读取可能需要数分钟",
1335
+ "scm.cached": "缓存",
1336
+ "scm.cachedRefreshing": "缓存 · 后台刷新中",
1337
+ "scm.cachedHint": "显示的是该工作区上次读取的结果,正在后台刷新;更改列表与暂存/提交等操作仍作用于这个工作区。",
1338
+ "scm.refreshFailed": "刷新失败:",
1335
1339
  "common.close": "关闭",
1336
1340
  "common.loading": "加载中…",
1337
1341
  "flow.title": "添加工作区",
@@ -1351,7 +1355,7 @@ window.__ModuleLoader__.load({
1351
1355
  "route.off": "关闭(直通本地)",
1352
1356
  "route.shadow": "影子(记日志仍走本地)",
1353
1357
  "route.enforce": "强制(真路由)",
1354
- "route.hint": "切换即时生效并写入 profile,无需重启",
1358
+ "route.hint": "切换即时生效并写入 profile,无需重启;默认「强制(真路由)」。",
1355
1359
  "route.saving": "保存中…",
1356
1360
  "route.tombstones": "墓碑",
1357
1361
  "route.aliasPath": "本地别名目录(原生工作区入口)",
@@ -1679,6 +1683,10 @@ window.__ModuleLoader__.load({
1679
1683
  "tab.git": "Git",
1680
1684
  "git.hint": "Pick a workspace whose remote path is a git repository.",
1681
1685
  "scm.loadingSlow": "Reading repository state… a large repo can take minutes on first read",
1686
+ "scm.cached": "cached",
1687
+ "scm.cachedRefreshing": "cached · refreshing",
1688
+ "scm.cachedHint": "Showing this workspace’s last read while it refreshes in the background; the change list and stage/commit actions still target this workspace.",
1689
+ "scm.refreshFailed": "Refresh failed:",
1682
1690
  "common.close": "Close",
1683
1691
  "common.loading": "Loading…",
1684
1692
  "flow.title": "Add workspace",
@@ -1698,7 +1706,7 @@ window.__ModuleLoader__.load({
1698
1706
  "route.off": "Off (local passthrough)",
1699
1707
  "route.shadow": "Shadow (log only, still local)",
1700
1708
  "route.enforce": "Enforce (real routing)",
1701
- "route.hint": "Applies immediately and persists to the profile; no restart needed.",
1709
+ "route.hint": "Applies immediately and persists to the profile; no restart needed. Default: enforce (real routing).",
1702
1710
  "route.saving": "Saving…",
1703
1711
  "route.tombstones": "tombstones",
1704
1712
  "route.aliasPath": "Local alias dir (native workspace entry)",
@@ -35251,7 +35259,7 @@ window.__ModuleLoader__.load({
35251
35259
  const { actx, input } = resolved;
35252
35260
  const snapshot = input.state.getSnapshot();
35253
35261
  if (input.insertReference === void 0 || snapshot.draftRev === void 0) return false;
35254
- if (snapshot.phase === "adjudicating" || snapshot.phase === "submitting") return false;
35262
+ if (snapshot.phase === "adjudicating" || snapshot.phase === "submitting" || snapshot.phase === "sending") return false;
35255
35263
  before = snapshot.draftRev;
35256
35264
  const at = appendOffset(snapshot);
35257
35265
  if (at === void 0) return false;
@@ -35306,6 +35314,7 @@ window.__ModuleLoader__.load({
35306
35314
  const { actx, input } = resolved;
35307
35315
  const snapshot = input.state.getSnapshot();
35308
35316
  const draft = snapshot.draft;
35317
+ if (snapshot.phase === "adjudicating" || snapshot.phase === "submitting" || snapshot.phase === "sending") return false;
35309
35318
  if ((snapshot.occurrences ?? []).length > 0 || snapshot.phase === "claimed") {
35310
35319
  const at = appendOffset(snapshot);
35311
35320
  if (snapshot.draftRev === void 0 || at === void 0) {
@@ -38121,7 +38130,7 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
38121
38130
  const primary = host.querySelectorAll("[class*=\"rvs-ck-\"]").length;
38122
38131
  const fallback = host.querySelectorAll("span[class^=\"rvs-t-\"]").length;
38123
38132
  const count = primary > 0 ? primary : fallback;
38124
- console.info(`[dsh-remote-vs] build 1027-1916 · ${source} · colored tokens: ${count} (${propsRef.current.path})`);
38133
+ console.info(`[dsh-remote-vs] build 1028-1532 · ${source} · colored tokens: ${count} (${propsRef.current.path})`);
38125
38134
  propsRef.current.onTokens?.(count);
38126
38135
  };
38127
38136
  const armFallback = () => {
@@ -38530,6 +38539,27 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
38530
38539
  function repoProbeCommand(workdir) {
38531
38540
  return `${git(workdir)} rev-parse --show-toplevel 2>/dev/null`;
38532
38541
  }
38542
+ /**
38543
+ * Repository probe that keeps the ANSWER (exit code + both streams) on stdout.
38544
+ *
38545
+ * `repoProbeCommand` throws the exit code and stderr away — everything that is
38546
+ * not a root line reads as "nothing". That is fine for the workspace wizard
38547
+ * ("nothing to offer") but not for the SCM panel, where "this is not a
38548
+ * repository" and "git could not answer" are DIFFERENT states: the first is a
38549
+ * fact that may be cached, the second must keep the last known change list and
38550
+ * report the reason. So this command always exits 0 and prints
38551
+ *
38552
+ * RVS_RC=<git exit code>
38553
+ * <everything git wrote, stderr merged>
38554
+ *
38555
+ * `LC_ALL=C` pins git's wording (the client matches `not a git repository`; a
38556
+ * localized host would otherwise read as a failure), and merging the streams is
38557
+ * what makes that message survive — git writes it to stderr, which the plain
38558
+ * probe discards.
38559
+ */
38560
+ function repoProbeDetailCommand(workdir) {
38561
+ return capped(`out=$(LC_ALL=C LANG=C ${git(workdir)} rev-parse --show-toplevel 2>&1); rc=$?; printf 'RVS_RC=%s\\n%s\\n' "$rc" "$out"`);
38562
+ }
38533
38563
  /** Discover repositories under a workspace root (depth-capped). */
38534
38564
  function repoDiscoveryCommand(root) {
38535
38565
  return capped(`find ${shQuote(assertRemoteDir(root))} -maxdepth 3 \\( -name .git -type d -o -name .git -type f \\) -prune -print 2>/dev/null`, 64e3);
@@ -38736,6 +38766,38 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
38736
38766
  const line = stdout.split("\n")[0]?.trim() ?? "";
38737
38767
  return line.startsWith("/") ? line : void 0;
38738
38768
  }
38769
+ /**
38770
+ * Parse `repoProbeDetailCommand`'s output into the three states the panel needs.
38771
+ *
38772
+ * "not a git repository" is matched on git's own message rather than on the exit
38773
+ * code: `rev-parse` exits 128 for that AND for a directory it cannot enter, so
38774
+ * the code alone cannot separate the two. The message is guaranteed English by
38775
+ * the command's `LC_ALL=C`.
38776
+ */
38777
+ function parseRepoProbe(stdout) {
38778
+ const marker = /^RVS_RC=(\d+)\n?/.exec(stdout);
38779
+ if (marker === null) return {
38780
+ kind: "failed",
38781
+ error: "the repository probe returned no status line"
38782
+ };
38783
+ const code = Number(marker[1]);
38784
+ const detail = stdout.slice(marker[0].length).trim();
38785
+ if (code === 0) {
38786
+ const root = detail.split("\n").map((line) => line.trim()).find((line) => line.startsWith("/"));
38787
+ return root === void 0 ? {
38788
+ kind: "failed",
38789
+ error: `git rev-parse printed no repository root: ${detail}`
38790
+ } : {
38791
+ kind: "repo",
38792
+ root
38793
+ };
38794
+ }
38795
+ if (/not a git repository/i.test(detail)) return { kind: "none" };
38796
+ return {
38797
+ kind: "failed",
38798
+ error: detail !== "" ? detail : `git rev-parse --show-toplevel failed [exit code: ${code}]`
38799
+ };
38800
+ }
38739
38801
  /** Repository dirs found under a workspace root. */
38740
38802
  function parseRepoDiscovery(stdout) {
38741
38803
  const seen = /* @__PURE__ */ new Set();
@@ -39952,14 +40014,18 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
39952
40014
  */
39953
40015
  const normalTimeoutMs = () => gitTimeoutMs();
39954
40016
  const longTimeoutMs = () => Math.min(MAX_TIMEOUT_MS, Math.max(DEFAULT_GIT_TIMEOUT_MS, gitTimeoutMs() * 2));
40017
+ /** The readable error for a failed exec result (`run` and the strict probe share it). */
40018
+ function failureOf(result, timeoutMs) {
40019
+ if (result.timedOut) return new GitOpError(`git timed out after ${timeoutSeconds(timeoutMs)} — the repository may be large or the host busy; raise gitTimeoutMs in the plugin settings`);
40020
+ const hint = gitErrorHint(result.stderr) ?? gitErrorHint(result.stdout);
40021
+ const detail = result.stderr.trim() !== "" ? result.stderr.trim() : result.stdout.trim() !== "" ? result.stdout.trim() : "";
40022
+ return new GitOpError(hint ?? detail ?? result.error ?? `git failed [exit code: ${result.exitCode ?? "?"}]`);
40023
+ }
39955
40024
  /** Run one command, raising a readable error on a non-zero exit. */
39956
40025
  async function run(alias, command, timeoutMs = normalTimeoutMs()) {
39957
40026
  const result = await api.exec(alias, command, timeoutMs);
39958
40027
  if (result.success) return result;
39959
- if (result.timedOut) throw new GitOpError(`git timed out after ${timeoutSeconds(timeoutMs)} — the repository may be large or the host busy; raise gitTimeoutMs in the plugin settings`);
39960
- const hint = gitErrorHint(result.stderr) ?? gitErrorHint(result.stdout);
39961
- const detail = result.stderr.trim() !== "" ? result.stderr.trim() : result.stdout.trim() !== "" ? result.stdout.trim() : "";
39962
- throw new GitOpError(hint ?? detail ?? result.error ?? `git failed [exit code: ${result.exitCode ?? "?"}]`);
40028
+ throw failureOf(result, timeoutMs);
39963
40029
  }
39964
40030
  /** Run one command tolerating failure (returns '' instead). */
39965
40031
  async function tryRun(alias, command, timeoutMs = normalTimeoutMs()) {
@@ -39974,6 +40040,23 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
39974
40040
  async function resolveRepoRoot(alias, workspaceRoot) {
39975
40041
  return parseRepoRoot(await tryRun(alias, repoProbeCommand(workspaceRoot), 6e4));
39976
40042
  }
40043
+ /** The repository root, or WHY the probe could not answer. */
40044
+ async function probeRepoRoot(alias, workspaceRoot) {
40045
+ let result;
40046
+ try {
40047
+ result = await api.exec(alias, repoProbeDetailCommand(workspaceRoot), normalTimeoutMs());
40048
+ } catch (error) {
40049
+ return {
40050
+ kind: "failed",
40051
+ error: error instanceof Error ? error.message : String(error)
40052
+ };
40053
+ }
40054
+ if (!result.success) return {
40055
+ kind: "failed",
40056
+ error: failureOf(result, normalTimeoutMs()).message
40057
+ };
40058
+ return parseRepoProbe(result.stdout);
40059
+ }
39977
40060
  /** Repositories under a workspace root (the workspace itself sorts first). */
39978
40061
  async function findRepos(alias, workspaceRoot) {
39979
40062
  const direct = await resolveRepoRoot(alias, workspaceRoot);
@@ -40569,6 +40652,207 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
40569
40652
  }
40570
40653
  return next ?? current;
40571
40654
  }
40655
+ //#endregion
40656
+ //#region src/client/scm/read-queue.ts
40657
+ /**
40658
+ * Bound a read by a hard settle deadline.
40659
+ *
40660
+ * A remote call can hang without ever settling — a stuck SSH round trip, and the
40661
+ * change ledger has measured exactly that on a monorepo. Without a bound, the
40662
+ * flight would stay registered forever: every later `join` would reuse the dead
40663
+ * promise, every `fresh` would wait for it, and the owner's busy flag would stay
40664
+ * up, so ONE hung host call would freeze that key for the rest of the page (the
40665
+ * pre-cache code never locked up: each refresh was independent and could fail on
40666
+ * its own). The deadline rejects the FLIGHT — releasing the key — while the
40667
+ * caller's failure handler owns the message; the underlying read may still
40668
+ * complete later, and nothing it paints was waiting on it.
40669
+ */
40670
+ function withSettleDeadline(read, ms) {
40671
+ return () => {
40672
+ const pending = read();
40673
+ pending.catch(() => void 0);
40674
+ let timer;
40675
+ const deadline = new Promise((_, reject) => {
40676
+ timer = setTimeout(() => {
40677
+ reject(/* @__PURE__ */ new Error(`reading did not settle within ${Math.round(ms / 1e3)}s — the host call may be stuck`));
40678
+ }, ms);
40679
+ timer.unref?.();
40680
+ });
40681
+ return Promise.race([pending, deadline]).finally(() => {
40682
+ if (timer !== void 0) clearTimeout(timer);
40683
+ });
40684
+ };
40685
+ }
40686
+ var ReadQueue = class {
40687
+ onChange;
40688
+ /** In-flight reads by key (insertion order is irrelevant here). */
40689
+ flights = /* @__PURE__ */ new Map();
40690
+ nextId = 0;
40691
+ /**
40692
+ * @param onChange - called whenever a read starts or settles, so the owner can
40693
+ * derive a "busy" flag from `inflightKeys()` without polling.
40694
+ */
40695
+ constructor(onChange) {
40696
+ this.onChange = onChange;
40697
+ }
40698
+ /** The keys with a read in flight (a caller derives its busy state from this). */
40699
+ inflightKeys() {
40700
+ return [...this.flights.keys()];
40701
+ }
40702
+ /** True while ONE key has a read in flight. */
40703
+ isBusy(key) {
40704
+ return this.flights.has(key);
40705
+ }
40706
+ /**
40707
+ * Run `read` for `key`, honouring `mode`.
40708
+ *
40709
+ * The returned promise settles with this key's answer: a rejection comes from
40710
+ * the read the caller ended up waiting on (its own, or the one it joined).
40711
+ * `read` must be equivalent for every caller that passes the same key — the
40712
+ * queue keeps only the FIRST caller's closure, because a joined read cannot
40713
+ * carry a second closure.
40714
+ *
40715
+ * @param settleMs - hard settle deadline for a read THIS call starts (`0` =
40716
+ * unbounded). A joined read keeps the deadline it was started with: its
40717
+ * caller already decided how long that read may take.
40718
+ */
40719
+ run(key, read, mode = "join", settleMs = 0) {
40720
+ const current = this.flights.get(key);
40721
+ if (current === void 0) return this.start(key, read, settleMs);
40722
+ if (mode === "join") return current.promise;
40723
+ return this.after(current, key, read, settleMs);
40724
+ }
40725
+ /**
40726
+ * `fresh` while a read is in flight: wait for it, then read once more.
40727
+ *
40728
+ * The follow-up is NOT started from here unconditionally, or two `fresh`
40729
+ * callers that arrived during the same read would each start one (two
40730
+ * concurrent walks, exactly what this class exists to prevent). The second
40731
+ * caller instead finds a flight whose id is newer than the one it waited on —
40732
+ * a read that started after its own call — and shares it.
40733
+ */
40734
+ async after(flight, key, read, settleMs) {
40735
+ await flight.promise.catch(() => void 0);
40736
+ const current = this.flights.get(key);
40737
+ if (current !== void 0 && current.id > flight.id) return current.promise;
40738
+ return this.start(key, read, settleMs);
40739
+ }
40740
+ /** Register a flight and run `read`; a synchronous throw becomes a rejection. */
40741
+ start(key, read, settleMs) {
40742
+ const id = ++this.nextId;
40743
+ const body = settleMs > 0 ? withSettleDeadline(read, settleMs) : read;
40744
+ const promise = Promise.resolve().then(body).finally(() => {
40745
+ const current = this.flights.get(key);
40746
+ if (current !== void 0 && current.id === id) this.flights.delete(key);
40747
+ this.onChange?.();
40748
+ });
40749
+ this.flights.set(key, {
40750
+ id,
40751
+ promise
40752
+ });
40753
+ this.onChange?.();
40754
+ return promise;
40755
+ }
40756
+ };
40757
+ //#endregion
40758
+ //#region src/client/scm/git-cache.ts
40759
+ /**
40760
+ * Separator between the binding id and the repository directory.
40761
+ *
40762
+ * NUL cannot appear in either half: a binding id comes from the host's table
40763
+ * and a workspace-relative sub-directory is a path. The key must be reversible
40764
+ * because `retainGitSnapshots` deletes everything whose binding is gone.
40765
+ */
40766
+ const KEY_SEP$1 = "\0";
40767
+ /** Cache key for one (workspace, repository directory) pair. */
40768
+ function gitCacheKey(bindingId, repoSubdir) {
40769
+ return `${bindingId}${KEY_SEP$1}${repoSubdir}`;
40770
+ }
40771
+ /** Split a cache key back into its two halves (undefined when it is not one). */
40772
+ function parseGitCacheKey(key) {
40773
+ const at = key.indexOf(KEY_SEP$1);
40774
+ if (at < 0) return void 0;
40775
+ return {
40776
+ bindingId: key.slice(0, at),
40777
+ repoSubdir: key.slice(at + 1)
40778
+ };
40779
+ }
40780
+ /**
40781
+ * How many (workspace, repository) pairs stay cached.
40782
+ *
40783
+ * Bindings are few (the workspace table is hand-managed) and each snapshot is
40784
+ * the panel's whole change list, so a small bound is plenty; the LRU trim only
40785
+ * exists so a long-lived page cannot grow without limit.
40786
+ */
40787
+ const SNAPSHOT_LIMIT = 12;
40788
+ /** Snapshots by cache key, in recency order (oldest insertion first). */
40789
+ const snapshots = /* @__PURE__ */ new Map();
40790
+ /** The cached read for one (workspace, repository directory), if any. */
40791
+ function readGitSnapshot(key) {
40792
+ const snapshot = snapshots.get(key);
40793
+ if (snapshot === void 0) return void 0;
40794
+ snapshots.delete(key);
40795
+ snapshots.set(key, snapshot);
40796
+ return snapshot;
40797
+ }
40798
+ /**
40799
+ * Remember one read.
40800
+ *
40801
+ * Called even when the read is no longer the binding on screen: the walk has
40802
+ * already been paid for, and caching it is what makes the NEXT switch back
40803
+ * instant instead of another minutes-long wait.
40804
+ *
40805
+ * An answer OLDER than the one already cached is dropped. Two reads of one
40806
+ * workspace can overlap when the repo picker changes the directory mid-read (the
40807
+ * settled directory is a second key, see the store's `readGit`), and the older
40808
+ * walk may finish last — letting it win would hand the user a change list that
40809
+ * predates the one already shown.
40810
+ */
40811
+ function writeGitSnapshot(key, snapshot) {
40812
+ const previous = snapshots.get(key);
40813
+ if (previous !== void 0 && previous.at > snapshot.at) return;
40814
+ snapshots.delete(key);
40815
+ snapshots.set(key, snapshot);
40816
+ while (snapshots.size > SNAPSHOT_LIMIT) {
40817
+ const oldest = snapshots.keys().next();
40818
+ if (oldest.done === true) break;
40819
+ snapshots.delete(oldest.value);
40820
+ }
40821
+ }
40822
+ /** Drop every snapshot whose workspace left the binding table. */
40823
+ function retainGitSnapshots(bindingIds) {
40824
+ for (const key of [...snapshots.keys()]) {
40825
+ const parsed = parseGitCacheKey(key);
40826
+ if (parsed === void 0 || !bindingIds.has(parsed.bindingId)) snapshots.delete(key);
40827
+ }
40828
+ }
40829
+ /**
40830
+ * Write a locally-known status into a cached snapshot.
40831
+ *
40832
+ * The plotted stage/unstage move is applied to the store's status WITHOUT a
40833
+ * repository-wide read (see `applyLocalStage`). Without this write-through the
40834
+ * cache would keep the pre-click list, so switching away and back would flip
40835
+ * the row back — the panel would show a change the user already staged.
40836
+ * A different `repoRoot` means the snapshot is not that repository: refusing is
40837
+ * what keeps one workspace's rows out of another's cache.
40838
+ */
40839
+ function patchCachedGitStatus(key, repoRoot, status) {
40840
+ const snapshot = snapshots.get(key);
40841
+ if (snapshot === void 0 || snapshot.repoRoot !== repoRoot) return;
40842
+ snapshots.set(key, {
40843
+ ...snapshot,
40844
+ status
40845
+ });
40846
+ }
40847
+ /** Same write-through for the expanded untracked directories. */
40848
+ function patchCachedUntrackedDirs(key, repoRoot, dirs) {
40849
+ const snapshot = snapshots.get(key);
40850
+ if (snapshot === void 0 || snapshot.repoRoot !== repoRoot) return;
40851
+ snapshots.set(key, {
40852
+ ...snapshot,
40853
+ untrackedDirs: dirs
40854
+ });
40855
+ }
40572
40856
  function clamp(value, min, max) {
40573
40857
  return Math.min(max, Math.max(min, value));
40574
40858
  }
@@ -41412,7 +41696,7 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
41412
41696
  hosts: [],
41413
41697
  workspaces: [],
41414
41698
  routing: {
41415
- mode: "off",
41699
+ mode: "enforce",
41416
41700
  aliasPaths: {},
41417
41701
  tombstones: []
41418
41702
  },
@@ -41424,6 +41708,7 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
41424
41708
  repoMissing: false,
41425
41709
  gitBusy: false,
41426
41710
  gitError: void 0,
41711
+ gitCached: false,
41427
41712
  gitDeadlineMs: DEFAULT_GIT_TIMEOUT_MS,
41428
41713
  tabs: [],
41429
41714
  activeTabId: void 0,
@@ -41500,6 +41785,7 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
41500
41785
  head: state.head,
41501
41786
  gitBusy: state.gitBusy,
41502
41787
  gitError: state.gitError,
41788
+ gitCached: state.gitCached,
41503
41789
  gitDeadlineMs: state.gitDeadlineMs,
41504
41790
  tabs: state.tabs,
41505
41791
  activeTab: state.tabs.find((item) => item.id === state.activeTabId),
@@ -41591,13 +41877,96 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
41591
41877
  };
41592
41878
  }
41593
41879
  /**
41594
- * Monotonic ticket for git refreshes. Two refreshes can overlap whenever the
41595
- * binding changes or a write lands mid-read, and both panels stay mounted, so
41596
- * an older response arriving last would otherwise write ITS repository into
41597
- * the state the newer binding is using — after which every staged/discard
41598
- * button would act on the wrong repo (scm/actions.ts takes alias + repoRoot).
41880
+ * Git reads, one walk at a time per (workspace, repository directory).
41881
+ *
41882
+ * Two reads can be wanted at once — the background refresh of the binding on
41883
+ * screen and a ⟳ / post-write read — and on a large repository a second walk of
41884
+ * the same tree is minutes of duplicate work whose answers disagree about which
41885
+ * one is current. The queue gives callers two contracts instead of that race
41886
+ * (`scm/read-queue.ts`): `join` shares a read already in flight (automatic
41887
+ * refreshes, workspace switches), `fresh` waits for it and reads again (⟳, and
41888
+ * every write, whose answer must include the write).
41889
+ */
41890
+ const gitReads = new ReadQueue(refreshGitBusy);
41891
+ /**
41892
+ * Which read may repaint the visible slice, per binding.
41893
+ *
41894
+ * The repo picker can ask for a DIFFERENT directory of the same workspace while
41895
+ * a read of another one is still walking; the older answer describes the
41896
+ * directory the user just left and must not land on top of the newer request.
41897
+ * Cache writes are deliberately not gated: they belong to the directory the read
41898
+ * was about, and are what makes the next visit instant.
41599
41899
  */
41600
- let gitGeneration = 0;
41900
+ const latestGitRead = /* @__PURE__ */ new Map();
41901
+ let gitReadTicket = 0;
41902
+ /** True when `bindingId`'s read `ticket` may paint the on-screen slice. */
41903
+ function gitReadIsCurrent(bindingId, ticket) {
41904
+ return latestGitRead.get(bindingId) === ticket && activeBinding().key === bindingId;
41905
+ }
41906
+ /** The cache key of what the panel is showing (workspace + repository directory). */
41907
+ function currentGitKey() {
41908
+ return gitCacheKey(activeBinding().key, state.repoSubdir);
41909
+ }
41910
+ /** Recompute the busy flag from the queue (called on every start/settle). */
41911
+ function refreshGitBusy() {
41912
+ const bindingId = activeBinding().key;
41913
+ const busy = gitReads.inflightKeys().some((key) => parseGitCacheKey(key)?.bindingId === bindingId);
41914
+ if (state.gitBusy !== busy) patch({ gitBusy: busy });
41915
+ }
41916
+ /**
41917
+ * A cached read this young is what a refresh would answer anyway.
41918
+ *
41919
+ * Flipping between two workspaces must not start a monorepo-wide walk on every
41920
+ * hop: within this window the cached slice is treated as current. Every explicit
41921
+ * refresh (⟳, a write) and every switch to an OLDER snapshot still re-reads.
41922
+ */
41923
+ const CACHED_FRESH_MS = 5e3;
41924
+ /**
41925
+ * The state fields one cached read stands for — the switch's synchronous paint.
41926
+ *
41927
+ * A MISS clears the slice exactly like the old code did: the workspace has never
41928
+ * been read, so there is nothing honest to show and the read about to start owns
41929
+ * the "Loading…" state. `repoSubdir` keeps the persisted choice so that read
41930
+ * runs in the directory the user last used.
41931
+ *
41932
+ * `refreshing` says whether a background read was actually started; when it is
41933
+ * false the slice is merely a few seconds old and is presented as current — a
41934
+ * "cached" badge with nothing refreshing would be noise, not information.
41935
+ */
41936
+ function cachedGitSlice(bindingId, refreshing) {
41937
+ const subdir = readRepoSubdir(bindingId);
41938
+ const snapshot = readGitSnapshot(gitCacheKey(bindingId, subdir));
41939
+ if (snapshot === void 0) return {
41940
+ status: void 0,
41941
+ head: void 0,
41942
+ repoRoot: void 0,
41943
+ repoSubdir: subdir,
41944
+ repoMissing: false,
41945
+ untrackedDirs: /* @__PURE__ */ new Map(),
41946
+ gitError: void 0,
41947
+ gitCached: false
41948
+ };
41949
+ return {
41950
+ status: snapshot.status,
41951
+ head: snapshot.head,
41952
+ repoRoot: snapshot.repoRoot,
41953
+ repoSubdir: snapshot.repoSubdir,
41954
+ repoMissing: snapshot.repoMissing,
41955
+ untrackedDirs: snapshot.untrackedDirs,
41956
+ gitError: void 0,
41957
+ gitCached: refreshing
41958
+ };
41959
+ }
41960
+ /** Paint the cached slice of `bindingId` (a selection moved there). */
41961
+ function adoptCachedGit(bindingId) {
41962
+ patch(cachedGitSlice(bindingId, true));
41963
+ }
41964
+ /** Mirror the expansion set into the cached snapshot of the binding on screen. */
41965
+ function mirrorUntrackedDirs() {
41966
+ const { workspace, key } = activeBinding();
41967
+ if (workspace === void 0) return;
41968
+ patchCachedUntrackedDirs(gitCacheKey(key, state.repoSubdir), state.repoRoot, state.untrackedDirs);
41969
+ }
41601
41970
  let booted = false;
41602
41971
  /** Install the context and load hosts/workspaces once per page. */
41603
41972
  function bootstrap(ctx) {
@@ -41629,7 +41998,12 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
41629
41998
  ready: true,
41630
41999
  error: void 0
41631
42000
  });
41632
- refreshGit();
42001
+ const bindingIds = new Set(payload.workspaces.map((item) => item.id));
42002
+ retainGitSnapshots(bindingIds);
42003
+ for (const id of [...latestGitRead.keys()]) if (!bindingIds.has(id)) latestGitRead.delete(id);
42004
+ const resolved = activeBinding().key;
42005
+ if (resolved !== key) adoptCachedGit(resolved);
42006
+ refreshGit("join");
41633
42007
  }, (error) => {
41634
42008
  patch({
41635
42009
  ready: true,
@@ -41730,20 +42104,19 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
41730
42104
  if (byUser) noteManualPick();
41731
42105
  stageOverrides = /* @__PURE__ */ new Map();
41732
42106
  stageOverrideTarget = "";
42107
+ const snapshot = readGitSnapshot(gitCacheKey(id, readRepoSubdir(id)));
42108
+ const stale = snapshot === void 0 || Date.now() - snapshot.at >= CACHED_FRESH_MS;
41733
42109
  patch({
41734
42110
  workspaceId: id,
41735
42111
  tabs: [],
41736
42112
  activeTabId: void 0,
41737
- status: void 0,
41738
- head: void 0,
41739
- repoRoot: void 0,
41740
- repoSubdir: readRepoSubdir(id),
41741
- untrackedDirs: /* @__PURE__ */ new Map()
42113
+ ...cachedGitSlice(id, stale)
41742
42114
  });
41743
42115
  try {
41744
42116
  localStorage.setItem(WS_KEY, id);
41745
42117
  } catch {}
41746
- refreshGit();
42118
+ refreshGitBusy();
42119
+ if (stale) refreshGit("join");
41747
42120
  }
41748
42121
  let searchSeq = 0;
41749
42122
  /**
@@ -41955,12 +42328,8 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
41955
42328
  const { workspace, key } = activeBinding();
41956
42329
  const clean = normalizeRepoSubdir(subdir);
41957
42330
  if (workspace !== void 0) writeRepoSubdir(key, clean);
41958
- patch({
41959
- repoSubdir: clean,
41960
- repoRoot: void 0,
41961
- gitError: void 0
41962
- });
41963
- await refreshGit();
42331
+ adoptCachedGit(key);
42332
+ await refreshGit("fresh");
41964
42333
  }
41965
42334
  /** Repositories found under the workspace root (workspace-relative, root first). */
41966
42335
  async function discoverRepos() {
@@ -41968,33 +42337,72 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
41968
42337
  if (workspace === void 0) return [];
41969
42338
  return (await findRepos(workspace.alias, workspace.remotePath)).map((dir) => repoSubdirOf(workspace.remotePath, dir)).filter((dir) => dir !== void 0);
41970
42339
  }
41971
- /** Re-read git status for the current binding. */
41972
- async function refreshGit() {
42340
+ /**
42341
+ * Re-read git for the binding on screen.
42342
+ *
42343
+ * Reads go through `scm/read-queue.ts`, keyed by (binding, repository directory):
42344
+ * `join` shares a read that is already in flight — an automatic refresh has no
42345
+ * reason to duplicate a walk — while `fresh` waits for it and then reads again,
42346
+ * because the caller just wrote something and the answer must include it. Either
42347
+ * mode never runs two walks of the same repository at once, which is what lets a
42348
+ * background refresh and the ⟳ button coexist instead of racing.
42349
+ *
42350
+ * The answer is cached under the key the read STARTED with (even when the user
42351
+ * has already switched away: the walk is paid for either way, and the cache is
42352
+ * what makes the next visit instant) and repaints only while it is still that
42353
+ * binding's latest read.
42354
+ *
42355
+ * @param mode - `fresh` (default) never reuses a read that started before the
42356
+ * call; `join` shares a running one (workspace switches, bootstrap).
42357
+ */
42358
+ async function refreshGit(mode = "fresh") {
41973
42359
  const { workspace, key } = activeBinding();
41974
42360
  if (workspace === void 0) return;
41975
- const gen = ++gitGeneration;
41976
- const stale = () => gen !== gitGeneration || activeBinding().key !== key;
42361
+ const subdir = state.repoSubdir;
42362
+ const cacheKey = gitCacheKey(key, subdir);
42363
+ const settleMs = gitReadSettleMs();
42364
+ try {
42365
+ await gitReads.run(cacheKey, () => readGit(key, subdir, cacheKey), mode, settleMs);
42366
+ } catch (error) {
42367
+ if (activeBinding().key !== key || currentGitKey() !== cacheKey) return;
42368
+ patch({
42369
+ gitError: error instanceof Error ? error.message : String(error),
42370
+ gitCached: state.repoRoot !== void 0
42371
+ });
42372
+ }
42373
+ }
42374
+ /**
42375
+ * One real read: probe the repository root, then status + branch header together,
42376
+ * then cache the answer and paint it when it still owns the screen.
42377
+ */
42378
+ async function readGit(bindingId, subdir, cacheKey) {
42379
+ const workspace = state.workspaces.find((item) => item.id === bindingId);
42380
+ if (workspace === void 0) return;
42381
+ const ticket = ++gitReadTicket;
42382
+ latestGitRead.set(bindingId, ticket);
41977
42383
  const stageAt = stageSeq;
41978
- patch({ gitBusy: true });
41979
42384
  try {
41980
- const workdir = repoWorkdir(workspace.remotePath, state.repoSubdir);
41981
- const root = await resolveRepoRoot(workspace.alias, workdir);
41982
- if (stale()) return;
41983
- if (root === void 0) {
41984
- patch({
42385
+ const workdir = repoWorkdir(workspace.remotePath, subdir);
42386
+ const probe = await withDeadline$2(probeRepoRoot(workspace.alias, workdir), gitDeadlineMs(), t("editor.readTimeout"));
42387
+ if (probe.kind === "failed") throw new Error(probe.error);
42388
+ if (probe.kind === "none") {
42389
+ const snapshot = {
41985
42390
  repoRoot: void 0,
41986
- repoMissing: true,
42391
+ repoSubdir: subdir,
41987
42392
  status: EMPTY_STATUS,
41988
42393
  head: void 0,
41989
- gitBusy: false,
41990
- gitError: void 0
41991
- });
42394
+ repoMissing: true,
42395
+ untrackedDirs: /* @__PURE__ */ new Map(),
42396
+ at: Date.now()
42397
+ };
42398
+ writeGitSnapshot(cacheKey, snapshot);
42399
+ paintGit(bindingId, ticket, snapshot);
41992
42400
  return;
41993
42401
  }
42402
+ const root = probe.root;
41994
42403
  const [detail, head] = await Promise.all([withDeadline$2(loadStatusDetail(workspace.alias, root), gitDeadlineMs(), t("editor.readTimeout")), withDeadline$2(loadHead(workspace.alias, root), gitDeadlineMs(), t("editor.readTimeout"))]);
41995
- if (stale()) return;
41996
- const settled = repoSubdirOf(workspace.remotePath, root) ?? state.repoSubdir;
41997
- if (settled !== state.repoSubdir) writeRepoSubdir(key, settled);
42404
+ const settled = repoSubdirOf(workspace.remotePath, root) ?? subdir;
42405
+ if (settled !== readRepoSubdir(bindingId) && latestGitRead.get(bindingId) === ticket) writeRepoSubdir(bindingId, settled);
41998
42406
  const target = stageTargetKey({
41999
42407
  alias: workspace.alias,
42000
42408
  root
@@ -42003,27 +42411,46 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
42003
42411
  if (stageOverrideTarget === target) {
42004
42412
  for (const [path, override] of stageOverrides) if (override.at <= stageAt) stageOverrides.delete(path);
42005
42413
  }
42006
- patch({
42414
+ const snapshot = {
42007
42415
  repoRoot: root,
42008
42416
  repoSubdir: settled,
42009
- repoMissing: false,
42010
42417
  status,
42011
42418
  head,
42012
- gitBusy: false,
42013
- gitError: void 0,
42014
- untrackedDirs: pruneDirExpansions(state.untrackedDirs, status)
42015
- });
42419
+ repoMissing: false,
42420
+ untrackedDirs: pruneDirExpansions(bindingId === activeBinding().key ? state.untrackedDirs : readGitSnapshot(cacheKey)?.untrackedDirs ?? /* @__PURE__ */ new Map(), status),
42421
+ at: Date.now()
42422
+ };
42423
+ writeGitSnapshot(cacheKey, snapshot);
42424
+ if (settled !== subdir) writeGitSnapshot(gitCacheKey(bindingId, settled), snapshot);
42425
+ paintGit(bindingId, ticket, snapshot);
42016
42426
  } catch (error) {
42017
- if (stale()) return;
42427
+ if (!gitReadIsCurrent(bindingId, ticket)) return;
42018
42428
  patch({
42019
- repoRoot: void 0,
42020
- head: void 0,
42021
- status: EMPTY_STATUS,
42022
- gitBusy: false,
42023
- gitError: error instanceof Error ? error.message : String(error)
42429
+ gitError: error instanceof Error ? error.message : String(error),
42430
+ gitCached: state.repoRoot !== void 0
42024
42431
  });
42025
42432
  }
42026
42433
  }
42434
+ /**
42435
+ * Adopt one read's answer when it still owns the screen.
42436
+ *
42437
+ * The ticket is what keeps two workspaces apart: a slow read of the workspace
42438
+ * the user just left still refreshes THAT workspace's cache — so switching back
42439
+ * is instant — but it never paints over the repository now on screen.
42440
+ */
42441
+ function paintGit(bindingId, ticket, snapshot) {
42442
+ if (!gitReadIsCurrent(bindingId, ticket)) return;
42443
+ patch({
42444
+ repoRoot: snapshot.repoRoot,
42445
+ repoSubdir: snapshot.repoSubdir,
42446
+ repoMissing: snapshot.repoMissing,
42447
+ status: snapshot.status,
42448
+ head: snapshot.head,
42449
+ untrackedDirs: snapshot.untrackedDirs,
42450
+ gitError: void 0,
42451
+ gitCached: false
42452
+ });
42453
+ }
42027
42454
  /** Refresh everything a working-tree change can invalidate. */
42028
42455
  async function afterWrite() {
42029
42456
  await refreshGit();
@@ -42067,11 +42494,17 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
42067
42494
  if (staged) for (const path of paths) next = insertStagedEntry(next, path);
42068
42495
  next = optimisticStageDetail(next, paths, staged);
42069
42496
  if (next === state.status) return;
42070
- const dirs = staged ? dropExpandedRows(state.untrackedDirs, paths) : state.untrackedDirs;
42497
+ const nextDirs = pruneDirExpansions(staged ? dropExpandedRows(state.untrackedDirs, paths) : state.untrackedDirs, next);
42071
42498
  patch({
42072
42499
  status: next,
42073
- untrackedDirs: pruneDirExpansions(dirs, next)
42500
+ untrackedDirs: nextDirs
42074
42501
  });
42502
+ const { workspace, key: bindingId } = activeBinding();
42503
+ if (workspace !== void 0) {
42504
+ const cacheKey = gitCacheKey(bindingId, state.repoSubdir);
42505
+ patchCachedGitStatus(cacheKey, state.repoRoot, next);
42506
+ patchCachedUntrackedDirs(cacheKey, state.repoRoot, nextDirs);
42507
+ }
42075
42508
  }
42076
42509
  /**
42077
42510
  * Stage / unstage paths with instant local feedback and ONE cheap check.
@@ -42111,6 +42544,7 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
42111
42544
  if (dirPath === "" || !dirPath.endsWith("/")) return;
42112
42545
  if (state.untrackedDirs.has(dirPath)) {
42113
42546
  patch({ untrackedDirs: collapseDir(state.untrackedDirs, dirPath) });
42547
+ mirrorUntrackedDirs();
42114
42548
  return;
42115
42549
  }
42116
42550
  if (dirProbes.has(dirPath)) return;
@@ -42120,6 +42554,7 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
42120
42554
  try {
42121
42555
  const stdout = await probeStatusDir(target.alias, target.root, dirPath);
42122
42556
  patch({ untrackedDirs: expandDir(state.untrackedDirs, dirPath, stdout) });
42557
+ mirrorUntrackedDirs();
42123
42558
  } finally {
42124
42559
  dirProbes.delete(dirPath);
42125
42560
  }
@@ -42228,6 +42663,24 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
42228
42663
  function gitDeadlineMs() {
42229
42664
  return clientDeadlineFor(gitTimeoutMs());
42230
42665
  }
42666
+ /**
42667
+ * Hard settle deadline for ONE queued git read (`scm/read-queue.ts`).
42668
+ *
42669
+ * A read that never settles would keep its key reserved forever: later `join`
42670
+ * calls reuse the dead promise, `fresh` callers wait for it, and the busy flag
42671
+ * stays up — one hung host call would freeze that key for the rest of the page
42672
+ * (the pre-cache code could always re-read, so this is the one regression a
42673
+ * shared flight introduces). The bound is generous on purpose: the status read
42674
+ * issues its probe and then its two loaders SEQUENTIALLY, so twice the configured
42675
+ * git budget plus slack is "stuck", not "slow".
42676
+ *
42677
+ * Exported because the panels own their own queues (SCM side lists, history log)
42678
+ * and must pass the same bound — a queue without one is a permanent lock waiting
42679
+ * for a host hiccup.
42680
+ */
42681
+ function gitReadSettleMs() {
42682
+ return gitDeadlineMs() * 2 + 3e4;
42683
+ }
42231
42684
  /** Deadline for one SFTP file read (editor, diff sides, preview). */
42232
42685
  function fileReadDeadlineMs() {
42233
42686
  return clientDeadlineFor(fileReadTimeoutMs());
@@ -43962,8 +44415,8 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
43962
44415
  }),
43963
44416
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
43964
44417
  className: "rvs-build",
43965
- title: `dsh-remote-vs build 1027-1916`,
43966
- children: "1027-1916"
44418
+ title: `dsh-remote-vs build 1028-1532`,
44419
+ children: "1028-1532"
43967
44420
  }),
43968
44421
  tokenCount !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
43969
44422
  className: "rvs-build",
@@ -47009,6 +47462,52 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
47009
47462
  });
47010
47463
  }
47011
47464
  //#endregion
47465
+ //#region src/client/scm/panel-cache.ts
47466
+ /** Separator for the composite key (cannot appear in an alias or a POSIX path). */
47467
+ const KEY_SEP = "\0";
47468
+ /** How many repositories each cache keeps; the LRU trim is only a bound. */
47469
+ const ENTRY_LIMIT = 12;
47470
+ /** Cache key of one repository, as the git panel addresses it: alias + root. */
47471
+ function repoCacheKey(alias, repoRoot) {
47472
+ return `${alias}${KEY_SEP}${repoRoot}`;
47473
+ }
47474
+ /** Insert-or-touch one entry, then trim the oldest beyond the bound. */
47475
+ function remember(cache, key, value) {
47476
+ cache.delete(key);
47477
+ cache.set(key, value);
47478
+ while (cache.size > ENTRY_LIMIT) {
47479
+ const oldest = cache.keys().next();
47480
+ if (oldest.done === true) break;
47481
+ cache.delete(oldest.value);
47482
+ }
47483
+ }
47484
+ /** Read-or-touch one entry (LRU order without changing the value). */
47485
+ function recall(cache, key) {
47486
+ const value = cache.get(key);
47487
+ if (value === void 0) return void 0;
47488
+ cache.delete(key);
47489
+ cache.set(key, value);
47490
+ return value;
47491
+ }
47492
+ const extrasCache = /* @__PURE__ */ new Map();
47493
+ const historyCache = /* @__PURE__ */ new Map();
47494
+ /** The cached side lists of one repository, if it was read before. */
47495
+ function readScmExtras(key) {
47496
+ return recall(extrasCache, key);
47497
+ }
47498
+ /** Remember one repository's side lists. */
47499
+ function writeScmExtras(key, snapshot) {
47500
+ remember(extrasCache, key, snapshot);
47501
+ }
47502
+ /** The cached commit log of one repository, if it was read before. */
47503
+ function readHistorySnapshot(key) {
47504
+ return recall(historyCache, key);
47505
+ }
47506
+ /** Remember one repository's commit log. */
47507
+ function writeHistorySnapshot(key, snapshot) {
47508
+ remember(historyCache, key, snapshot);
47509
+ }
47510
+ //#endregion
47012
47511
  //#region src/client/scm/HistoryPanel.tsx
47013
47512
  /**
47014
47513
  * History view: the commit graph with branch lanes and merge curves (lane
@@ -47019,6 +47518,11 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
47019
47518
  * repository shows the newest GIT_LOG_MAX commits and is filtered client-side.
47020
47519
  * The docstring here used to promise scroll-triggered paging that does not
47021
47520
  * exist, which is worse than no comment: a reader trusts it.
47521
+ *
47522
+ * Each repository's last answer is kept in memory (`./panel-cache.ts`), so a
47523
+ * switch back to a workspace repaints its commit list at once and the log is
47524
+ * re-read behind it — the tab used to blank on every switch, and worse, kept the
47525
+ * PREVIOUS repository's rows until the new log arrived.
47022
47526
  */
47023
47527
  const ROW_H = 22;
47024
47528
  const LANE_W = 13;
@@ -47034,6 +47538,15 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
47034
47538
  "#ffa657"
47035
47539
  ];
47036
47540
  /**
47541
+ * One log read per repository at a time.
47542
+ *
47543
+ * Same contracts as the git status queue (`./read-queue.ts`): the automatic
47544
+ * read on entering the tab `join`s one in flight, and the ⟳ button asks for a
47545
+ * `fresh` one. Without it, flipping tabs while a log read runs could start a
47546
+ * second `git log` of the same repository and let the slower answer repaint.
47547
+ */
47548
+ const logReads = new ReadQueue();
47549
+ /**
47037
47550
  * Ref badge decoration for one `%D` entry. Extracted from a pair of nested
47038
47551
  * ternaries (forbidden by this project's review checklist) that were doing two
47039
47552
  * different jobs — CSS modifier and display text — inside one JSX expression.
@@ -47126,10 +47639,21 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
47126
47639
  function laneColor(lane) {
47127
47640
  return LANE_COLORS[lane % LANE_COLORS.length];
47128
47641
  }
47642
+ /** A stable empty list: the "this repository was never read" fallback. */
47643
+ const EMPTY_COMMITS = [];
47129
47644
  function HistoryPanel() {
47130
47645
  const store = useExplorer();
47131
47646
  const { t, lang, alias, repoRoot } = store;
47132
47647
  const [commits, setCommits] = (0, react.useState)([]);
47648
+ /**
47649
+ * The repository `commits` belongs to.
47650
+ *
47651
+ * A state update lands one paint AFTER the repository changes, so without this
47652
+ * key the first frame after a switch would still show the PREVIOUS repository's
47653
+ * rows — whose checkout buttons would then point those hashes at the new
47654
+ * repository. The render-time arbitration below (`rows`) closes that frame.
47655
+ */
47656
+ const [commitsKey, setCommitsKey] = (0, react.useState)("");
47133
47657
  const [loading, setLoading] = (0, react.useState)(false);
47134
47658
  const [error, setError] = (0, react.useState)(void 0);
47135
47659
  const [expanded, setExpanded] = (0, react.useState)(void 0);
@@ -47143,23 +47667,37 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
47143
47667
  * buttons that act on the currently bound repository.
47144
47668
  */
47145
47669
  const loadSeq = (0, react.useRef)(0);
47146
- const load = (0, react.useCallback)(async () => {
47670
+ const repoKey = alias === "" || repoRoot === void 0 ? "" : repoCacheKey(alias, repoRoot);
47671
+ /** The cached log of the repository being shown, when it was read before. */
47672
+ const cachedCommits = (0, react.useMemo)(() => repoKey === "" ? void 0 : readHistorySnapshot(repoKey)?.commits, [repoKey]);
47673
+ /** What THIS frame may show: rows of this repository, or none. */
47674
+ const rows = commitsKey === repoKey ? commits : cachedCommits ?? EMPTY_COMMITS;
47675
+ const load = (0, react.useCallback)(async (mode = "join") => {
47147
47676
  if (alias === "" || repoRoot === void 0) {
47148
47677
  loadSeq.current += 1;
47149
- setCommits([]);
47678
+ setCommitsKey("");
47679
+ setCommits(EMPTY_COMMITS);
47150
47680
  setLoading(false);
47151
47681
  return;
47152
47682
  }
47683
+ const key = repoCacheKey(alias, repoRoot);
47153
47684
  const ticket = ++loadSeq.current;
47685
+ const cached = readHistorySnapshot(key);
47686
+ setCommitsKey(key);
47687
+ setCommits(cached?.commits ?? EMPTY_COMMITS);
47154
47688
  setLoading(true);
47155
47689
  setError(void 0);
47156
47690
  try {
47157
- const list = await api.git.log(alias, repoRoot, 200);
47691
+ const list = await logReads.run(key, () => api.git.log(alias, repoRoot, 200), mode, gitReadSettleMs());
47692
+ writeHistorySnapshot(key, {
47693
+ commits: list,
47694
+ at: Date.now()
47695
+ });
47158
47696
  if (loadSeq.current !== ticket) return;
47697
+ setCommitsKey(key);
47159
47698
  setCommits(list);
47160
47699
  } catch (caught) {
47161
47700
  if (loadSeq.current !== ticket) return;
47162
- setCommits([]);
47163
47701
  setError(caught instanceof Error ? caught.message : String(caught));
47164
47702
  } finally {
47165
47703
  if (loadSeq.current === ticket) setLoading(false);
@@ -47169,10 +47707,10 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
47169
47707
  load();
47170
47708
  }, [load]);
47171
47709
  const visible = (0, react.useMemo)(() => {
47172
- if (filter.trim() === "") return commits;
47710
+ if (filter.trim() === "") return rows;
47173
47711
  const needle = filter.trim().toLowerCase();
47174
- return commits.filter((commit) => commit.subject.toLowerCase().includes(needle) || commit.author.toLowerCase().includes(needle) || commit.hash.startsWith(needle));
47175
- }, [commits, filter]);
47712
+ return rows.filter((commit) => commit.subject.toLowerCase().includes(needle) || commit.author.toLowerCase().includes(needle) || commit.hash.startsWith(needle));
47713
+ }, [rows, filter]);
47176
47714
  const graph = (0, react.useMemo)(() => layoutLanes(visible), [visible]);
47177
47715
  const toggleCommit = async (hash) => {
47178
47716
  if (expanded === hash) {
@@ -47207,10 +47745,11 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
47207
47745
  className: "rvs-pill",
47208
47746
  children: ["⑂ ", store.head?.detached === true ? t("scm.detached") : head]
47209
47747
  }),
47748
+ loading && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MiniSpinner, {}),
47210
47749
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconButton, {
47211
47750
  title: t("scm.refresh"),
47212
47751
  onClick: () => {
47213
- load();
47752
+ load("fresh");
47214
47753
  },
47215
47754
  disabled: loading,
47216
47755
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconRefresh, { size: 14 })
@@ -47234,7 +47773,7 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
47234
47773
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
47235
47774
  className: "rvs-scroll",
47236
47775
  children: [
47237
- loading && commits.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
47776
+ loading && rows.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
47238
47777
  className: "rvs-empty",
47239
47778
  children: [
47240
47779
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MiniSpinner, {}),
@@ -47242,7 +47781,7 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
47242
47781
  t("common.loading")
47243
47782
  ]
47244
47783
  }),
47245
- !loading && commits.length === 0 && error === void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
47784
+ !loading && rows.length === 0 && error === void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
47246
47785
  className: "rvs-empty",
47247
47786
  children: t("scm.logEmpty")
47248
47787
  }),
@@ -47697,6 +48236,20 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
47697
48236
  * and the change list never disagree.
47698
48237
  */
47699
48238
  const MSG_KEY = "dsh-remote-vs.commitMsg";
48239
+ /**
48240
+ * One extras read per repository at a time.
48241
+ *
48242
+ * The panel can ask for the same three lists twice at once — the visibility
48243
+ * effect when it comes back on screen and the ⟳ refresh — and two identical
48244
+ * `for-each-ref` / `stash list` walks of a monorepo are pure waste. Same
48245
+ * contracts as the git reads (`scm/read-queue.ts`): `join` for the automatic
48246
+ * path, `fresh` for a manual refresh.
48247
+ */
48248
+ const extrasReads = new ReadQueue();
48249
+ /** Stable empty lists: "this repository's side lists are not loaded (yet)". */
48250
+ const NO_STASHES = [];
48251
+ const NO_BRANCHES = [];
48252
+ const NO_REMOTES = [];
47700
48253
  function messageOf(error) {
47701
48254
  return error instanceof GitOpError || error instanceof Error ? error.message : String(error);
47702
48255
  }
@@ -47715,6 +48268,15 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
47715
48268
  const [stashes, setStashes] = (0, react.useState)([]);
47716
48269
  const [branches, setBranches] = (0, react.useState)([]);
47717
48270
  const [remotes, setRemotes] = (0, react.useState)([]);
48271
+ /**
48272
+ * The repository the three lists above belong to (`repoCacheKey`).
48273
+ *
48274
+ * The setState that adopts a repository's lists lands one paint AFTER the
48275
+ * switch, so a render-time merge (see `extras` below) is what keeps the
48276
+ * PREVIOUS repository's stash rows off the screen — their buttons act through
48277
+ * the CURRENT `repoRoot`, i.e. on the wrong repository.
48278
+ */
48279
+ const [listedRepoKey, setListedRepoKey] = (0, react.useState)("");
47718
48280
  const [branchOpen, setBranchOpen] = (0, react.useState)(false);
47719
48281
  const [remoteOpen, setRemoteOpen] = (0, react.useState)(false);
47720
48282
  const [newBranch, setNewBranch] = (0, react.useState)("");
@@ -47734,22 +48296,47 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
47734
48296
  localStorage.setItem(MSG_KEY, message);
47735
48297
  } catch {}
47736
48298
  }, [message]);
47737
- const loadExtras = (0, react.useCallback)(async () => {
48299
+ /**
48300
+ * The repository the extras on screen belong to (`repoCacheKey`).
48301
+ *
48302
+ * A read that is still in flight when the workspace changes must not paint its
48303
+ * answer over the new repository's lists: the panel is kept alive across
48304
+ * switches, so the response needs to be checked against where the panel is
48305
+ * NOW, not where it was when the request went out.
48306
+ */
48307
+ const extrasKeyRef = (0, react.useRef)("");
48308
+ const loadExtras = (0, react.useCallback)(async (mode = "join") => {
47738
48309
  if (alias === "" || repoRoot === void 0) {
47739
- setStashes([]);
47740
- setBranches([]);
47741
- setRemotes([]);
48310
+ extrasKeyRef.current = "";
48311
+ setListedRepoKey("");
48312
+ setStashes(NO_STASHES);
48313
+ setBranches(NO_BRANCHES);
48314
+ setRemotes(NO_REMOTES);
47742
48315
  return;
47743
48316
  }
48317
+ const repoKey = repoCacheKey(alias, repoRoot);
48318
+ extrasKeyRef.current = repoKey;
47744
48319
  try {
47745
- const [stashList, branchList, remoteList] = await Promise.all([
47746
- loadStashes(alias, repoRoot),
47747
- loadBranches(alias, repoRoot, head?.branch ?? ""),
47748
- loadRemotes(alias, repoRoot)
47749
- ]);
47750
- setStashes(stashList);
47751
- setBranches(branchList);
47752
- setRemotes(remoteList);
48320
+ const snapshot = await extrasReads.run(repoKey, async () => {
48321
+ const [stashList, branchList, remoteList] = await Promise.all([
48322
+ loadStashes(alias, repoRoot),
48323
+ loadBranches(alias, repoRoot, head?.branch ?? ""),
48324
+ loadRemotes(alias, repoRoot)
48325
+ ]);
48326
+ const answer = {
48327
+ stashes: stashList,
48328
+ branches: branchList,
48329
+ remotes: remoteList,
48330
+ at: Date.now()
48331
+ };
48332
+ writeScmExtras(repoKey, answer);
48333
+ return answer;
48334
+ }, mode, gitReadSettleMs());
48335
+ if (extrasKeyRef.current !== repoKey) return;
48336
+ setListedRepoKey(repoKey);
48337
+ setStashes(snapshot.stashes);
48338
+ setBranches(snapshot.branches);
48339
+ setRemotes(snapshot.remotes);
47753
48340
  } catch {}
47754
48341
  }, [
47755
48342
  alias,
@@ -47773,15 +48360,48 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
47773
48360
  (0, react.useEffect)(() => {
47774
48361
  if (props.visible === false || listedExtras.current === extrasKey) return;
47775
48362
  listedExtras.current = extrasKey;
47776
- loadExtras();
48363
+ const repoKey = repoRoot === void 0 ? "" : repoCacheKey(alias, repoRoot);
48364
+ extrasKeyRef.current = repoKey;
48365
+ const cached = repoKey === "" ? void 0 : readScmExtras(repoKey);
48366
+ if (cached !== void 0) {
48367
+ setListedRepoKey(repoKey);
48368
+ setStashes(cached.stashes);
48369
+ setBranches(cached.branches);
48370
+ setRemotes(cached.remotes);
48371
+ }
48372
+ loadExtras("join");
47777
48373
  }, [
47778
48374
  extrasKey,
47779
48375
  loadExtras,
47780
- props.visible
48376
+ props.visible,
48377
+ alias,
48378
+ repoRoot
47781
48379
  ]);
48380
+ /** The repository the panel is showing the side lists of. */
48381
+ const extrasRepoKey = repoRoot === void 0 ? "" : repoCacheKey(alias, repoRoot);
48382
+ /** Its cached lists, when it was read before (stable across paints). */
48383
+ const cachedExtras = (0, react.useMemo)(() => extrasRepoKey === "" ? void 0 : readScmExtras(extrasRepoKey), [extrasRepoKey]);
48384
+ /**
48385
+ * What THIS frame may show.
48386
+ *
48387
+ * A setState adopting a repository's lists lands one paint after the switch, so
48388
+ * the merge is what stops the PREVIOUS repository's stash/branch rows (whose
48389
+ * buttons run against the CURRENT `repoRoot`) from ever appearing under the
48390
+ * new repository. This repository's cache, when present, fills that frame;
48391
+ * otherwise the sections are simply empty until its own read adopts them.
48392
+ */
48393
+ const extras = listedRepoKey === extrasRepoKey ? {
48394
+ stashes,
48395
+ branches,
48396
+ remotes
48397
+ } : cachedExtras ?? {
48398
+ stashes: NO_STASHES,
48399
+ branches: NO_BRANCHES,
48400
+ remotes: NO_REMOTES
48401
+ };
47782
48402
  const refreshAll = (0, react.useCallback)(async () => {
47783
48403
  await store.afterWrite();
47784
- await loadExtras();
48404
+ await loadExtras("fresh");
47785
48405
  }, [store, loadExtras]);
47786
48406
  /**
47787
48407
  * How much of the repository a verb's success invalidates.
@@ -48079,6 +48699,11 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
48079
48699
  children: t("scm.title")
48080
48700
  }),
48081
48701
  store.gitBusy && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MiniSpinner, {}),
48702
+ store.gitCached && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
48703
+ className: "rvs-pill rvs-pill-cached",
48704
+ title: t("scm.cachedHint"),
48705
+ children: store.gitBusy ? t("scm.cachedRefreshing") : t("scm.cached")
48706
+ }),
48082
48707
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconButton, {
48083
48708
  title: t("scm.refresh"),
48084
48709
  onClick: () => {
@@ -48129,6 +48754,14 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
48129
48754
  store.setRepoSubdir(subdir).then(() => toast(t("scm.repoDirSwitched")));
48130
48755
  }
48131
48756
  }),
48757
+ repoRoot !== void 0 && store.gitError !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
48758
+ className: "rvs-banner error",
48759
+ children: [
48760
+ t("scm.refreshFailed"),
48761
+ " ",
48762
+ store.gitError
48763
+ ]
48764
+ }),
48132
48765
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
48133
48766
  className: "rvs-scroll",
48134
48767
  children: [repoRoot === void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -48239,7 +48872,7 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
48239
48872
  },
48240
48873
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconClose, { size: 12 })
48241
48874
  }), () => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { children: staged.map((entry) => changeRow(entry, true)) })),
48242
- section("stash", t("scm.stashes"), stashes.length, /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconButton, {
48875
+ section("stash", t("scm.stashes"), extras.stashes.length, /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconButton, {
48243
48876
  title: t("scm.stashSave"),
48244
48877
  disabled: busy !== void 0,
48245
48878
  onClick: () => {
@@ -48250,13 +48883,13 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
48250
48883
  title: t("scm.refresh"),
48251
48884
  disabled: busy !== void 0,
48252
48885
  onClick: () => {
48253
- loadExtras();
48886
+ loadExtras("fresh");
48254
48887
  },
48255
48888
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconRefresh, { size: 13 })
48256
- })] }), () => stashes.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
48889
+ })] }), () => extras.stashes.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
48257
48890
  className: "rvs-empty",
48258
48891
  children: t("scm.noStash")
48259
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { children: stashes.map((item) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
48892
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { children: extras.stashes.map((item) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
48260
48893
  className: "rvs-scm-item",
48261
48894
  style: { cursor: "default" },
48262
48895
  title: `${item.ref} ${fullTime(item.date)}`,
@@ -48445,7 +49078,7 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
48445
49078
  maxHeight: 200,
48446
49079
  overflow: "auto"
48447
49080
  },
48448
- children: branches.map((branch) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
49081
+ children: extras.branches.map((branch) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
48449
49082
  className: "rvs-scm-item",
48450
49083
  style: {
48451
49084
  cursor: "default",
@@ -48561,7 +49194,7 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
48561
49194
  })
48562
49195
  ]
48563
49196
  }),
48564
- remotes.map((remote) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
49197
+ extras.remotes.map((remote) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
48565
49198
  className: "rvs-scm-item",
48566
49199
  style: {
48567
49200
  cursor: "default",
@@ -63927,7 +64560,8 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
63927
64560
  function SettingsHostsSection(_props) {
63928
64561
  const [t] = (0, react.useState)(() => makeT(detectLang()));
63929
64562
  const [hosts, setHosts] = (0, react.useState)([]);
63930
- const [mode, setMode] = (0, react.useState)("off");
64563
+ /** Pre-load placeholder; matches the plugin's default (nforce) until the host answers. */
64564
+ const [mode, setMode] = (0, react.useState)("enforce");
63931
64565
  const [version, setVersion] = (0, react.useState)("");
63932
64566
  const [saving, setSaving] = (0, react.useState)(false);
63933
64567
  const [error, setError] = (0, react.useState)(void 0);