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/README.en.md +15 -4
- package/README.md +13 -3
- package/cordis.patch.yml +10 -5
- package/lib/client.js +728 -94
- package/lib/index.js +2 -2
- package/lib/types/client/composer.d.ts +5 -1
- package/lib/types/client/scm/HistoryPanel.d.ts +5 -0
- package/lib/types/client/scm/actions.d.ts +25 -0
- package/lib/types/client/scm/git-cache.d.ts +93 -0
- package/lib/types/client/scm/panel-cache.d.ts +43 -0
- package/lib/types/client/scm/read-queue.d.ts +74 -0
- package/lib/types/client/store.d.ts +66 -3
- package/lib/types/git-ops.d.ts +38 -0
- package/lib/types/index.d.ts +5 -0
- package/package.json +37 -37
- package/src/client/SettingsHosts.tsx +2 -1
- package/src/client/composer.ts +11 -2
- package/src/client/i18n.ts +10 -2
- package/src/client/scm/HistoryPanel.tsx +61 -11
- package/src/client/scm/ScmPanel.tsx +123 -22
- package/src/client/scm/actions.ts +49 -6
- package/src/client/scm/git-cache.ts +159 -0
- package/src/client/scm/panel-cache.ts +93 -0
- package/src/client/scm/read-queue.ts +151 -0
- package/src/client/store.tsx +360 -60
- package/src/client/vsc/vsc.css +10 -0
- package/src/git-ops.ts +60 -0
- package/src/index.ts +10 -2
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
|
|
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
|
-
|
|
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: "
|
|
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
|
-
*
|
|
41595
|
-
*
|
|
41596
|
-
*
|
|
41597
|
-
*
|
|
41598
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
41959
|
-
|
|
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
|
-
/**
|
|
41972
|
-
|
|
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
|
|
41976
|
-
const
|
|
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,
|
|
41981
|
-
const
|
|
41982
|
-
if (
|
|
41983
|
-
if (
|
|
41984
|
-
|
|
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
|
-
|
|
42391
|
+
repoSubdir: subdir,
|
|
41987
42392
|
status: EMPTY_STATUS,
|
|
41988
42393
|
head: void 0,
|
|
41989
|
-
|
|
41990
|
-
|
|
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
|
-
|
|
41996
|
-
|
|
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
|
-
|
|
42414
|
+
const snapshot = {
|
|
42007
42415
|
repoRoot: root,
|
|
42008
42416
|
repoSubdir: settled,
|
|
42009
|
-
repoMissing: false,
|
|
42010
42417
|
status,
|
|
42011
42418
|
head,
|
|
42012
|
-
|
|
42013
|
-
|
|
42014
|
-
|
|
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 (
|
|
42427
|
+
if (!gitReadIsCurrent(bindingId, ticket)) return;
|
|
42018
42428
|
patch({
|
|
42019
|
-
|
|
42020
|
-
|
|
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
|
|
42497
|
+
const nextDirs = pruneDirExpansions(staged ? dropExpandedRows(state.untrackedDirs, paths) : state.untrackedDirs, next);
|
|
42071
42498
|
patch({
|
|
42072
42499
|
status: next,
|
|
42073
|
-
untrackedDirs:
|
|
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
|
|
43966
|
-
children: "
|
|
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
|
|
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
|
-
|
|
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
|
|
47710
|
+
if (filter.trim() === "") return rows;
|
|
47173
47711
|
const needle = filter.trim().toLowerCase();
|
|
47174
|
-
return
|
|
47175
|
-
}, [
|
|
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 &&
|
|
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 &&
|
|
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
|
-
|
|
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
|
-
|
|
47740
|
-
|
|
47741
|
-
|
|
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
|
|
47746
|
-
|
|
47747
|
-
|
|
47748
|
-
|
|
47749
|
-
|
|
47750
|
-
|
|
47751
|
-
|
|
47752
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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);
|