dsh-remote-vs 0.8.9 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -11,7 +11,7 @@ window.__ModuleLoader__.load({
11
11
  let react_jsx_runtime = require("react/jsx-runtime");
12
12
  let react_dom = require("react-dom");
13
13
  //#region \0dsh-css:E:\work_base\dsh_remote_vs\src\client\vsc\vsc.css.mjs
14
- const css$3 = "/**\n * VS Code-style design system for the remote explorer. Every surface below\n * is scoped under `.rvs-root` so the sheet can never leak into the host GUI,\n * and every host token carries a literal fallback: the plugin also renders in\n * the native right-Sidebar fallback and on hosts whose alias tokens differ.\n *\n * Palette and metrics are aligned with the reference implementation\n * (dsh-solution-explorer): 13px/20px panel type, 5px/10px tree rows with a\n * 10px radius, a 30px activity button with the badge tucked into the icon's\n * bottom-right corner, GitHub-Dark token colors, and the\n * `--e2b714 / #4ec9b0 / #f14c4c` modified/added/deleted triple.\n */\n\n.rvs-root {\n --rvs-fg: var(--dsw-alias-label-primary, #e6e6e6);\n --rvs-fg2: var(--dsw-alias-label-secondary, #b9b9b9);\n --rvs-fg3: var(--dsw-alias-label-tertiary, #8b8b8b);\n --rvs-hover: var(--dsw-alias-interactive-bg-hover, rgba(127, 127, 127, .14));\n --rvs-active: var(--dsw-alias-interactive-bg-active, rgba(77, 107, 254, .22));\n --rvs-border: var(--dsw-alias-border-l1, rgba(127, 127, 127, .25));\n --rvs-border2: var(--dsw-alias-border-l2, rgba(127, 127, 127, .38));\n --rvs-input: var(--dsw-alias-bg-input, rgba(127, 127, 127, .12));\n --rvs-overlay: var(--dsw-alias-bg-overlay, #24262e);\n --rvs-layer3: var(--dsw-alias-bg-layer-3, rgba(127, 127, 127, .16));\n --rvs-brand: var(--dsw-alias-brand-primary, #4d6bfe);\n --rvs-accent: var(--dsw-alias-button-info-fill, #4d6bfe);\n --rvs-danger: var(--dsw-alias-state-error-primary, #e05555);\n --rvs-git-m: #e2b714;\n --rvs-git-a: #4ec9b0;\n --rvs-git-d: #f14c4c;\n --rvs-git-r: #4ec9b0;\n --rvs-git-c: #58a6ff;\n --rvs-git-u: #f14c4c;\n --rvs-git-x: #6e6e6e;\n --rvs-add-bg: rgba(78, 201, 176, .15);\n --rvs-del-bg: rgba(241, 76, 76, .15);\n /* Opaque equivalents, for elements that must not show what scrolls under\n them (the pinned diff line numbers). */\n --rvs-add-solid: #e4f6f1;\n --rvs-del-solid: #fdeaea;\n --rvs-mono: 'Cascadia Code', 'Fira Code', 'JetBrains Mono', Consolas, 'Courier New', monospace;\n /* Opaque code surfaces (Light+ default; the dark block below flips them). */\n --rvs-code-bg: #ffffff;\n --rvs-code-fg: #1f1f1f;\n --rvs-code-gutter-fg: #237893;\n\n display: flex;\n flex-direction: row;\n height: 100%;\n min-height: 0;\n overflow: hidden;\n color: var(--rvs-fg);\n font-size: 13px;\n line-height: 20px;\n}\n\n/**\n * Code surfaces are OPAQUE, in the editor's own palette rather than the DSH\n * skin: syntax colors need a definite background to stay readable, and the\n * app shell marks its resolved scheme on <body> (same signal theme.ts reads).\n */\nbody[data-ds-dark-theme] .rvs-root {\n --rvs-code-bg: #1e1e1e;\n --rvs-code-fg: #d4d4d4;\n --rvs-code-gutter-fg: #858585;\n --rvs-add-solid: #16332e;\n --rvs-del-solid: #3b2325;\n}\n\n.rvs-edge {\n background: var(--rvs-code-bg);\n color: var(--rvs-code-fg);\n}\n\n.rvs-root *,\n.rvs-portal * {\n box-sizing: border-box;\n}\n\n/* ------------------------------------------------------------ activity rail */\n\n.rvs-rail {\n flex: none;\n width: 38px;\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 4px;\n padding: 8px 0;\n border-right: 1px solid var(--rvs-border);\n background: var(--dsw-specific-sidebar-fill, transparent);\n}\n\n.rvs-rail-btn {\n position: relative;\n flex: none;\n width: 30px;\n height: 30px;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 0;\n border: none;\n border-radius: 6px;\n background: transparent;\n color: var(--rvs-fg3);\n cursor: pointer;\n}\n\n.rvs-rail-btn:hover {\n color: var(--rvs-fg);\n background: var(--rvs-hover);\n}\n\n.rvs-rail-btn.active {\n color: var(--rvs-fg);\n background: var(--rvs-hover);\n box-shadow: inset 2px 0 0 var(--rvs-brand);\n}\n\n.rvs-rail-btn.danger:hover {\n color: var(--rvs-danger);\n}\n\n.rvs-rail-badge {\n position: absolute;\n bottom: 1px;\n right: 1px;\n min-width: 14px;\n height: 14px;\n padding: 0 3px;\n border-radius: 7px;\n background: var(--rvs-accent);\n color: #fff;\n font-size: 9px;\n font-weight: 700;\n line-height: 14px;\n text-align: center;\n pointer-events: none;\n}\n\n.rvs-rail-sep {\n flex: none;\n width: 20px;\n height: 1px;\n margin: 4px 0;\n background: var(--rvs-border);\n}\n\n/* -------------------------------------------------------- panel + editor */\n\n.rvs-column {\n flex: 1;\n min-width: 0;\n min-height: 0;\n display: flex;\n flex-direction: column;\n}\n\n.rvs-pane {\n display: flex;\n flex-direction: column;\n min-height: 0;\n min-width: 0;\n overflow: hidden;\n}\n\n/**\n * One lazily-mounted panel slot: a visited panel stays mounted and is merely\n * hidden, so returning to the explorer (or the terminal) neither refetches nor\n * flashes a reload.\n */\n.rvs-viewslot {\n flex: 1;\n min-height: 0;\n min-width: 0;\n flex-direction: column;\n height: 100%;\n overflow: hidden;\n}\n\n/**\n * Padding for the legacy views (workspaces / terminal) that draw no chrome.\n *\n * It MUST be a flex column: the workspaces view scroller is its child and gets\n * its bounded height only from a flex parent (`flex:1; min-height:0`). As a\n * plain block it grew with its content instead, so the panel clipped at the\n * viewslot's `overflow:hidden` and the binding list could not scroll at all —\n * a seventh workspace (and the wizard under it) was simply unreachable.\n */\n.rvs-viewpad {\n display: flex;\n flex-direction: column;\n min-height: 0;\n padding: 10px 12px 14px;\n box-sizing: border-box;\n}\n\n.rvs-pane.grow {\n flex: 1;\n}\n\n.rvs-header {\n flex: none;\n display: flex;\n align-items: center;\n gap: 6px;\n height: 34px;\n padding: 0 8px 0 10px;\n border-bottom: 1px solid var(--rvs-border);\n}\n\n.rvs-header .rvs-title {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n font-size: 11px;\n font-weight: 700;\n letter-spacing: .04em;\n text-transform: uppercase;\n color: var(--rvs-fg3);\n}\n\n.rvs-hbtn {\n flex: none;\n width: 22px;\n height: 22px;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 0;\n border: none;\n border-radius: 5px;\n background: transparent;\n color: var(--rvs-fg3);\n cursor: pointer;\n}\n\n.rvs-hbtn:hover {\n color: var(--rvs-fg);\n background: var(--rvs-hover);\n}\n\n.rvs-hbtn.danger:hover {\n color: #fff;\n background: var(--rvs-danger);\n}\n\n.rvs-hbtn:disabled {\n opacity: .4;\n cursor: default;\n}\n\n.rvs-scroll {\n flex: 1;\n min-height: 0;\n overflow: auto;\n scrollbar-width: thin;\n}\n\n.rvs-globalbar {\n flex: none;\n display: flex;\n align-items: center;\n gap: 6px;\n padding: 4px 8px;\n border-bottom: 1px solid var(--rvs-border);\n font-size: 11px;\n}\n\n.rvs-scroll::-webkit-scrollbar {\n width: 8px;\n height: 8px;\n}\n\n.rvs-scroll::-webkit-scrollbar-thumb {\n background: var(--dsw-alias-scrollbar-bg-l2, rgba(127, 127, 127, .4));\n border-radius: 4px;\n}\n\n.rvs-scroll::-webkit-scrollbar-thumb:hover {\n background: var(--dsw-alias-scrollbar-hover-l2, rgba(127, 127, 127, .6));\n}\n\n.rvs-scroll::-webkit-scrollbar-track {\n background: transparent;\n}\n\n.rvs-vsplit-handle {\n flex: none;\n position: relative;\n height: 7px;\n cursor: row-resize;\n touch-action: none;\n}\n\n.rvs-vsplit-handle::after {\n content: '';\n position: absolute;\n left: 0;\n right: 0;\n top: 3px;\n height: 1px;\n background: var(--rvs-border);\n}\n\n.rvs-vsplit-handle:hover::after,\n.rvs-vsplit-handle[data-dragging='true']::after {\n top: 2px;\n height: 3px;\n border-radius: 2px;\n background: var(--rvs-brand);\n}\n\n/* ------------------------------------------------------------------ tree */\n\n.rvs-tree {\n padding: 4px 6px 12px;\n min-width: 0;\n}\n\n.rvs-node {\n display: flex;\n align-items: center;\n gap: 6px;\n min-width: 0;\n width: 100%;\n padding: 4px 8px;\n border: none;\n background: transparent;\n border-radius: 8px;\n color: var(--rvs-fg);\n font: inherit;\n font-size: 13px;\n line-height: 18px;\n text-align: left;\n cursor: pointer;\n}\n\n.rvs-node:hover {\n background: var(--rvs-hover);\n}\n\n.rvs-node.selected {\n background: var(--rvs-active);\n}\n\n.rvs-node.cut {\n opacity: .5;\n}\n\n.rvs-node.drop-target {\n background: var(--rvs-active);\n outline: 1px solid var(--rvs-brand);\n outline-offset: -1px;\n}\n\n/* Where a file drag will land: a slip must not silently drop a tree into the\n wrong directory. */\n.rvs-drop-hint {\n margin: 2px 8px 4px;\n padding: 3px 8px;\n border: 1px dashed var(--rvs-brand);\n border-radius: 3px;\n font-size: 11px;\n color: var(--rvs-brand);\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n direction: rtl;\n unicode-bidi: plaintext;\n}\n/* Upload progress: one line under the header. A multi-minute transfer with no\n feedback is indistinguishable from a hung explorer, so the row names the\n count and the file on the wire. */\n.rvs-upload-bar {\n flex: none;\n display: flex;\n align-items: center;\n gap: 6px;\n padding: 3px 10px 4px;\n font-size: 11px;\n color: var(--dsw-alias-text-secondary, rgba(127, 127, 127, .95));\n}\n\n.rvs-upload-text {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n direction: rtl;\n unicode-bidi: plaintext;\n}\n\n.rvs-node-icon {\n flex: none;\n width: 16px;\n height: 16px;\n display: flex;\n align-items: center;\n justify-content: center;\n color: var(--rvs-fg3);\n}\n\n.rvs-node-name {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.rvs-node-actions {\n flex: none;\n display: none;\n align-items: center;\n gap: 2px;\n}\n\n.rvs-node:hover .rvs-node-actions {\n display: flex;\n}\n\n.rvs-git-letter {\n flex: none;\n width: 14px;\n margin-left: 2px;\n font-size: 11px;\n font-weight: 700;\n text-align: center;\n}\n\n.rvs-git-M, .rvs-letter-M { color: var(--rvs-git-m); }\n.rvs-git-A, .rvs-letter-A { color: var(--rvs-git-a); }\n.rvs-git-D, .rvs-letter-D { color: var(--rvs-git-d); }\n.rvs-git-R, .rvs-letter-R { color: var(--rvs-git-r); }\n.rvs-git-C, .rvs-letter-C { color: var(--rvs-git-c); }\n.rvs-git-U, .rvs-letter-U { color: var(--rvs-git-u); }\n.rvs-git-q, .rvs-letter-q, .rvs-git-\\\\? { color: var(--rvs-git-a); }\n.rvs-git-x, .rvs-letter-x { color: var(--rvs-git-x); }\n.rvs-git-T, .rvs-letter-T { color: var(--rvs-git-m); }\n\n.rvs-rename-input {\n flex: 1;\n min-width: 0;\n padding: 0 4px;\n border: 1px solid var(--rvs-brand);\n border-radius: 4px;\n background: var(--rvs-input);\n color: var(--rvs-fg);\n font: inherit;\n font-size: 13px;\n line-height: 18px;\n outline: none;\n}\n\n.rvs-empty {\n padding: 18px 14px;\n color: var(--rvs-fg3);\n font-size: 12px;\n text-align: center;\n line-height: 1.7;\n}\n\n.rvs-banner {\n margin: 6px 8px;\n padding: 6px 9px;\n border-radius: 6px;\n font-size: 12px;\n line-height: 1.5;\n word-break: break-word;\n white-space: pre-wrap;\n}\n\n.rvs-banner.info {\n background: var(--rvs-hover);\n color: var(--rvs-fg2);\n}\n\n.rvs-banner.error {\n background: rgba(200, 60, 60, .16);\n color: var(--rvs-danger);\n}\n\n.rvs-banner.warn {\n background: rgba(226, 183, 20, .14);\n color: var(--rvs-git-m);\n}\n\n/* --------------------------------------------------------- source control */\n\n.rvs-scm-section {\n border-bottom: 1px solid var(--rvs-border);\n}\n\n.rvs-scm-head {\n display: flex;\n align-items: center;\n gap: 4px;\n height: 28px;\n padding: 0 6px 0 4px;\n cursor: pointer;\n user-select: none;\n font-size: 11px;\n font-weight: 700;\n letter-spacing: .04em;\n text-transform: uppercase;\n color: var(--rvs-fg2);\n}\n\n.rvs-scm-head:hover {\n background: var(--rvs-hover);\n}\n\n.rvs-scm-head .rvs-count {\n margin-left: 6px;\n font-size: 11px;\n font-weight: 600;\n letter-spacing: 0;\n color: var(--rvs-fg3);\n}\n\n.rvs-scm-head-actions {\n margin-left: auto;\n display: flex;\n align-items: center;\n gap: 2px;\n}\n\n.rvs-scm-section.collapsed > *:not(.rvs-scm-head) {\n display: none !important;\n}\n\n.rvs-chevron {\n flex: none;\n transition: transform .12s ease;\n}\n\n.rvs-chevron.open {\n transform: rotate(90deg);\n}\n\n.rvs-scm-item {\n display: flex;\n align-items: center;\n gap: 6px;\n width: 100%;\n padding: 3px 8px 3px 22px;\n border: none;\n background: transparent;\n color: var(--rvs-fg);\n font: inherit;\n font-size: 12.5px;\n line-height: 19px;\n text-align: left;\n cursor: pointer;\n}\n\n.rvs-scm-item:hover {\n background: var(--rvs-hover);\n}\n\n.rvs-scm-item.selected {\n background: var(--rvs-active);\n}\n\n.rvs-scm-path {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.rvs-scm-dir {\n flex: none;\n color: var(--rvs-fg3);\n}\n\n.rvs-scm-actions {\n flex: none;\n display: none;\n align-items: center;\n gap: 1px;\n}\n\n.rvs-scm-item:hover .rvs-scm-actions {\n display: flex;\n}\n\n.rvs-commit-box {\n flex: none;\n padding: 8px 10px;\n border-bottom: 1px solid var(--rvs-border);\n}\n\n.rvs-commit-input {\n width: 100%;\n min-height: 58px;\n max-height: 180px;\n padding: 6px 8px;\n border: 1px solid var(--rvs-border2);\n border-radius: 8px;\n background: var(--rvs-input);\n color: var(--rvs-fg);\n font: inherit;\n font-size: 12.5px;\n line-height: 18px;\n resize: vertical;\n outline: none;\n}\n\n.rvs-commit-input:focus {\n border-color: var(--rvs-brand);\n}\n\n.rvs-commit-row {\n display: flex;\n gap: 6px;\n margin-top: 6px;\n}\n\n.rvs-commit-btn {\n flex: 1;\n padding: 5px 8px;\n border: none;\n border-radius: 6px;\n background: var(--rvs-accent);\n color: #fff;\n font: inherit;\n font-size: 12.5px;\n font-weight: 600;\n cursor: pointer;\n}\n\n.rvs-commit-btn:disabled {\n opacity: .45;\n cursor: default;\n}\n\n.rvs-repo-item {\n display: flex;\n align-items: center;\n gap: 6px;\n padding: 5px 10px 5px 12px;\n cursor: pointer;\n font-size: 12.5px;\n}\n\n.rvs-repo-item:hover {\n background: var(--rvs-hover);\n}\n\n.rvs-repo-item.active {\n background: var(--rvs-active);\n}\n\n.rvs-repo-name {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.rvs-repo-branch {\n flex: none;\n font-size: 11px;\n color: var(--rvs-fg3);\n}\n\n.rvs-pill {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n padding: 1px 8px;\n border-radius: 999px;\n background: var(--rvs-hover);\n font-size: 11px;\n font-weight: 600;\n color: var(--rvs-fg);\n}\n\n.rvs-subpanel {\n margin: 6px 8px;\n padding: 8px;\n border: 1px solid var(--rvs-border2);\n border-radius: 8px;\n font-size: 12px;\n}\n\n.rvs-subpanel-head {\n display: flex;\n align-items: center;\n gap: 6px;\n margin-bottom: 6px;\n color: var(--rvs-fg2);\n font-weight: 600;\n}\n\n.rvs-chip {\n flex: none;\n padding: 2px 8px;\n border: 1px solid var(--rvs-border2);\n border-radius: 6px;\n background: transparent;\n color: var(--rvs-fg);\n font: inherit;\n font-size: 11.5px;\n cursor: pointer;\n}\n\n.rvs-chip:hover {\n background: var(--rvs-hover);\n}\n\n.rvs-chip.primary {\n background: var(--rvs-accent);\n border-color: transparent;\n color: #fff;\n}\n\n.rvs-chip.danger:hover {\n background: var(--rvs-danger);\n border-color: transparent;\n color: #fff;\n}\n\n.rvs-input {\n flex: 1;\n min-width: 0;\n padding: 4px 8px;\n border: 1px solid var(--rvs-border2);\n border-radius: 6px;\n background: var(--rvs-input);\n color: var(--rvs-fg);\n font: inherit;\n font-size: 12px;\n outline: none;\n}\n\n.rvs-input:focus {\n border-color: var(--rvs-brand);\n}\n\n.rvs-select {\n flex: 1;\n min-width: 0;\n padding: 3px 6px;\n border: 1px solid var(--rvs-border2);\n border-radius: 6px;\n background: var(--rvs-input);\n color: var(--rvs-fg);\n font: inherit;\n font-size: 12px;\n outline: none;\n color-scheme: light dark;\n}\n\n.rvs-select option {\n background: Canvas;\n color: CanvasText;\n}\n\n/* ------------------------------------------------- conversation-view mount */\n\n/**\n * Bound the center surface to the height the host gave the slot.\n *\n * `flex: 1 1 0` is the load-bearing declaration, and BOTH halves of it matter.\n * The host's own slot rules are:\n *\n * .wSkVaW_viewArea { flex: 1; min-height: 0; overflow: hidden }\n * [data-phase=active] .viewArea { flex: 1 0 auto; min-height: auto }\n * .wSkVaW_root { height: 100%; position: relative }\n *\n * While a conversation is active the slot's flex-basis is `auto`, so the slot's\n * height is driven by its child's INTRINSIC size. A full-height editor then\n * pushed it to 5510px for a 300-line file, the outer scroller took over, and\n * the tab strip and file info bar scrolled off the top with the code.\n *\n * `flex-basis: 0` breaks that cycle: the item stops contributing its content\n * height, so the slot stays the size the host intended and `.cm-scroller`\n * becomes the only scroller. Measured in the fixture (.spike/layout-fixture.html,\n * which mirrors the slot rules above) — only the bottom row fits:\n *\n * flex: 1 1 0 + min-height: 0 -> 736px OK\n * flex: 1 1 auto (i.e. `flex: 1`) -> 5510px broken\n * height: 0 + min-height: 100% -> 5510px broken\n * max-height: 100% -> 5510px broken\n * height: 100vh -> 5510px broken\n *\n * `height: 100%` is deliberately NOT set here any more. It resolves to `auto`\n * against a slot whose height is not definite — it reads like the fix but\n * measures as a no-op, which is how this regressed once already.\n *\n * NOT `position: absolute` either — that was another failed attempt: `.viewArea`\n * is not itself positioned, so `inset: 0` escaped to the host's `.wSkVaW_root`\n * (which is `position: relative`) and the plugin's global bar floated on top of\n * the host's 对话 / 轨迹 / 远程编辑器 tab row.\n *\n * The sidebar's `.rvs-viewslot` is deliberately not covered: it has a bounding\n * parent and this bug is specific to the conversation slot's `min-height: auto`\n * phase.\n */\n.rvs-center {\n flex: 1 1 0;\n display: flex;\n flex-direction: column;\n min-height: 0;\n min-width: 0;\n overflow: hidden;\n}\n\n/* ---------------------------------------------------------------- editor */\n\n.rvs-etabs {\n flex: none;\n display: flex;\n align-items: stretch;\n gap: 2px;\n height: 32px;\n padding: 0 4px;\n overflow-x: auto;\n overflow-y: hidden;\n border-bottom: 1px solid var(--rvs-border);\n scrollbar-width: none;\n}\n\n.rvs-etabs::-webkit-scrollbar {\n height: 0;\n}\n\n.rvs-etab {\n flex: none;\n display: flex;\n align-items: center;\n gap: 5px;\n max-width: 190px;\n margin: 4px 0;\n padding: 0 4px 0 8px;\n border: none;\n border-radius: 6px;\n background: transparent;\n color: var(--rvs-fg3);\n font: inherit;\n font-size: 12px;\n cursor: pointer;\n}\n\n.rvs-etab:hover {\n background: var(--rvs-hover);\n color: var(--rvs-fg);\n}\n\n.rvs-etab.active {\n background: var(--rvs-layer3);\n color: var(--rvs-fg);\n}\n\n.rvs-etab-name {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.rvs-etab-dot {\n flex: none;\n font-size: 9px;\n color: var(--rvs-git-m);\n}\n\n.rvs-etab-close {\n flex: none;\n width: 16px;\n height: 16px;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 0;\n border: none;\n border-radius: 4px;\n background: transparent;\n color: inherit;\n cursor: pointer;\n opacity: .65;\n}\n\n.rvs-etab-close:hover {\n background: var(--rvs-hover);\n opacity: 1;\n}\n\n.rvs-einfo {\n flex: none;\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 2px 8px 2px 10px;\n border-bottom: 1px solid var(--rvs-border);\n font-size: 11px;\n line-height: 18px;\n color: var(--rvs-fg3);\n}\n\n.rvs-einfo-path {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n /* Truncate from the LEFT (keep the tail) without reordering the text:\n `plaintext` gives the content its own LTR base direction, which is what\n stops a leading \"/\" from rendering as a trailing one. */\n direction: rtl;\n unicode-bidi: plaintext;\n text-align: left;\n}\n\n.rvs-einfo-spacer {\n margin-left: auto;\n}\n\n.rvs-status.dirty { color: var(--rvs-git-m); }\n.rvs-status.saved { color: var(--rvs-git-a); }\n.rvs-status.saving { color: var(--rvs-fg2); }\n\n.rvs-body {\n flex: 1;\n min-height: 0;\n min-width: 0;\n display: flex;\n flex-direction: column;\n overflow: hidden;\n /* Anchors portalled overlays (the find widget) outside every scroller. */\n position: relative;\n background: var(--rvs-code-bg);\n color: var(--rvs-code-fg);\n}\n\n.rvs-editor-host {\n flex: 1;\n min-height: 0;\n overflow: hidden;\n background: var(--rvs-code-bg);\n}\n\n/* Keep a panel mounted but out of layout (and out of the a11y tree). Used by\n the editor host in preview mode: unmounting it destroyed the buffer and the\n undo stack, hiding it preserves both. */\n.rvs-hidden {\n display: none !important;\n}\n\n/* Find-match marks. Painted by our own StateField (`findMatchHighlight`), and\n deliberately the SAME colours as the `.cm-searchMatch` rules below so the\n two paths cannot drift apart. */\n.rvs-find-match {\n background-color: rgba(226, 183, 20, .30);\n outline: 1px solid rgba(226, 183, 20, .55);\n border-radius: 2px;\n}\n\n.rvs-find-match.current {\n background-color: rgba(226, 183, 20, .48);\n}\n\n/* ------------------------------------------------------- preview surfaces */\n\n.rvs-preview-img {\n flex: 1;\n min-height: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n overflow: auto;\n background-color: var(--rvs-code-bg);\n background-image:\n linear-gradient(45deg, rgba(127, 127, 127, .08) 25%, transparent 25%),\n linear-gradient(-45deg, rgba(127, 127, 127, .08) 25%, transparent 25%),\n linear-gradient(45deg, transparent 75%, rgba(127, 127, 127, .08) 75%),\n linear-gradient(-45deg, transparent 75%, rgba(127, 127, 127, .08) 75%);\n background-size: 20px 20px, 20px 20px, 20px 20px, 20px 20px;\n background-position: 0 0, 0 10px, 10px -10px, -10px 0;\n}\n\n.rvs-preview-img img {\n max-width: 100%;\n max-height: 100%;\n object-fit: contain;\n image-rendering: auto;\n}\n\n.rvs-markdown {\n flex: 1;\n min-height: 0;\n overflow: auto;\n padding: 12px 14px 24px;\n font-size: 13px;\n line-height: 1.7;\n color: var(--rvs-code-fg);\n background: var(--rvs-code-bg);\n user-select: text;\n}\n\n.rvs-markdown h1, .rvs-markdown h2, .rvs-markdown h3, .rvs-markdown h4 {\n margin: 14px 0 6px;\n line-height: 1.35;\n}\n\n.rvs-markdown h1 { font-size: 19px; border-bottom: 1px solid var(--rvs-border); padding-bottom: 5px; }\n.rvs-markdown h2 { font-size: 16px; border-bottom: 1px solid var(--rvs-border); padding-bottom: 4px; }\n.rvs-markdown h3 { font-size: 14px; }\n.rvs-markdown p { margin: 6px 0; }\n.rvs-markdown ul, .rvs-markdown ol { margin: 6px 0; padding-left: 22px; }\n.rvs-markdown li { margin: 2px 0; }\n.rvs-markdown code {\n padding: 1px 5px;\n border-radius: 4px;\n background: var(--rvs-hover);\n font-family: var(--rvs-mono);\n font-size: 12px;\n}\n.rvs-markdown pre {\n margin: 8px 0;\n padding: 9px 11px;\n border-radius: 8px;\n background: var(--rvs-input);\n overflow-x: auto;\n}\n.rvs-markdown pre code { padding: 0; background: transparent; }\n.rvs-markdown blockquote {\n margin: 8px 0;\n padding: 2px 12px;\n border-left: 3px solid var(--rvs-border2);\n color: var(--rvs-fg2);\n}\n.rvs-markdown a { color: var(--dsw-alias-label-link, #58a6ff); text-decoration: none; }\n.rvs-markdown a:hover { text-decoration: underline; }\n.rvs-markdown table { border-collapse: collapse; margin: 8px 0; font-size: 12px; }\n.rvs-markdown th, .rvs-markdown td { border: 1px solid var(--rvs-border2); padding: 4px 8px; }\n.rvs-markdown hr { border: none; border-top: 1px solid var(--rvs-border); margin: 12px 0; }\n\n.rvs-html-frame {\n flex: 1;\n min-height: 0;\n width: 100%;\n border: none;\n background: #fff;\n}\n\n/* ------------------------------------------------------------------ diff */\n\n.rvs-diff {\n flex: 1;\n min-height: 0;\n display: flex;\n flex-direction: column;\n overflow: hidden;\n}\n\n.rvs-diff-toolbar {\n flex: none;\n display: flex;\n align-items: center;\n gap: 6px;\n padding: 3px 8px;\n border-bottom: 1px solid var(--rvs-border);\n font-size: 11px;\n color: var(--rvs-fg3);\n}\n\n/**\n * Prev/next change, in the toolbar's free space.\n *\n * Two chevrons around a tabular counter: the pair then reads as \"jump to change\n * n of N\" instead of two anonymous arrows, and the counter doubles as the \"where\n * am I\" cue while scrolling by hand.\n */\n.rvs-hunknav {\n display: inline-flex;\n align-items: center;\n gap: 1px;\n margin-right: 2px;\n}\n\n.rvs-hunknav-pos {\n min-width: 30px;\n text-align: center;\n font-size: 11px;\n font-variant-numeric: tabular-nums;\n color: var(--rvs-fg3);\n user-select: none;\n}\n\n.rvs-seg {\n display: inline-flex;\n border: 1px solid var(--rvs-border2);\n border-radius: 6px;\n overflow: hidden;\n}\n\n.rvs-seg button {\n padding: 2px 9px;\n border: none;\n background: transparent;\n color: var(--rvs-fg2);\n font: inherit;\n font-size: 11px;\n cursor: pointer;\n}\n\n.rvs-seg button.active {\n background: var(--rvs-active);\n color: var(--rvs-fg);\n}\n\n.rvs-seg button:hover:not(.active) {\n background: var(--rvs-hover);\n}\n\n.rvs-diff-grid {\n flex: 1;\n min-height: 0;\n overflow-y: auto;\n overflow-x: hidden;\n font-family: var(--rvs-mono);\n font-size: 12px;\n line-height: 18px;\n scrollbar-width: thin;\n background: var(--rvs-code-bg);\n color: var(--rvs-code-fg);\n}\n\n/**\n * Top horizontal scrollbar.\n *\n * The two panes scroll horizontally on their own — so a long line can never\n * widen the layout — and this proxy bar drives both of them from the TOP of\n * the view, instead of forcing a scroll to the bottom of the file to reach a\n * horizontal bar. Its inner spacer is sized to the widest line, and the\n * component only renders it when a line really is wider than the grid.\n *\n * Sized and coloured to be SEEN: as a 13 px band with `scrollbar-width: thin`\n * and a faint thumb, it read as \"there is no horizontal scrollbar at all\".\n */\n.rvs-hbar {\n flex: none;\n height: 15px;\n overflow-x: auto;\n overflow-y: hidden;\n border-bottom: 1px solid var(--rvs-border);\n background: var(--rvs-hover);\n}\n\n.rvs-hbar-inner {\n height: 1px;\n}\n\n.rvs-hbar::-webkit-scrollbar {\n height: 11px;\n}\n\n.rvs-hbar::-webkit-scrollbar-thumb {\n background: var(--dsw-alias-scrollbar-bg-l2, rgba(150, 160, 180, .75));\n border-radius: 6px;\n border: 2px solid transparent;\n background-clip: padding-box;\n}\n\n.rvs-hbar::-webkit-scrollbar-thumb:hover {\n background: var(--dsw-alias-scrollbar-hover-l2, rgba(170, 180, 200, .95));\n background-clip: padding-box;\n}\n\n.rvs-hbar::-webkit-scrollbar-track {\n background: transparent;\n}\n\n.rvs-diff-cols {\n display: flex;\n align-items: stretch;\n min-width: 0;\n}\n\n/**\n * One diff pane. `flex: 1 1 0` keeps the panes EXACTLY equal however long a\n * line is (the previous row markup let a long line widen its own pane, which\n * is what broke the symmetry). Each pane keeps its own horizontal scroll, and\n * its scrollbar is now VISIBLE: the top proxy remains the convenient control,\n * but the pane's own bar is what a user reaches for first, and it sits right\n * under the content when the file is shorter than the view.\n */\n.rvs-diff-col {\n flex: 1 1 0;\n min-width: 0;\n overflow-x: auto;\n overflow-y: hidden;\n}\n\n.rvs-diff-col::-webkit-scrollbar {\n height: 9px;\n width: 0;\n}\n\n.rvs-diff-col::-webkit-scrollbar-thumb {\n background: var(--dsw-alias-scrollbar-bg-l2, rgba(150, 160, 180, .6));\n border-radius: 5px;\n}\n\n.rvs-diff-col::-webkit-scrollbar-thumb:hover {\n background: var(--dsw-alias-scrollbar-hover-l2, rgba(170, 180, 200, .85));\n}\n\n.rvs-diff-col::-webkit-scrollbar-track {\n background: transparent;\n}\n\n.rvs-dline {\n display: flex;\n align-items: flex-start;\n height: 18px;\n line-height: 18px;\n white-space: pre;\n}\n\n.rvs-dline-num {\n /* Pinned while the line scrolls sideways, so a long line never takes the\n line numbers away with it. */\n position: sticky;\n left: 0;\n z-index: 1;\n flex: none;\n width: 3.4em;\n padding-right: 6px;\n text-align: right;\n color: var(--rvs-fg3);\n opacity: .85;\n user-select: none;\n background: var(--rvs-code-bg);\n}\n\n.rvs-row-del .rvs-dline-num {\n background: var(--rvs-del-solid);\n}\n\n.rvs-row-add .rvs-dline-num {\n background: var(--rvs-add-solid);\n}\n\n.rvs-dline-text {\n flex: 1;\n padding-right: 10px;\n user-select: text;\n}\n\n.rvs-gutter-col {\n flex: none;\n width: 30px;\n border-left: 1px solid var(--rvs-border);\n border-right: 1px solid var(--rvs-border);\n user-select: none;\n}\n\n.rvs-gutter-line {\n height: 18px;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n.rvs-diff-grid::-webkit-scrollbar {\n width: 9px;\n height: 0;\n}\n\n.rvs-diff-grid::-webkit-scrollbar-thumb {\n background: var(--dsw-alias-scrollbar-bg-l2, rgba(127, 127, 127, .4));\n border-radius: 4px;\n}\n\n.rvs-num {\n flex: none;\n display: inline-block;\n width: 3.2em;\n padding-right: 6px;\n text-align: right;\n color: var(--rvs-fg3);\n opacity: .6;\n user-select: none;\n}\n\n.rvs-row-del {\n background: var(--rvs-del-bg);\n}\n\n.rvs-row-del .rvs-dline-text,\n.rvs-row-del .rvs-dline-num,\n.rvs-row-del .rvs-num {\n color: #f14c4c;\n}\n\n.rvs-row-add {\n background: var(--rvs-add-bg);\n}\n\n.rvs-row-add .rvs-dline-text,\n.rvs-row-add .rvs-dline-num,\n.rvs-row-add .rvs-num {\n color: #4ec9b0;\n}\n\n.rvs-row-empty {\n color: var(--rvs-fg3);\n opacity: .35;\n}\n\n.rvs-gutter {\n flex: none;\n width: 30px;\n display: flex;\n align-items: center;\n justify-content: center;\n border-left: 1px solid var(--rvs-border);\n border-right: 1px solid var(--rvs-border);\n user-select: none;\n}\n\n.rvs-gutter-dot {\n width: 6px;\n height: 6px;\n border-radius: 50%;\n opacity: .7;\n}\n\n.rvs-hunk-btns {\n display: flex;\n gap: 2px;\n}\n\n.rvs-hunk-btn {\n width: 13px;\n height: 15px;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 0;\n border: none;\n border-radius: 3px;\n background: transparent;\n color: var(--rvs-fg3);\n font-size: 11px;\n line-height: 1;\n cursor: pointer;\n}\n\n.rvs-hunk-btn:hover {\n background: var(--rvs-hover);\n color: var(--rvs-fg);\n}\n\n.rvs-hunk-btn.stage:hover {\n color: var(--rvs-git-a);\n}\n\n.rvs-hunk-btn.revert:hover {\n color: var(--rvs-git-d);\n}\n\n/**\n * Token colors: VS Code Dark+ / Light+, matching the CodeMirror theme in\n * `vsc/editor-theme.ts`. Keeping ONE palette for both surfaces means a file\n * does not change colors when the diff is switched to its editable side.\n */\n.rvs-t-comment { color: #6a9955; }\n.rvs-t-string { color: #ce9178; }\n.rvs-t-keyword { color: #569cd6; }\n.rvs-t-number { color: #b5cea8; }\n.rvs-t-func { color: #dcdcaa; }\n.rvs-t-type { color: #4ec9b0; }\n.rvs-t-attr { color: #9cdcfe; }\n.rvs-t-variable { color: #9cdcfe; }\n.rvs-t-operator { color: #d4d4d4; }\n.rvs-t-punct { color: #d4d4d4; }\n.rvs-t-tag { color: #569cd6; }\n.rvs-t-meta { color: #c586c0; }\n.rvs-t-insert { color: #4ec9b0; }\n.rvs-t-delete { color: #f14c4c; }\n\nbody:not([data-ds-dark-theme]) .rvs-t-comment { color: #008000; }\nbody:not([data-ds-dark-theme]) .rvs-t-string { color: #a31515; }\nbody:not([data-ds-dark-theme]) .rvs-t-keyword { color: #0000ff; }\nbody:not([data-ds-dark-theme]) .rvs-t-number { color: #098658; }\nbody:not([data-ds-dark-theme]) .rvs-t-func { color: #795e26; }\nbody:not([data-ds-dark-theme]) .rvs-t-type { color: #267f99; }\nbody:not([data-ds-dark-theme]) .rvs-t-attr,\nbody:not([data-ds-dark-theme]) .rvs-t-variable { color: #001080; }\nbody:not([data-ds-dark-theme]) .rvs-t-operator,\nbody:not([data-ds-dark-theme]) .rvs-t-punct { color: #1f1f1f; }\nbody:not([data-ds-dark-theme]) .rvs-t-meta { color: #af00db; }\n\n/**\n * CodeMirror token colors, applied through FIXED class names emitted by the\n * highlight style (`vsc/editor-palette.ts`). Fixed names mean the colors come\n * from this stylesheet — the same one that paints the rest of the UI — so a\n * code file can never go monochrome because a generated class name changed or\n * an injected style failed. Same Dark+ / Light+ palette as the diff panes\n * above, so a file keeps its colors when switching between the two views.\n */\n.rvs-ck-comment { color: #6a9955; }\n.rvs-ck-keyword { color: #569cd6; }\n.rvs-ck-control { color: #c586c0; }\n.rvs-ck-string { color: #ce9178; }\n.rvs-ck-regexp { color: #d16969; }\n.rvs-ck-number { color: #b5cea8; }\n.rvs-ck-constant { color: #569cd6; }\n.rvs-ck-func { color: #dcdcaa; }\n.rvs-ck-type { color: #4ec9b0; }\n.rvs-ck-variable { color: #9cdcfe; }\n.rvs-ck-attr { color: #9cdcfe; }\n.rvs-ck-meta { color: #c586c0; }\n.rvs-ck-punct { color: #d4d4d4; }\n.rvs-ck-op { color: #d4d4d4; }\n.rvs-ck-heading { color: #569cd6; font-weight: 700; }\n.rvs-ck-em { font-style: italic; }\n.rvs-ck-link { color: #9cdcfe; text-decoration: underline; }\n.rvs-ck-strike { text-decoration: line-through; }\n.rvs-ck-invalid { color: #f44747; }\n\nbody:not([data-ds-dark-theme]) .rvs-ck-comment { color: #008000; }\nbody:not([data-ds-dark-theme]) .rvs-ck-keyword { color: #0000ff; }\nbody:not([data-ds-dark-theme]) .rvs-ck-control { color: #af00db; }\nbody:not([data-ds-dark-theme]) .rvs-ck-string { color: #a31515; }\nbody:not([data-ds-dark-theme]) .rvs-ck-regexp { color: #811f3f; }\nbody:not([data-ds-dark-theme]) .rvs-ck-number { color: #098658; }\nbody:not([data-ds-dark-theme]) .rvs-ck-constant { color: #0070c1; }\nbody:not([data-ds-dark-theme]) .rvs-ck-func { color: #795e26; }\nbody:not([data-ds-dark-theme]) .rvs-ck-type { color: #267f99; }\nbody:not([data-ds-dark-theme]) .rvs-ck-variable { color: #001080; }\nbody:not([data-ds-dark-theme]) .rvs-ck-attr { color: #e50000; }\nbody:not([data-ds-dark-theme]) .rvs-ck-meta { color: #0000ff; }\nbody:not([data-ds-dark-theme]) .rvs-ck-punct,\nbody:not([data-ds-dark-theme]) .rvs-ck-op { color: #1f1f1f; }\nbody:not([data-ds-dark-theme]) .rvs-ck-heading { color: #0000ff; }\nbody:not([data-ds-dark-theme]) .rvs-ck-link { color: #001080; }\nbody:not([data-ds-dark-theme]) .rvs-ck-invalid { color: #cd3131; }\n\n/**\n * Opaque editor surface, also as plain CSS: the CodeMirror theme paints it too\n * (following the scheme through a Compartment), but a transparent editor is\n * the one failure a user notices immediately, so the container states it as\n * well.\n */\n.rvs-edge .cm-editor,\n.rvs-edge .cm-scroller,\n.rvs-edge .cm-gutters {\n background-color: var(--rvs-code-bg);\n}\n\n.rvs-edge .cm-editor {\n color: var(--rvs-code-fg);\n}\n\n.rvs-edge .cm-gutters {\n color: var(--rvs-code-gutter-fg, var(--rvs-fg3));\n border-right: 1px solid var(--rvs-border);\n}\n\n.rvs-edge .cm-activeLine {\n background-color: rgba(127, 127, 127, .08);\n}\n\n.rvs-edge .cm-activeLineGutter {\n background-color: rgba(127, 127, 127, .12);\n}\n\n/**\n * The code editor's own scrollbars.\n *\n * These had NO rule at all: CodeMirror's scroller is `.cm-scroller`, which is\n * none of `.rvs-scroll` / `.rvs-diff-grid` / `.rvs-hbar`, so the editor fell\n * back to the browser default — too thin to hit with a mouse.\n *\n * Both notations are needed and must stay in step: Chromium honours\n * `scrollbar-width` over `::-webkit-scrollbar` when both apply, so setting\n * only the webkit half would silently have no effect. Keep the numbers equal.\n */\n.rvs-edge .cm-scroller {\n scrollbar-width: auto;\n}\n\n.rvs-edge .cm-scroller::-webkit-scrollbar {\n width: 14px;\n height: 14px;\n}\n\n.rvs-edge .cm-scroller::-webkit-scrollbar-track {\n background: transparent;\n}\n\n.rvs-edge .cm-scroller::-webkit-scrollbar-thumb {\n /* Opaque, not a translucent token: a thin wash over code is exactly the\n low-contrast target that is hard to grab. */\n background: rgba(121, 121, 121, .8);\n border: 3px solid transparent;\n border-radius: 7px;\n background-clip: padding-box;\n min-height: 36px;\n}\n\n.rvs-edge .cm-scroller::-webkit-scrollbar-thumb:hover {\n background: rgba(150, 150, 150, .95);\n border: 3px solid transparent;\n background-clip: padding-box;\n}\n\n.rvs-edge .cm-scroller::-webkit-scrollbar-corner {\n background: transparent;\n}\n\n/* ------------------------------------------------------------ find widget */\n\n/**\n * \"You landed here\": the transient highlight on the line a jump landed on\n * (search hit, @-reference).\n *\n * Deliberately NOT a warm/amber wash: over the dark editor that muddies into a\n * brown-black band that reads as a shadow and swallows annotation colors. This\n * is the accent blue instead — a tinted band with a bright edge on the left and\n * hairlines top/bottom, so it reads as a marked region while every token keeps\n * its own colour and contrast.\n */\n.rvs-reveal-line {\n background-color: rgba(88, 166, 255, .22);\n box-shadow:\n inset 3px 0 0 0 #58a6ff,\n inset 0 1px 0 0 rgba(88, 166, 255, .38),\n inset 0 -1px 0 0 rgba(88, 166, 255, .38);\n}\n\nbody:not([data-ds-dark-theme]) .rvs-reveal-line {\n background-color: rgba(77, 107, 254, .14);\n box-shadow:\n inset 3px 0 0 0 #4d6bfe,\n inset 0 1px 0 0 rgba(77, 107, 254, .35),\n inset 0 -1px 0 0 rgba(77, 107, 254, .35);\n}\n\n/* --------------------------------------------------------- session changes */\n\n/**\n * 「本会话改了什么」:代码行背景 + 左侧改动栏(Cursor 风格)。\n *\n * 三种形态必须**一眼分得开**,光靠绿/红两个色相不够(色弱用户看新增行和删除行\n * 会糊成同一片),所以每种的左侧色条宽度/花样的语义也不同:\n * - 新增:实心绿条;\n * - 修改:实心蓝条(不是绿:修改块里既有留下的行也有新写的行,用\"中性的已变更\"\n * 色不会误导读者以为整块都是新内容);\n * - 删除:**斜纹**红底 + 更粗的红条 —— 这一行其实已经不存在了,只在中止位置上\n * 留个锚点,斜纹是这一层\"虚\"的信息里最不容易被当成真内容的花样。\n *\n * 选择器带上 `.cm-line`:`.cm-activeLine` 由 CodeMirror 的主题注入(运行时\n * `<style>`,通常排在本文档之后),只写 `.rvs-change-*` 两者同权重时会输给它,\n * 光标所在的那一行改动就会消失。\n */\n.rvs-edge .cm-line.rvs-change-add {\n background-color: rgba(46, 160, 67, .17);\n box-shadow:\n inset 3px 0 0 0 #2ea043,\n inset 0 -1px 0 0 rgba(46, 160, 67, .28);\n}\n\n.rvs-edge .cm-line.rvs-change-mod {\n background-color: rgba(56, 139, 253, .15);\n box-shadow:\n inset 3px 0 0 0 #388bfd,\n inset 0 -1px 0 0 rgba(56, 139, 253, .28);\n}\n\n.rvs-edge .cm-line.rvs-change-del {\n background-color: rgba(248, 81, 73, .09);\n background-image: repeating-linear-gradient(135deg, rgba(248, 81, 73, .17) 0 6px, transparent 6px 12px);\n box-shadow: inset 4px 0 0 0 #f85149;\n}\n\n/* 改动栏本身:符号 + 一列与行背景同色的窄条,读起来才是一条\"改动边\"。 */\n.rvs-edge .cm-gutterElement.rvs-change-gutter-add {\n box-shadow: inset 2px 0 0 0 #2ea043;\n}\n\n.rvs-edge .cm-gutterElement.rvs-change-gutter-mod {\n box-shadow: inset 2px 0 0 0 #388bfd;\n}\n\n.rvs-edge .cm-gutterElement.rvs-change-gutter-del {\n box-shadow: inset 2px 0 0 0 #f85149;\n}\n\n.rvs-edge .rvs-change-mark {\n display: inline-block;\n width: 12px;\n font-family: var(--rvs-mono);\n font-size: 12px;\n line-height: 1;\n text-align: center;\n user-select: none;\n}\n\n.rvs-edge .rvs-change-mark-add { color: #2ea043; }\n.rvs-edge .rvs-change-mark-mod { color: #388bfd; }\n.rvs-edge .rvs-change-mark-del { color: #f85149; }\n\n/**\n * 行内(字符级)差异:上面那三种底色回答\"哪几行变了\",这两条回答\"那一行的哪几个\n * 字变了\"。只改一个词的编辑,整行同色时完全看不出改了什么。\n *\n * `rvs-change-word-add` 刻意**只给背景、不给 `color`**:代码有自己的语法着色,\n * 覆盖前景色会把关键字/字符串/注释染成同一种颜色,比不标还难读。透明度压在行\n * 底色之上一点点,叠起来仍读得出\"整块改动\"的边界,又能看出块内哪几个字是新的。\n */\n.rvs-edge .rvs-change-word-add {\n background-color: rgba(46, 160, 67, .22);\n border-radius: 2px;\n}\n\n/**\n * 被删掉、在新文件里已经不存在的内容:零宽 widget,红底 + 删除线。\n *\n * 删除线画在 chip 自己身上(chip 是独立元素,不会波及周围代码);`white-space:\n * pre` 保留被删文本自己的缩进 —— 截断/去空白都会让 chip 谎报\"删了什么\"。\n * 字号略小 + 降一点不透明度,是让它读起来像\"残留物\"而不是一行真代码。\n */\n.rvs-edge .rvs-change-word-del {\n padding: 0 3px;\n margin: 0 1px;\n border-radius: 3px;\n background-color: rgba(248, 81, 73, .18);\n font-size: .92em;\n text-decoration: line-through;\n opacity: .85;\n white-space: pre;\n}\n\n/**\n * 撑宽度的隐形符号:只用它占位,绝不显示(见 `change-marks.ts` 的 SpacerMarker,\n * CodeMirror 会给这个 spacer 元素加 `visibility: hidden`,这里不重复声明)。\n */\n.rvs-edge .rvs-change-spacer {\n opacity: 0;\n}\n\n/* 浅色主题:同样的色相,压暗 + 提高不透明度,在近白底上才看得清。 */\nbody:not([data-ds-dark-theme]) .rvs-edge .cm-line.rvs-change-add {\n background-color: rgba(31, 136, 61, .13);\n box-shadow:\n inset 3px 0 0 0 #1f883d,\n inset 0 -1px 0 0 rgba(31, 136, 61, .22);\n}\n\nbody:not([data-ds-dark-theme]) .rvs-edge .cm-line.rvs-change-mod {\n background-color: rgba(9, 105, 218, .12);\n box-shadow:\n inset 3px 0 0 0 #0969da,\n inset 0 -1px 0 0 rgba(9, 105, 218, .22);\n}\n\nbody:not([data-ds-dark-theme]) .rvs-edge .cm-line.rvs-change-del {\n background-color: rgba(207, 34, 46, .07);\n background-image: repeating-linear-gradient(135deg, rgba(207, 34, 46, .14) 0 6px, transparent 6px 12px);\n box-shadow: inset 4px 0 0 0 #cf222e;\n}\n\nbody:not([data-ds-dark-theme]) .rvs-edge .cm-gutterElement.rvs-change-gutter-add {\n box-shadow: inset 2px 0 0 0 #1f883d;\n}\n\nbody:not([data-ds-dark-theme]) .rvs-edge .cm-gutterElement.rvs-change-gutter-mod {\n box-shadow: inset 2px 0 0 0 #0969da;\n}\n\nbody:not([data-ds-dark-theme]) .rvs-edge .cm-gutterElement.rvs-change-gutter-del {\n box-shadow: inset 2px 0 0 0 #cf222e;\n}\n\nbody:not([data-ds-dark-theme]) .rvs-edge .rvs-change-mark-add { color: #1f883d; }\nbody:not([data-ds-dark-theme]) .rvs-edge .rvs-change-mark-mod { color: #0969da; }\nbody:not([data-ds-dark-theme]) .rvs-edge .rvs-change-mark-del { color: #cf222e; }\n\n/* 浅色主题的行内差异:同样的色相压暗,在近白底上才看得清(暗色那套会糊成一片)。 */\nbody:not([data-ds-dark-theme]) .rvs-edge .rvs-change-word-add {\n background-color: rgba(26, 127, 55, .18);\n}\n\nbody:not([data-ds-dark-theme]) .rvs-edge .rvs-change-word-del {\n background-color: rgba(207, 34, 46, .14);\n}\n\n/**\n * 改动跳转按钮:浮在编辑器右上角。\n *\n * 放在编辑器上而不是面板里 —— 文件打开后光标就在这里,而面板可能被收起、被滚走。\n * 容器 `.rvs-body` 已经是 `position: relative`(它本来用于挂 find 浮层),所以这里\n * 只做绝对定位,不引入新的定位上下文。`pointer-events` 只开在按钮上:这块区域压在\n * 编辑器文字上方,整块可点会把右上角那几行变成死区。\n */\n.rvs-change-nav {\n position: absolute;\n top: 8px;\n right: 14px;\n z-index: 5;\n display: flex;\n align-items: center;\n gap: 2px;\n padding: 3px 4px;\n border: 1px solid var(--rvs-border2);\n border-radius: 8px;\n background: Canvas;\n box-shadow: 0 2px 8px rgba(0, 0, 0, .28);\n pointer-events: none;\n}\n\n/* The counter is what makes this read as \"3 of 7 changes\" rather than two\n anonymous arrows — and it keeps the buttons from looking like decoration. */\n.rvs-change-nav-count {\n padding: 0 6px 0 4px;\n color: var(--rvs-fg2);\n font-size: 11px;\n font-variant-numeric: tabular-nums;\n line-height: 22px;\n white-space: nowrap;\n}\n\n.rvs-change-nav-btn {\n width: 26px;\n height: 22px;\n padding: 0;\n border: 1px solid transparent;\n border-radius: 5px;\n background: transparent;\n color: var(--rvs-fg);\n font-size: 13px;\n font-weight: 600;\n line-height: 20px;\n text-align: center;\n cursor: pointer;\n pointer-events: auto;\n}\n\n.rvs-change-nav-btn:hover {\n border-color: var(--rvs-border2);\n background: var(--rvs-hover);\n color: var(--rvs-brand);\n}\n\n.rvs-change-nav-btn:active { background: var(--rvs-active); }\n\n/**\n * 悬浮在改动行上出现的「撤销这处改动」按钮。\n *\n * 它**不在编辑器的 DOM 里**:`undo-affordance.ts` 把它当 `position: fixed` 的元素挂到\n * `document.body`(和「添加到对话」那个浮层同一套做法),所以这里的选择器不带\n * `.rvs-edge` —— 带上了就一条都不生效。固定定位 + 高 z-index 是刻意的:宿主容器有\n * `overflow: hidden`,挂在里面会被 CodeMirror 的滚动层裁掉。\n *\n * 颜色按\"危险动作\"给:默认红边红字、悬停实心红。底色不透明(不用半透明的 overlay\n * token),否则后面的代码文字会透出来。\n */\n.rvs-undo-affordance {\n position: fixed;\n z-index: 2100;\n display: inline-flex;\n align-items: center;\n gap: 4px;\n max-width: 220px;\n padding: 3px 9px;\n border: 1px solid #f85149;\n border-radius: 6px;\n background-color: #1e2229;\n color: #ffb4ae;\n /* 按钮挂在 body 上,没有 .rvs-edge 的字体上下文:跟随宿主页面字体。 */\n font-family: inherit;\n font-size: 12px;\n line-height: 1.5;\n white-space: nowrap;\n cursor: pointer;\n box-shadow: 0 6px 18px rgba(0, 0, 0, .45);\n /* 纵向以悬停行的中线对齐(横向坐标由 JS 按行尾算好)。 */\n transform: translateY(-50%);\n}\n\n.rvs-undo-affordance:hover {\n background-color: #f85149;\n border-color: #f85149;\n color: #fff;\n}\n\n.rvs-undo-affordance:active {\n transform: translateY(-50%) scale(.97);\n}\n\n.rvs-undo-affordance:focus-visible {\n outline: 2px solid #58a6ff;\n outline-offset: 1px;\n}\n\n/* 删除锚点:这一行其实已经不在了,虚线边框把它和\"真实存在的行\"分开。 */\n.rvs-undo-affordance[data-kind=\"del\"] {\n border-style: dashed;\n}\n\nbody:not([data-ds-dark-theme]) .rvs-undo-affordance {\n border-color: #cf222e;\n background-color: #ffffff;\n color: #a40e26;\n box-shadow: 0 6px 18px rgba(31, 35, 40, .18);\n}\n\nbody:not([data-ds-dark-theme]) .rvs-undo-affordance:hover {\n background-color: #cf222e;\n border-color: #cf222e;\n color: #fff;\n}\n\nbody:not([data-ds-dark-theme]) .rvs-undo-affordance:focus-visible {\n outline-color: #4d6bfe;\n}\n\n/**\n * CodeMirror's own search decorations, also stated here: the editor's theme is\n * injected by the library, and these must stay readable even when only this\n * stylesheet is in play.\n */\n.rvs-edge .cm-searchMatch {\n background-color: rgba(226, 183, 20, .30);\n outline: 1px solid rgba(226, 183, 20, .55);\n}\n\n.rvs-edge .cm-searchMatch-selected {\n background-color: rgba(226, 183, 20, .48);\n}\n\n.rvs-edge .cm-selectionBackground,\n.rvs-edge .cm-content ::selection {\n background-color: rgba(58, 110, 165, .55) !important;\n}\n\n/** Build stamp in the editor info bar: identifies the loaded client bundle. */\n.rvs-build {\n flex: none;\n padding: 0 5px;\n border: 1px solid var(--rvs-border);\n border-radius: 4px;\n color: var(--rvs-fg3);\n font-size: 10px;\n font-family: var(--rvs-mono);\n opacity: .85;\n user-select: text;\n}\n\n.rvs-find {\n position: absolute;\n top: 6px;\n right: 18px;\n z-index: 30;\n display: flex;\n align-items: center;\n gap: 3px;\n padding: 3px 4px 3px 7px;\n border: 1px solid var(--rvs-border2);\n border-radius: 8px;\n background-color: Canvas;\n background-image: linear-gradient(var(--dsw-alias-bg-overlay, transparent), var(--dsw-alias-bg-overlay, transparent));\n color: var(--rvs-fg);\n box-shadow: 0 4px 14px rgba(0, 0, 0, .28);\n font-size: 12px;\n}\n\n.rvs-find-input {\n width: 170px;\n padding: 2px 6px;\n border: 1px solid var(--rvs-border2);\n border-radius: 5px;\n background: var(--rvs-input);\n color: var(--rvs-fg);\n font: inherit;\n font-size: 12px;\n outline: none;\n}\n\n.rvs-find-input:focus {\n border-color: var(--rvs-brand);\n}\n\n.rvs-find-count {\n flex: none;\n min-width: 54px;\n text-align: center;\n color: var(--rvs-fg3);\n font-size: 11px;\n white-space: nowrap;\n}\n\n.rvs-find-count.none {\n color: var(--rvs-git-d);\n}\n\n.rvs-code {\n flex: 1;\n min-height: 0;\n overflow: auto;\n margin: 0;\n padding: 6px 0 16px;\n font-family: var(--rvs-mono);\n font-size: 12.5px;\n line-height: 18px;\n white-space: pre;\n scrollbar-width: thin;\n background: var(--rvs-code-bg);\n color: var(--rvs-code-fg);\n}\n\n.rvs-code-line {\n display: flex;\n align-items: flex-start;\n min-width: max-content;\n}\n\n.rvs-code-line:hover {\n background: var(--rvs-hover);\n}\n\n.rvs-code-text {\n flex: 1;\n padding-right: 10px;\n white-space: pre-wrap;\n word-break: break-word;\n user-select: text;\n}\n\n/* ---------------------------------------------------------- history list */\n\n.rvs-commit-item {\n display: flex;\n align-items: center;\n gap: 6px;\n width: 100%;\n padding: 4px 8px 4px 4px;\n border: none;\n background: transparent;\n color: var(--rvs-fg);\n font: inherit;\n font-size: 12.5px;\n line-height: 18px;\n text-align: left;\n cursor: pointer;\n}\n\n.rvs-commit-item:hover {\n background: var(--rvs-hover);\n}\n\n.rvs-commit-item.selected {\n background: var(--rvs-active);\n}\n\n.rvs-graph-svg {\n flex: none;\n display: block;\n}\n\n.rvs-commit-hash {\n flex: none;\n font-family: var(--rvs-mono);\n font-size: 11px;\n color: var(--rvs-fg3);\n}\n\n.rvs-commit-msg {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.rvs-commit-date {\n flex: none;\n font-size: 11px;\n color: var(--rvs-fg3);\n}\n\n.rvs-ref-badge {\n flex: none;\n padding: 0 5px;\n border-radius: 8px;\n border: 1px solid var(--rvs-border2);\n font-size: 10px;\n line-height: 15px;\n color: var(--rvs-fg2);\n}\n\n.rvs-ref-badge.head {\n border-color: var(--rvs-brand);\n background: rgba(77, 107, 254, .2);\n color: var(--rvs-fg);\n}\n\n.rvs-ref-badge.tag {\n border-color: rgba(226, 183, 20, .55);\n background: rgba(226, 183, 20, .16);\n color: var(--rvs-git-m);\n}\n\n.rvs-detail {\n margin: 0 6px 8px;\n padding: 8px 10px;\n border: 1px solid var(--rvs-border2);\n border-left: 3px solid var(--rvs-accent);\n border-radius: 8px;\n background: var(--rvs-layer3);\n font-size: 12px;\n}\n\n.rvs-detail-meta {\n color: var(--rvs-fg3);\n font-size: 11px;\n margin-bottom: 6px;\n white-space: pre-wrap;\n}\n\n.rvs-file-row {\n display: flex;\n align-items: center;\n gap: 6px;\n padding: 2px 4px;\n border-radius: 5px;\n cursor: pointer;\n}\n\n.rvs-file-row:hover {\n background: var(--rvs-hover);\n}\n\n.rvs-file-path {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n /* Same tail-first truncation as the info bar; `plaintext` keeps the path's\n own LTR order (see .rvs-einfo-path). */\n direction: rtl;\n unicode-bidi: plaintext;\n text-align: left;\n}\n\n.rvs-file-status {\n flex: none;\n width: 16px;\n text-align: center;\n font-weight: 700;\n font-size: 11px;\n}\n\n/* --------------------------------------------------------- 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.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) {
@@ -1374,6 +1374,19 @@ window.__ModuleLoader__.load({
1374
1374
  "files.deleteConfirm": "确定删除 {target} 吗?此操作在远端不可撤销。",
1375
1375
  "files.open": "打开",
1376
1376
  "files.openAsText": "以文本打开",
1377
+ "files.openInFloat": "在悬浮编辑器中打开",
1378
+ "float.title": "悬浮编辑器",
1379
+ "float.dragHint": "拖动标题栏移动窗口;拖动边缘缩放;双击标题栏最大化或还原",
1380
+ "float.minimize": "最小化",
1381
+ "float.ballHint": "点击恢复悬浮编辑器,拖动可移动",
1382
+ "float.dockViewHint": "双击「远程编辑器」标签可浮出编辑器",
1383
+ "float.maximize": "最大化",
1384
+ "float.restore": "还原窗口",
1385
+ "float.dock": "停靠回主界面",
1386
+ "float.placeholder": "远程编辑器正在悬浮窗口中",
1387
+ "float.placeholderHint": "编辑器只有一份实例,已浮出到窗口中央;关闭悬浮窗或点「停靠」后回到这里。",
1388
+ "float.dockBack": "停靠回主界面",
1389
+ "float.unavailable": "当前宿主没有全帧悬浮层(shell.overlay),悬浮编辑器不可用。",
1377
1390
  "files.rename": "重命名",
1378
1391
  "files.copy": "复制",
1379
1392
  "files.cut": "剪切",
@@ -1708,6 +1721,19 @@ window.__ModuleLoader__.load({
1708
1721
  "files.deleteConfirm": "Delete {target}? This cannot be undone on the remote host.",
1709
1722
  "files.open": "Open",
1710
1723
  "files.openAsText": "Open as text",
1724
+ "files.openInFloat": "Open in floating editor",
1725
+ "float.title": "Floating editor",
1726
+ "float.dragHint": "Drag the title bar to move; drag an edge to resize; double-click the title bar to maximize or restore",
1727
+ "float.minimize": "Minimize",
1728
+ "float.ballHint": "Click to restore the floating editor, drag to move it",
1729
+ "float.dockViewHint": "Double-click the \"remote editor\" tab to float the editor",
1730
+ "float.maximize": "Maximize",
1731
+ "float.restore": "Restore window",
1732
+ "float.dock": "Dock back into the main layout",
1733
+ "float.placeholder": "The remote editor is floating over the app",
1734
+ "float.placeholderHint": "There is only one editor instance and it is out in the floating window; close that window or press Dock to bring it back here.",
1735
+ "float.dockBack": "Dock back",
1736
+ "float.unavailable": "This host has no frame-wide overlay slot (shell.overlay); the floating editor is unavailable.",
1711
1737
  "files.rename": "Rename",
1712
1738
  "files.copy": "Copy",
1713
1739
  "files.cut": "Cut",
@@ -35581,7 +35607,7 @@ window.__ModuleLoader__.load({
35581
35607
  "&": {
35582
35608
  backgroundColor: p.bg,
35583
35609
  color: p.fg,
35584
- fontSize: "13px",
35610
+ fontSize: "var(--rvs-editor-font-size, 13px)",
35585
35611
  height: "100%"
35586
35612
  },
35587
35613
  ".cm-content": {
@@ -37619,14 +37645,18 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
37619
37645
  (0, react.useImperativeHandle)(ref, () => ({
37620
37646
  getContent: () => viewRef.current?.state.doc.toString() ?? savedRef.current,
37621
37647
  isDirty: () => dirtyRef.current,
37622
- markSaved: () => {
37648
+ markSaved: (savedText, tabId) => {
37623
37649
  const view = viewRef.current;
37624
- if (view !== void 0) {
37625
- savedRef.current = view.state.doc.toString();
37626
- savedDocRef.current = view.state.doc;
37650
+ if (view === void 0) return;
37651
+ if (propsRef.current.tabId !== tabId) return;
37652
+ savedRef.current = savedText;
37653
+ const saved = EditorState.create({ doc: savedText }).doc;
37654
+ savedDocRef.current = saved;
37655
+ const dirty = view.state.doc !== saved && !view.state.doc.eq(saved);
37656
+ if (dirty !== dirtyRef.current) {
37657
+ dirtyRef.current = dirty;
37658
+ propsRef.current.onDirty(dirty);
37627
37659
  }
37628
- dirtyRef.current = false;
37629
- propsRef.current.onDirty(false);
37630
37660
  },
37631
37661
  getViewState: () => {
37632
37662
  const view = viewRef.current;
@@ -37671,6 +37701,33 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
37671
37701
  if (props.visible === false) return;
37672
37702
  viewRef.current?.requestMeasure();
37673
37703
  }, [props.visible]);
37704
+ /**
37705
+ * A save that landed AFTER this editor mounted.
37706
+ *
37707
+ * The save path moves the tab's `baseline` (the bytes now on disk) and asks
37708
+ * the live editor to re-derive its dirty flag. That works while the same
37709
+ * instance is mounted, but the pane can be remounted while the remote write
37710
+ * is in flight (a surface flip, a dock, a tab switch): the replacement
37711
+ * derives against the baseline it saw at MOUNT, and the corrected value only
37712
+ * arrives later as a prop. Without this effect the tab keeps the `dirty:false`
37713
+ * the save optimistically wrote, while the buffer holds newer text — dot off,
37714
+ * Save disabled, close guard silent. Re-derive, never touch the document.
37715
+ */
37716
+ (0, react.useEffect)(() => {
37717
+ const view = viewRef.current;
37718
+ const savedText = props.savedText;
37719
+ if (view === void 0 || savedText === void 0) return;
37720
+ const baseline = EditorState.create({ doc: savedText }).doc;
37721
+ const previous = savedDocRef.current;
37722
+ if (previous !== void 0 && previous.eq(baseline)) return;
37723
+ savedDocRef.current = baseline;
37724
+ savedRef.current = savedText;
37725
+ const dirty = view.state.doc !== baseline && !view.state.doc.eq(baseline);
37726
+ if (dirty !== dirtyRef.current) {
37727
+ dirtyRef.current = dirty;
37728
+ propsRef.current.onDirty(dirty);
37729
+ }
37730
+ }, [props.savedText]);
37674
37731
  const chatGate = props.chatEnabled === true && props.sessionId !== void 0 && props.remoteRoot !== void 0;
37675
37732
  (0, react.useEffect)(() => {
37676
37733
  if (!chatGate) popupRef.current.hide();
@@ -37856,7 +37913,17 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
37856
37913
  parent: host
37857
37914
  });
37858
37915
  viewRef.current = view;
37859
- savedDocRef.current = view.state.doc;
37916
+ const savedText = propsRef.current.savedText;
37917
+ const baseline = savedText !== void 0 ? EditorState.create({ doc: savedText }).doc : view.state.doc;
37918
+ savedDocRef.current = baseline;
37919
+ if (savedText !== void 0) {
37920
+ savedRef.current = savedText;
37921
+ const dirty = view.state.doc !== baseline && !view.state.doc.eq(baseline);
37922
+ if (dirty !== dirtyRef.current) {
37923
+ dirtyRef.current = dirty;
37924
+ propsRef.current.onDirty(dirty);
37925
+ }
37926
+ }
37860
37927
  setView(view);
37861
37928
  /**
37862
37929
  * Come back to where this file was left.
@@ -37994,7 +38061,7 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
37994
38061
  const primary = host.querySelectorAll("[class*=\"rvs-ck-\"]").length;
37995
38062
  const fallback = host.querySelectorAll("span[class^=\"rvs-t-\"]").length;
37996
38063
  const count = primary > 0 ? primary : fallback;
37997
- console.info(`[dsh-remote-vs] build 1026-0953 · ${source} · colored tokens: ${count} (${propsRef.current.path})`);
38064
+ console.info(`[dsh-remote-vs] build 1027-1757 · ${source} · colored tokens: ${count} (${propsRef.current.path})`);
37998
38065
  propsRef.current.onTokens?.(count);
37999
38066
  };
38000
38067
  const armFallback = () => {
@@ -38080,6 +38147,122 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
38080
38147
  ] });
38081
38148
  });
38082
38149
  //#endregion
38150
+ //#region src/client/live-editor.ts
38151
+ let registration;
38152
+ /** Scroll + caret per tab id, surviving an unmount (see the header). */
38153
+ const viewStates = /* @__PURE__ */ new Map();
38154
+ /**
38155
+ * Register the editor that is on screen right now.
38156
+ *
38157
+ * The caller passes a getter, not a handle: `editorRef.current` is `null`
38158
+ * during the first render and is cleared before the parent's cleanup runs, so
38159
+ * a snapshot taken later must read through the getter.
38160
+ * @returns disposer; only the registration that is still current is cleared.
38161
+ */
38162
+ function registerLiveEditor(tabId, getHandle) {
38163
+ const entry = {
38164
+ tabId,
38165
+ getHandle
38166
+ };
38167
+ registration = entry;
38168
+ return () => {
38169
+ if (registration === entry) registration = void 0;
38170
+ };
38171
+ }
38172
+ /**
38173
+ * Snapshot the live position (scroll + caret) without touching the document.
38174
+ *
38175
+ * Cheap, so the store can call it on EVERY switch: a surface flip remounts the
38176
+ * very same tab, and the replacement's render happens before the old view's
38177
+ * unmount report — the position has to be in the map before that render.
38178
+ */
38179
+ function captureLiveEditorPosition() {
38180
+ const entry = registration;
38181
+ if (entry === void 0) return;
38182
+ const handle = entry.getHandle();
38183
+ if (handle === null) return;
38184
+ const viewState = handle.getViewState();
38185
+ if (viewState !== void 0) viewStates.set(entry.tabId, viewState);
38186
+ }
38187
+ /**
38188
+ * Whether the live buffer differs from its disk baseline.
38189
+ *
38190
+ * The store uses this to skip the O(document) text copy on clean tabs (see
38191
+ * `captureLiveBuffer`): the boolean is cheap, `getContent()` materializes the
38192
+ * whole file as a string.
38193
+ */
38194
+ function isLiveEditorDirty() {
38195
+ return registration?.getHandle()?.isDirty() === true;
38196
+ }
38197
+ /**
38198
+ * Copy the live buffer's text, for a switch that would otherwise lose it.
38199
+ *
38200
+ * Called from the store BEFORE the tab switch / surface flip: at that moment
38201
+ * the editor is still mounted and its CodeMirror view still owns the edits.
38202
+ * @returns undefined when no file editor is live (a diff tab, or a view that
38203
+ * was never the selected one) — there is nothing to carry over then.
38204
+ */
38205
+ function captureLiveEditorText() {
38206
+ const entry = registration;
38207
+ if (entry === void 0) return void 0;
38208
+ const handle = entry.getHandle();
38209
+ if (handle === null) return void 0;
38210
+ try {
38211
+ return {
38212
+ tabId: entry.tabId,
38213
+ text: handle.getContent()
38214
+ };
38215
+ } catch {
38216
+ return;
38217
+ }
38218
+ }
38219
+ /**
38220
+ * Whether one tab's file editor is the one registered right now.
38221
+ *
38222
+ * The save path asks this before it clears a tab's dirty flag: if an editor IS
38223
+ * mounted for that tab, the flag belongs to the live buffer and only the
38224
+ * editor may clear it (see `markLiveEditorSaved`).
38225
+ */
38226
+ function isLiveEditorFor(tabId) {
38227
+ return registration?.tabId === tabId;
38228
+ }
38229
+ /**
38230
+ * Advance the registered editor's dirty baseline to the bytes a save wrote.
38231
+ *
38232
+ * Routed through the MODULE-LEVEL registration rather than a React ref on
38233
+ * purpose: the pane can be remounted while the remote write is in flight (a
38234
+ * surface flip, a dock, a tab switch), and the ref the save started with is
38235
+ * then detached — that is exactly how a tab ended up reported clean while its
38236
+ * live buffer was newer than the file. The registration always names the editor
38237
+ * mounted NOW, so the correction lands on whichever instance is on screen, and
38238
+ * it is skipped (never misapplied) when that editor shows another tab: that tab
38239
+ * heals on its next mount from `savedText` (see `CodeEditor`).
38240
+ * @returns true when an editor for this tab took the new baseline.
38241
+ */
38242
+ function markLiveEditorSaved(tabId, savedText) {
38243
+ const entry = registration;
38244
+ if (entry === void 0 || entry.tabId !== tabId) return false;
38245
+ entry.getHandle()?.markSaved(savedText, tabId);
38246
+ return true;
38247
+ }
38248
+ /** Remember one tab's scroll + caret (reported by the editor, see EditorPane). */
38249
+ function rememberEditorViewState(tabId, state) {
38250
+ viewStates.set(tabId, state);
38251
+ }
38252
+ /** The stored position for one tab, or undefined when it was never seen. */
38253
+ function editorViewStateOf(tabId) {
38254
+ return viewStates.get(tabId);
38255
+ }
38256
+ /**
38257
+ * Drop positions of tabs that no longer exist.
38258
+ *
38259
+ * The map used to live in a component with the same cleanup: without it a
38260
+ * long session accumulated one state object per file ever opened.
38261
+ */
38262
+ function pruneEditorViewStates(liveIds) {
38263
+ for (const id of [...viewStates.keys()]) if (!liveIds.has(id)) viewStates.delete(id);
38264
+ }
38265
+ //#endregion
38083
38266
  //#region src/client/use-remote-chat-target.ts
38084
38267
  /**
38085
38268
  * The session↔binding gate behind the remote `@file` buttons and the
@@ -40326,6 +40509,212 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
40326
40509
  }
40327
40510
  return next ?? current;
40328
40511
  }
40512
+ function clamp(value, min, max) {
40513
+ return Math.min(max, Math.max(min, value));
40514
+ }
40515
+ /** True when the stored value is a usable finite number (not NaN/Infinity). */
40516
+ function finite(value) {
40517
+ return typeof value === "number" && Number.isFinite(value);
40518
+ }
40519
+ /** The effective editor surface: floating wins, otherwise the base mount. */
40520
+ function effectiveEditorSurface(base, floatOpen) {
40521
+ return floatOpen ? "float" : base;
40522
+ }
40523
+ /**
40524
+ * The rect a fresh floating window opens at: about two thirds of the frame,
40525
+ * centred, with a hard size cap so a 4K monitor does not produce a 2600px
40526
+ * editor for a 40-column file.
40527
+ */
40528
+ function defaultFloatRect(viewport) {
40529
+ const maxWidth = Math.max(360, viewport.width - 16);
40530
+ const maxHeight = Math.max(220, viewport.height - 16);
40531
+ const width = Math.min(maxWidth, Math.max(360, Math.round(viewport.width * .62)), 1180);
40532
+ const height = Math.min(maxHeight, Math.max(220, Math.round(viewport.height * .7)), 860);
40533
+ return clampFloatRect({
40534
+ x: Math.round((viewport.width - width) / 2),
40535
+ y: Math.round((viewport.height - height) / 2) - 12,
40536
+ width,
40537
+ height
40538
+ }, viewport);
40539
+ }
40540
+ /**
40541
+ * Bring a rect back inside the viewport and to a legal size.
40542
+ *
40543
+ * Used on every drag step, on restore, and whenever the browser window is
40544
+ * resized: a rect persisted on a 2560px screen must not open a window whose
40545
+ * title bar is off-screen on a 1366px laptop.
40546
+ */
40547
+ function clampFloatRect(rect, viewport) {
40548
+ const width = clamp(rect.width, 360, Math.max(360, viewport.width));
40549
+ const height = clamp(rect.height, 220, Math.max(220, viewport.height));
40550
+ const x = clamp(rect.x, 0, Math.max(0, viewport.width - width));
40551
+ const y = clamp(rect.y, 0, Math.max(0, viewport.height - height));
40552
+ return {
40553
+ x: Math.round(x),
40554
+ y: Math.round(y),
40555
+ width: Math.round(width),
40556
+ height: Math.round(height)
40557
+ };
40558
+ }
40559
+ /** Move the window by a pointer delta, staying inside the viewport. */
40560
+ function dragFloatRect(start, dx, dy, viewport) {
40561
+ return clampFloatRect({
40562
+ ...start,
40563
+ x: start.x + dx,
40564
+ y: start.y + dy
40565
+ }, viewport);
40566
+ }
40567
+ /**
40568
+ * Resize from one grip.
40569
+ *
40570
+ * Each axis is adjusted from the edge that MOVED (west/north resize changes the
40571
+ * origin too), then clamped: the minimum size is anchored at the opposite edge
40572
+ * (so dragging the north grip past the bottom stops instead of flipping the
40573
+ * rect) and no edge may leave the viewport.
40574
+ */
40575
+ function resizeFloatRect(start, handle, dx, dy, viewport) {
40576
+ let left = start.x;
40577
+ let top = start.y;
40578
+ let right = start.x + start.width;
40579
+ let bottom = start.y + start.height;
40580
+ if (handle.includes("e")) right += dx;
40581
+ if (handle.includes("w")) left += dx;
40582
+ if (handle.includes("s")) bottom += dy;
40583
+ if (handle.includes("n")) top += dy;
40584
+ if (right - left < 360) if (handle.includes("w")) left = right - 360;
40585
+ else right = left + 360;
40586
+ if (bottom - top < 220) if (handle.includes("n")) top = bottom - 220;
40587
+ else bottom = top + 220;
40588
+ left = clamp(left, 0, Math.max(0, viewport.width - 360));
40589
+ top = clamp(top, 0, Math.max(0, viewport.height - 220));
40590
+ right = clamp(right, left + 360, viewport.width);
40591
+ bottom = clamp(bottom, top + 220, viewport.height);
40592
+ return {
40593
+ x: Math.round(left),
40594
+ y: Math.round(top),
40595
+ width: Math.round(right - left),
40596
+ height: Math.round(bottom - top)
40597
+ };
40598
+ }
40599
+ /** The rect a maximized window occupies: the frame minus a small margin. */
40600
+ function maximizeFloatRect(viewport) {
40601
+ const edge = 8;
40602
+ return {
40603
+ x: edge,
40604
+ y: edge,
40605
+ width: Math.max(360, viewport.width - edge * 2),
40606
+ height: Math.max(220, viewport.height - edge * 2)
40607
+ };
40608
+ }
40609
+ /**
40610
+ * Parse a persisted rect.
40611
+ *
40612
+ * Anything malformed is discarded rather than repaired into a guess: the
40613
+ * caller then opens the default rect. Values that are merely STALE (a rect from
40614
+ * another screen) are legal input and get clamped.
40615
+ */
40616
+ function parseFloatRect(raw, viewport) {
40617
+ if (typeof raw !== "string" || raw.trim() === "") return void 0;
40618
+ let parsed;
40619
+ try {
40620
+ parsed = JSON.parse(raw);
40621
+ } catch {
40622
+ return;
40623
+ }
40624
+ if (typeof parsed !== "object" || parsed === null) return void 0;
40625
+ const candidate = parsed;
40626
+ if (!finite(candidate.x) || !finite(candidate.y) || !finite(candidate.width) || !finite(candidate.height)) return;
40627
+ return clampFloatRect({
40628
+ x: candidate.x,
40629
+ y: candidate.y,
40630
+ width: candidate.width,
40631
+ height: candidate.height
40632
+ }, viewport);
40633
+ }
40634
+ /** Serialize a rect for localStorage. */
40635
+ function serializeFloatRect(rect) {
40636
+ return JSON.stringify({
40637
+ x: rect.x,
40638
+ y: rect.y,
40639
+ width: rect.width,
40640
+ height: rect.height
40641
+ });
40642
+ }
40643
+ /**
40644
+ * One Ctrl+wheel step: up (negative deltaY) grows the text, down shrinks it.
40645
+ * Values are whole pixels so the stored size stays stable across sessions.
40646
+ */
40647
+ function stepFloatZoom(current, deltaY) {
40648
+ if (!finite(current)) return 13;
40649
+ if (deltaY === 0) return clamp(Math.round(current), 9, 28);
40650
+ return clamp(Math.round(current) + (deltaY < 0 ? 1 : -1), 9, 28);
40651
+ }
40652
+ /** Parse the persisted editor font size (fallback = the default 13px). */
40653
+ function parseFloatFontSize(raw) {
40654
+ if (typeof raw !== "string") return 13;
40655
+ const value = Number(raw);
40656
+ if (!finite(value)) return 13;
40657
+ return clamp(Math.round(value), 9, 28);
40658
+ }
40659
+ /**
40660
+ * Where the minimized ball may sit: fully on screen, whatever position was
40661
+ * asked for (a ball restored on a smaller monitor must stay clickable).
40662
+ */
40663
+ function clampBallPosition(pos, viewport) {
40664
+ const maxX = Math.max(0, viewport.width - 44);
40665
+ const maxY = Math.max(0, viewport.height - 44);
40666
+ const x = finite(pos.x) ? pos.x : 0;
40667
+ const y = finite(pos.y) ? pos.y : 0;
40668
+ return {
40669
+ x: Math.round(clamp(x, 0, maxX)),
40670
+ y: Math.round(clamp(y, 0, maxY))
40671
+ };
40672
+ }
40673
+ /**
40674
+ * The ball's default spot when a window is minimized: its own top-left corner,
40675
+ * so the ball appears where the window was instead of jumping to a corner.
40676
+ */
40677
+ function ballFromRect(rect, viewport) {
40678
+ return clampBallPosition({
40679
+ x: rect.x,
40680
+ y: rect.y
40681
+ }, viewport);
40682
+ }
40683
+ /**
40684
+ * Whether a finished gesture was a CLICK rather than a drag.
40685
+ *
40686
+ * The minimized window is one small target with two actions on the same
40687
+ * pointer: a press that never really moved restores the window, a real drag
40688
+ * moves the ball. Without this distinction every move-by-3px would also
40689
+ * restore, and every click would be a zero-distance drag.
40690
+ */
40691
+ function isBallClick(dx, dy) {
40692
+ return Math.abs(dx) <= 4 && Math.abs(dy) <= 4;
40693
+ }
40694
+ /** Parse a persisted ball position (stale geometry is clamped, garbage dropped). */
40695
+ function parseBallPosition(raw, viewport) {
40696
+ if (typeof raw !== "string" || raw.trim() === "") return void 0;
40697
+ let parsed;
40698
+ try {
40699
+ parsed = JSON.parse(raw);
40700
+ } catch {
40701
+ return;
40702
+ }
40703
+ if (typeof parsed !== "object" || parsed === null) return void 0;
40704
+ const candidate = parsed;
40705
+ if (!finite(candidate.x) || !finite(candidate.y)) return void 0;
40706
+ return clampBallPosition({
40707
+ x: candidate.x,
40708
+ y: candidate.y
40709
+ }, viewport);
40710
+ }
40711
+ /** Serialize a ball position for localStorage. */
40712
+ function serializeBallPosition(pos) {
40713
+ return JSON.stringify({
40714
+ x: pos.x,
40715
+ y: pos.y
40716
+ });
40717
+ }
40329
40718
  //#endregion
40330
40719
  //#region src/client/shell.ts
40331
40720
  /**
@@ -40980,6 +41369,8 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
40980
41369
  activeTabId: void 0,
40981
41370
  revision: 0,
40982
41371
  editorSurface: "sidebar",
41372
+ floatOpen: false,
41373
+ floatAvailable: false,
40983
41374
  view: readStoredView(),
40984
41375
  searchRequest: void 0,
40985
41376
  revealRequest: void 0,
@@ -41054,7 +41445,9 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
41054
41445
  activeTab: state.tabs.find((item) => item.id === state.activeTabId),
41055
41446
  activeTabId: state.activeTabId,
41056
41447
  revision: state.revision,
41057
- editorSurface: state.editorSurface,
41448
+ editorSurface: effectiveEditorSurface(state.editorSurface, state.floatOpen),
41449
+ floatOpen: state.floatOpen,
41450
+ floatAvailable: state.floatAvailable,
41058
41451
  view: state.view,
41059
41452
  searchRequest: state.searchRequest,
41060
41453
  revealRequest: state.revealRequest,
@@ -41073,6 +41466,10 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
41073
41466
  untrackedDirs: state.untrackedDirs,
41074
41467
  toggleUntrackedDir,
41075
41468
  openFile,
41469
+ openInFloatEditor,
41470
+ openDiffInFloatEditor,
41471
+ openFloatingEditor,
41472
+ dockFloatingEditor,
41076
41473
  openDiff,
41077
41474
  openCommitFile,
41078
41475
  activateTab,
@@ -41699,7 +42096,7 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
41699
42096
  * is told where the file opened instead of being left guessing.
41700
42097
  */
41701
42098
  function revealEditor() {
41702
- if (state.editorSurface !== "center") return;
42099
+ if (effectiveEditorSurface(state.editorSurface, state.floatOpen) !== "center") return;
41703
42100
  const label = t("view.editor");
41704
42101
  if (revealRetry !== void 0) {
41705
42102
  clearTimeout(revealRetry);
@@ -41718,6 +42115,49 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
41718
42115
  } : tab) });
41719
42116
  }
41720
42117
  /**
42118
+ * Copy the live editor buffer onto its tab before the editor is remounted.
42119
+ *
42120
+ * Why this exists: `CodeEditor` reads its `initial` text ONCE, on mount
42121
+ * (`src/client/CodeEditor.tsx`), and the tab's stored text is only ever written
42122
+ * by a load/reload/handoff — typing lives exclusively in CodeMirror. Every tab
42123
+ * switch (a normal file click, another tab in the strip, a diff) and every
42124
+ * editor-surface flip therefore unmounts and remounts the editor, and without
42125
+ * this handoff the edits typed since the load would silently disappear (and a
42126
+ * later Ctrl+S would write the pre-edit text back).
42127
+ *
42128
+ * The text is written into the tab, which is exactly the input the replacement
42129
+ * mount reads: no extra channel exists or is needed. The DISK-side baseline is
42130
+ * deliberately left alone (`EditorTab.baseline`), so the remount still knows
42131
+ * the text is unsaved. The scroll + caret half of the handoff lives in
42132
+ * `live-editor.ts`.
42133
+ */
42134
+ function captureLiveBuffer() {
42135
+ captureLiveEditorPosition();
42136
+ const active = state.tabs.find((item) => item.id === state.activeTabId);
42137
+ if (active !== void 0 && active.kind === "file" && active.text === (active.baseline ?? active.text) && !isLiveEditorDirty()) return;
42138
+ const captured = captureLiveEditorText();
42139
+ if (captured === void 0) return;
42140
+ const tab = state.tabs.find((item) => item.id === captured.tabId);
42141
+ if (tab === void 0 || tab.kind !== "file" || tab.text === captured.text) return;
42142
+ patchTab(captured.tabId, { text: captured.text });
42143
+ }
42144
+ /**
42145
+ * Flip the editor into the floating surface, capturing the live buffer first.
42146
+ *
42147
+ * A no-op while already floating (the editor is not remounted then), and a
42148
+ * toast-only refusal on a host that cannot host the window — the caller still
42149
+ * opens the file, just on the normal surface.
42150
+ */
42151
+ function enterFloatSurface() {
42152
+ if (!state.floatAvailable) {
42153
+ toast(t("float.unavailable"), true);
42154
+ return;
42155
+ }
42156
+ if (state.floatOpen) return;
42157
+ captureLiveBuffer();
42158
+ patch({ floatOpen: true });
42159
+ }
42160
+ /**
41721
42161
  * Deadline for one git round trip.
41722
42162
  *
41723
42163
  * Was a fixed 45 s shared with file reads, which made a cold `git status` on a
@@ -41752,6 +42192,7 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
41752
42192
  }
41753
42193
  /** Open (or focus) a remote file in the editor tab strip. */
41754
42194
  function openFile(fullPath, options = {}) {
42195
+ captureLiveBuffer();
41755
42196
  const workspace = currentWorkspace();
41756
42197
  if (workspace === void 0 || fullPath === "") return;
41757
42198
  const id = `file:${workspace.alias}:${fullPath}`;
@@ -41800,7 +42241,8 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
41800
42241
  loading: false,
41801
42242
  binary: read.binary,
41802
42243
  truncated: read.truncated,
41803
- text: read.binary ? "" : read.text ?? ""
42244
+ text: read.binary ? "" : read.text ?? "",
42245
+ ...read.binary ? {} : { baseline: read.text ?? "" }
41804
42246
  });
41805
42247
  } catch (error) {
41806
42248
  patchTab(id, {
@@ -41838,12 +42280,21 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
41838
42280
  openFile(remotePath, options);
41839
42281
  return true;
41840
42282
  }
41841
- /** Whether the editor tab is mounted in the centre conversation view. */
41842
- function isEditorCentered() {
41843
- return state.editorSurface === "center";
42283
+ /**
42284
+ * Whether the plugin's own rich editor is the place a remote file click should
42285
+ * land (the click interceptor's gate, see `remote-file-open.ts`).
42286
+ *
42287
+ * The centre conversation view is the normal case; while the editor floats it
42288
+ * is the floating window instead — the file still belongs in OUR editor, not in
42289
+ * the host's native preview. On the sidebar-fallback layout (`sidebar`) the
42290
+ * interceptor stays off, exactly as before this feature.
42291
+ */
42292
+ function isEditorTakeoverActive() {
42293
+ return effectiveEditorSurface(state.editorSurface, state.floatOpen) !== "sidebar";
41844
42294
  }
41845
42295
  /** Open (or focus) a working-tree diff of one repository path. */
41846
42296
  function openDiff(rel, staged, untracked = false) {
42297
+ captureLiveBuffer();
41847
42298
  const workspace = currentWorkspace();
41848
42299
  if (workspace === void 0 || state.repoRoot === void 0 || rel === "") {
41849
42300
  toast(t("git.notARepo"), true);
@@ -41892,6 +42343,7 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
41892
42343
  }
41893
42344
  /** Open (or focus) one file of one commit (`ref^` against `ref`). */
41894
42345
  function openCommitFile(hash, rel) {
42346
+ captureLiveBuffer();
41895
42347
  const workspace = currentWorkspace();
41896
42348
  const root = state.repoRoot;
41897
42349
  if (workspace === void 0 || root === void 0 || rel === "") return;
@@ -41937,6 +42389,7 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
41937
42389
  })();
41938
42390
  }
41939
42391
  function activateTab(id) {
42392
+ captureLiveBuffer();
41940
42393
  patch({ activeTabId: id });
41941
42394
  }
41942
42395
  function closeTab(id) {
@@ -41950,14 +42403,14 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
41950
42403
  function updateTab(id, next) {
41951
42404
  patchTab(id, next);
41952
42405
  }
41953
- /** Write one tab back to the remote host. */
42406
+ /** Write one tab back to the remote host. @returns true when the write landed. */
41954
42407
  async function saveTab(id, content) {
41955
42408
  const workspace = currentWorkspace();
41956
42409
  const tab = state.tabs.find((item) => item.id === id);
41957
- if (workspace === void 0 || tab === void 0 || tab.kind !== "file" || tab.binary || tab.truncated) return;
42410
+ if (workspace === void 0 || tab === void 0 || tab.kind !== "file" || tab.binary || tab.truncated) return false;
41958
42411
  if (typeof content !== "string") {
41959
42412
  toast(t("editor.needSource"), true);
41960
- return;
42413
+ return false;
41961
42414
  }
41962
42415
  const text = content;
41963
42416
  patchTab(id, {
@@ -41966,15 +42419,42 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
41966
42419
  });
41967
42420
  try {
41968
42421
  await api.fileOp.write(workspace.alias, tab.fullPath, text);
42422
+ /**
42423
+ * The remote file now holds `text`, so that becomes the disk-side dirty
42424
+ * baseline (a later remount compares against it).
42425
+ *
42426
+ * The dirty FLAG is deliberately NOT cleared while the tab may still hold
42427
+ * something newer than what was written. Two ways that happens, and both
42428
+ * must keep the flag:
42429
+ *
42430
+ * - a live editor is mounted for this tab: the user may have typed while
42431
+ * the write was in flight, and only the editor can tell (it re-derives
42432
+ * through `markLiveEditorSaved` below);
42433
+ * - the tab was switched away from mid-write: its stored `text` is then the
42434
+ * handoff copy of that newer buffer, so comparing it with what was
42435
+ * written detects the difference without an editor.
42436
+ *
42437
+ * Only a tab whose stored text IS the written text, with no mounted editor,
42438
+ * has nothing newer — there the flag is cleared directly. (Clearing it
42439
+ * otherwise is how a tab read "saved" while its buffer was not: dot off,
42440
+ * Save disabled, close guard silent.)
42441
+ */
42442
+ const liveEditor = isLiveEditorFor(id);
42443
+ const current = state.tabs.find((item) => item.id === id);
42444
+ const newer = current !== void 0 && current.text !== text;
41969
42445
  patchTab(id, {
41970
42446
  saving: false,
41971
- dirty: false
42447
+ baseline: text,
42448
+ ...liveEditor || newer ? {} : { dirty: false }
41972
42449
  });
42450
+ markLiveEditorSaved(id, text);
41973
42451
  toast(`${t("editor.saved")} · ${tab.rel}`, false);
41974
42452
  await afterWrite();
42453
+ return true;
41975
42454
  } catch (error) {
41976
42455
  patchTab(id, { saving: false });
41977
42456
  toast(error instanceof Error ? error.message : String(error), true);
42457
+ return false;
41978
42458
  }
41979
42459
  }
41980
42460
  /**
@@ -42008,7 +42488,8 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
42008
42488
  text: read.binary ? "" : read.text ?? "",
42009
42489
  binary: read.binary,
42010
42490
  truncated: read.truncated,
42011
- dirty: false
42491
+ dirty: false,
42492
+ ...read.binary ? {} : { baseline: read.text ?? "" }
42012
42493
  });
42013
42494
  patchTab(id, { reloadNonce: (tab.reloadNonce ?? 0) + 1 });
42014
42495
  } catch (error) {
@@ -42037,6 +42518,88 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
42037
42518
  patch({ editorSurface: surface });
42038
42519
  }
42039
42520
  /**
42521
+ * Record that the host's frame-wide overlay slot actually rendered the
42522
+ * floating window host (see `FloatingEditorWindow`).
42523
+ *
42524
+ * This is a runtime handshake, not a configuration flag: on a host whose
42525
+ * `shell.overlay` slot is missing, the entry never mounts and the menu entries
42526
+ * stay disabled instead of opening a surface that cannot paint.
42527
+ */
42528
+ function setFloatEditorAvailable(available) {
42529
+ if (available) {
42530
+ if (state.floatAvailable) return;
42531
+ patch({ floatAvailable: true });
42532
+ return;
42533
+ }
42534
+ if (!state.floatAvailable && !state.floatOpen) return;
42535
+ captureLiveBuffer();
42536
+ patch({
42537
+ floatAvailable: false,
42538
+ floatOpen: false
42539
+ });
42540
+ }
42541
+ /**
42542
+ * Float the editor in its current state, without opening anything.
42543
+ *
42544
+ * The entry is a double-click on the shell's 「远程编辑器」 tab (see
42545
+ * `float-tab-trigger.ts`): the user is already looking at the editor and wants
42546
+ * it out of the layout — no file, no diff, same tabs and buffers.
42547
+ *
42548
+ * Idempotent: while the editor already floats this does nothing (the window is
42549
+ * on screen; a second trigger must not restart it).
42550
+ */
42551
+ function openFloatingEditor() {
42552
+ if (state.floatOpen) return;
42553
+ captureLiveBuffer();
42554
+ enterFloatSurface();
42555
+ }
42556
+ /**
42557
+ * Open one file in the floating editor (file-tree / search / SCM menu action).
42558
+ *
42559
+ * Order matters twice:
42560
+ * 1. the live buffer is captured BEFORE the surface flips (the flip unmounts
42561
+ * the editor that owns it);
42562
+ * 2. a second capture runs even when already floating, because opening
42563
+ * another file is a tab switch — the very action that remounts the editor
42564
+ * and would otherwise drop the edits typed in the tab being left.
42565
+ * A host whose overlay slot is unavailable gets the file in the normal surface
42566
+ * instead (with the reason toasted), so the click is never a dead end.
42567
+ */
42568
+ function openInFloatEditor(fullPath, options = {}) {
42569
+ if (currentWorkspace() === void 0 || fullPath === "") {
42570
+ openFile(fullPath, options);
42571
+ return;
42572
+ }
42573
+ captureLiveBuffer();
42574
+ enterFloatSurface();
42575
+ openFile(fullPath, options);
42576
+ }
42577
+ /**
42578
+ * The floating variant of `openDiff` — the SCM change list's entry.
42579
+ *
42580
+ * Same handoff order as `openInFloatEditor`; a diff tab has no text buffer of
42581
+ * its own, but the file editor that WAS on screen still does.
42582
+ */
42583
+ function openDiffInFloatEditor(rel, staged, untracked = false) {
42584
+ if (currentWorkspace() === void 0 || state.repoRoot === void 0 || rel === "") {
42585
+ openDiff(rel, staged, untracked);
42586
+ return;
42587
+ }
42588
+ captureLiveBuffer();
42589
+ enterFloatSurface();
42590
+ openDiff(rel, staged, untracked);
42591
+ }
42592
+ /**
42593
+ * Dock the editor back onto its base surface (centre conversation view or the
42594
+ * sidebar fallback), carrying the live buffer with it.
42595
+ */
42596
+ function dockFloatingEditor() {
42597
+ if (!state.floatOpen) return;
42598
+ captureLiveBuffer();
42599
+ patch({ floatOpen: false });
42600
+ revealEditor();
42601
+ }
42602
+ /**
42040
42603
  * 换一份「本会话改了什么」的标记。
42041
42604
  *
42042
42605
  * `activeHunk` 归零是刻意的:数据换了一份,上一份里"跳到第 3 处"的下标没有任何
@@ -43153,23 +43716,26 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
43153
43716
  * buffer rather than the previous file's undo stack, which is what we want,
43154
43717
  * but it also reset the scroll and the caret.
43155
43718
  *
43156
- * The editor REPORTS its position (see `CodeEditor`'s `onViewState`) instead
43157
- * of this component reading it back through `editorRef`, because the ref is
43158
- * already `null` by the time any cleanup here could run: React clears a
43159
- * child's ref before running the parent's cleanup. Both earlier attempts
43160
- * failed on exactly that — a `useEffect` keyed on the active tab captured the
43161
- * INCOMING tab's editor (always scrollTop 0), and a `useLayoutEffect` cleanup
43162
- * found `editorRef.current === null`. `.spike/tabswitch-check.mjs` reproduces
43163
- * both failures and pins the working wiring.
43719
+ * The map lives in `live-editor.ts`, NOT in a `useRef` here, because the pane
43720
+ * is unmounted when the editor floats out (and again when it docks back): a
43721
+ * per-component map would lose the position on the one surface swap that
43722
+ * most needs it. It survives there for the same reason the store does.
43164
43723
  *
43165
43724
  * Keyed by tab id alone, which is safe because only ONE `EditorPane` is ever
43166
43725
  * mounted: the centre surface when the host has a `conversation.view` slot,
43167
- * otherwise the sidebar fallback. With two live editors for the same tab the
43168
- * hidden one would report its untouched scrollTop 0 and overwrite the visible
43169
- * one's, snapping every switch back to the top — so `RemoteRoot` gates both
43170
- * mounts on `editorSurface` and must keep doing so.
43726
+ * the floating window while the editor floats, otherwise the sidebar
43727
+ * fallback. With two live editors for the same tab the hidden one would
43728
+ * report its untouched scrollTop 0 and overwrite the visible one's, snapping
43729
+ * every switch back to the top — so `RemoteRoot`, `RemoteEditorView` and the
43730
+ * floating window all gate their mounts on `editorSurface` and must keep
43731
+ * doing so.
43732
+ *
43733
+ * The editor REPORTS its position (see `CodeEditor`'s `onViewState`) instead
43734
+ * of this component reading it back through `editorRef`, because the ref is
43735
+ * already `null` by the time any cleanup here could run: React clears a
43736
+ * child's ref before running the parent's cleanup. `.spike/tabswitch-check.mjs`
43737
+ * reproduces the failed variants and pins the working wiring.
43171
43738
  */
43172
- const viewStates = (0, react.useRef)(/* @__PURE__ */ new Map());
43173
43739
  /**
43174
43740
  * Store a reading under the tab that produced it.
43175
43741
  *
@@ -43185,19 +43751,41 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
43185
43751
  * accept a report for any tab.
43186
43752
  */
43187
43753
  const rememberViewState = (0, react.useCallback)((tabId, state) => {
43188
- viewStates.current.set(tabId, state);
43754
+ rememberEditorViewState(tabId, state);
43189
43755
  }, []);
43190
43756
  /**
43191
- * Closed tabs may not keep their scroll + caret. This map is keyed by tab id
43192
- * and was only ever written, and the pane is mounted for the life of the
43193
- * page: a session that opened a few hundred files held a state object for
43194
- * every one of them, long after the tab was gone.
43757
+ * Closed tabs may not keep their scroll + caret. The map is keyed by tab id
43758
+ * and is only ever written: a session that opened a few hundred files would
43759
+ * otherwise hold a state object for every one of them, long after the tab is
43760
+ * gone.
43195
43761
  */
43196
43762
  (0, react.useEffect)(() => {
43197
- const live = new Set(tabs.map((tab) => tab.id));
43198
- for (const id of [...viewStates.current.keys()]) if (!live.has(id)) viewStates.current.delete(id);
43763
+ pruneEditorViewStates(new Set(tabs.map((tab) => tab.id)));
43199
43764
  }, [tabs]);
43200
- activeTab !== void 0 && activeTab.kind === "file" && !activeTab.image && !activeTab.binary && !activeTab.loading && activeTab.error;
43765
+ /**
43766
+ * True only while the CodeMirror instance for the active tab is mounted.
43767
+ *
43768
+ * The editor used to be conditionally unmounted in preview mode, which made
43769
+ * `getContent()` return undefined; the save path then fell back to the
43770
+ * tab's loaded text and wrote the PRE-EDIT bytes back to the remote file
43771
+ * while reporting success. Refusing to save without a live buffer is the
43772
+ * load-bearing half of that fix — the editor staying mounted (below) is the
43773
+ * other half, so switching preview no longer drops unsaved edits either.
43774
+ */
43775
+ const editorMounted = activeTab !== void 0 && activeTab.kind === "file" && !activeTab.image && !activeTab.binary && !activeTab.loading && activeTab.error === void 0;
43776
+ /**
43777
+ * Advertise the live editor so the store can snapshot it before a surface
43778
+ * flip (see `captureLiveEditor`): the floating window is a different mount of
43779
+ * the SAME pane, so switching to it unmounts this component.
43780
+ *
43781
+ * The getter is `() => editorRef.current`, not the handle itself: the handle
43782
+ * only exists after `CodeEditor` mounts, and it is cleared before the
43783
+ * cleanup that would otherwise run here.
43784
+ */
43785
+ (0, react.useEffect)(() => {
43786
+ if (!editorMounted || activeTab === void 0) return;
43787
+ return registerLiveEditor(activeTab.id, () => editorRef.current);
43788
+ }, [editorMounted, activeTab?.id]);
43201
43789
  const saveActive = async () => {
43202
43790
  const tab = activeTab;
43203
43791
  if (tab === void 0) return;
@@ -43314,8 +43902,8 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
43314
43902
  }),
43315
43903
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
43316
43904
  className: "rvs-build",
43317
- title: `dsh-remote-vs build 1026-0953`,
43318
- children: "1026-0953"
43905
+ title: `dsh-remote-vs build 1027-1757`,
43906
+ children: "1027-1757"
43319
43907
  }),
43320
43908
  tokenCount !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
43321
43909
  className: "rvs-build",
@@ -43439,6 +44027,7 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
43439
44027
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CodeEditor, {
43440
44028
  ref: editorRef,
43441
44029
  initial: activeTab.text,
44030
+ savedText: activeTab.baseline,
43442
44031
  path: activeTab.fullPath,
43443
44032
  tabId: activeTab.id,
43444
44033
  readOnly: activeTab.truncated,
@@ -43453,7 +44042,7 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
43453
44042
  ...activeTab.changeSet === void 0 ? {} : { changeSet: activeTab.changeSet },
43454
44043
  ...activeTab.revealHunkLine === void 0 ? {} : { revealHunkLine: activeTab.revealHunkLine },
43455
44044
  ...activeTab.revealHunkNonce === void 0 ? {} : { revealHunkNonce: activeTab.revealHunkNonce },
43456
- ...viewStates.current.get(activeTab.id) === void 0 ? {} : { viewState: viewStates.current.get(activeTab.id) },
44045
+ ...editorViewStateOf(activeTab.id) === void 0 ? {} : { viewState: editorViewStateOf(activeTab.id) },
43457
44046
  onTokens: setTokenCount,
43458
44047
  onViewState: rememberViewState,
43459
44048
  onStepHunk: (dir) => {
@@ -43478,6 +44067,518 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
43478
44067
  });
43479
44068
  }
43480
44069
  //#endregion
44070
+ //#region src/client/FloatingEditorWindow.tsx
44071
+ /**
44072
+ * The floating editor window: the plugin's ONE editor pane hosted in a
44073
+ * draggable, resizable frame above the app shell.
44074
+ *
44075
+ * It is mounted by the client entry into the host's frame-wide overlay slot
44076
+ * (`shell.overlay`) and reads the same module-level store as every other
44077
+ * surface, so tabs / previews / diffs / find / change marks / undo / @file are
44078
+ * not a re-implementation: this file only owns the FRAME (position, size,
44079
+ * z-order, drag + resize + zoom) and the handoff around the mount swap.
44080
+ *
44081
+ * The mount is exclusive: `EditorPane` must exist exactly once, so the centre
44082
+ * conversation view and the sidebar fallback render a placeholder while
44083
+ * `editorSurface === 'float'` (see `FloatingEditorPlaceholder`).
44084
+ */
44085
+ /** Where the last window rect is remembered (per browser/profile). */
44086
+ const RECT_KEY = "dsh-remote-vs.float.rect";
44087
+ /** Where the last editor font size is remembered. */
44088
+ const FONT_KEY = "dsh-remote-vs.float.font";
44089
+ /** Where the minimized ball's spot is remembered. */
44090
+ const BALL_KEY = "dsh-remote-vs.float.ball";
44091
+ /** The resize grips, in the order they are rendered. */
44092
+ const RESIZE_HANDLES = [
44093
+ "n",
44094
+ "s",
44095
+ "e",
44096
+ "w",
44097
+ "ne",
44098
+ "nw",
44099
+ "se",
44100
+ "sw"
44101
+ ];
44102
+ function readViewport() {
44103
+ return {
44104
+ width: window.innerWidth,
44105
+ height: window.innerHeight
44106
+ };
44107
+ }
44108
+ function readStoredRect() {
44109
+ const viewport = readViewport();
44110
+ try {
44111
+ const stored = parseFloatRect(localStorage.getItem(RECT_KEY), viewport);
44112
+ if (stored !== void 0) return stored;
44113
+ } catch {}
44114
+ return defaultFloatRect(viewport);
44115
+ }
44116
+ function readStoredFontSize() {
44117
+ try {
44118
+ return parseFloatFontSize(localStorage.getItem(FONT_KEY));
44119
+ } catch {
44120
+ return 13;
44121
+ }
44122
+ }
44123
+ /**
44124
+ * The ball's remembered spot, or undefined the first time.
44125
+ *
44126
+ * Read at open, not at minimize: the spot belongs to the feature (per browser
44127
+ * profile), and re-deriving it from the window corner on every minimize is what
44128
+ * made the "remembered" claim false. A stale spot from a bigger screen is
44129
+ * clamped by the parser, so it stays clickable.
44130
+ */
44131
+ function readStoredBall() {
44132
+ const viewport = readViewport();
44133
+ try {
44134
+ return parseBallPosition(localStorage.getItem(BALL_KEY), viewport);
44135
+ } catch {
44136
+ return;
44137
+ }
44138
+ }
44139
+ /** The window frame plus the editor. `ctx` is only needed for the boot hook. */
44140
+ function FloatingEditorWindow(props) {
44141
+ useBootstrap(props.ctx);
44142
+ const store = useExplorer();
44143
+ const t = store.t;
44144
+ const open = store.floatOpen;
44145
+ const activeTab = store.activeTab;
44146
+ const [rect, setRect] = (0, react.useState)(void 0);
44147
+ const [viewport, setViewport] = (0, react.useState)(() => typeof window === "undefined" ? {
44148
+ width: 0,
44149
+ height: 0
44150
+ } : readViewport());
44151
+ const [fontSize, setFontSize] = (0, react.useState)(13);
44152
+ const [maximized, setMaximized] = (0, react.useState)(false);
44153
+ const [collapsed, setCollapsed] = (0, react.useState)(false);
44154
+ const [dragging, setDragging] = (0, react.useState)(false);
44155
+ /** The minimized ball's origin; undefined until the first minimize. */
44156
+ const [ball, setBall] = (0, react.useState)(void 0);
44157
+ const viewportRef = (0, react.useRef)(viewport);
44158
+ viewportRef.current = viewport;
44159
+ const dragRef = (0, react.useRef)(void 0);
44160
+ /** The normal (non-maximized) rect, restored by the restore button. */
44161
+ const restoreRectRef = (0, react.useRef)(void 0);
44162
+ const bodyRef = (0, react.useRef)(null);
44163
+ /** Latest font size, so the wheel handler never has to read stale state. */
44164
+ const fontRef = (0, react.useRef)(fontSize);
44165
+ fontRef.current = fontSize;
44166
+ /** True once the frame has a resolved rect (body/zoom effects depend on it). */
44167
+ const rectReady = rect !== void 0;
44168
+ /** Previous `open`, so "opened again" can reset the minimized state. */
44169
+ const wasOpenRef = (0, react.useRef)(open);
44170
+ /** The geometry a drag/resize gesture points at right now (move AND finalize). */
44171
+ const resolveDrag = (drag, pointerX, pointerY) => {
44172
+ const dx = pointerX - drag.pointerX;
44173
+ const dy = pointerY - drag.pointerY;
44174
+ if (drag.kind === "ball") return clampBallPosition({
44175
+ x: drag.start.x + dx,
44176
+ y: drag.start.y + dy
44177
+ }, viewportRef.current);
44178
+ return drag.kind === "move" ? dragFloatRect(drag.rect, dx, dy, viewportRef.current) : resizeFloatRect(drag.rect, drag.handle, dx, dy, viewportRef.current);
44179
+ };
44180
+ /**
44181
+ * Runtime handshake with the store (see `setFloatEditorAvailable`): the menu
44182
+ * entry is enabled only once this component really mounted inside the host's
44183
+ * overlay slot. A host without that slot never reaches this effect.
44184
+ */
44185
+ (0, react.useEffect)(() => {
44186
+ setFloatEditorAvailable(true);
44187
+ return () => setFloatEditorAvailable(false);
44188
+ }, []);
44189
+ (0, react.useEffect)(() => {
44190
+ if (!open || rect !== void 0) return;
44191
+ const next = readViewport();
44192
+ setViewport(next);
44193
+ setRect(readStoredRect());
44194
+ setFontSize(readStoredFontSize());
44195
+ setBall(readStoredBall());
44196
+ }, [open, rect]);
44197
+ /**
44198
+ * A window that was minimized when it was docked must not come back as a
44199
+ * bare 34px title bar: "open in floating editor" would look broken. Every
44200
+ * fresh open starts expanded; a minimize made WHILE open is kept.
44201
+ */
44202
+ (0, react.useEffect)(() => {
44203
+ if (open && !wasOpenRef.current) setCollapsed(false);
44204
+ wasOpenRef.current = open;
44205
+ }, [open]);
44206
+ (0, react.useEffect)(() => {
44207
+ const onResize = () => {
44208
+ const next = readViewport();
44209
+ setViewport(next);
44210
+ setBall((prev) => prev === void 0 ? prev : clampBallPosition(prev, next));
44211
+ setRect((prev) => {
44212
+ if (prev === void 0) return prev;
44213
+ return maximized ? maximizeFloatRect(next) : clampFloatRect(prev, next);
44214
+ });
44215
+ };
44216
+ window.addEventListener("resize", onResize);
44217
+ return () => window.removeEventListener("resize", onResize);
44218
+ }, [maximized]);
44219
+ const persistRect = (0, react.useCallback)((next) => {
44220
+ try {
44221
+ localStorage.setItem(RECT_KEY, serializeFloatRect(next));
44222
+ } catch {}
44223
+ }, []);
44224
+ const persistBall = (0, react.useCallback)((next) => {
44225
+ try {
44226
+ localStorage.setItem(BALL_KEY, serializeBallPosition(next));
44227
+ } catch {}
44228
+ }, []);
44229
+ /**
44230
+ * Drag / resize move on the WINDOW, not on the grip: the pointer regularly
44231
+ * leaves the 4px handle mid-gesture, and pointer capture on a grip that
44232
+ * re-renders is easy to lose. One listener pair per gesture is enough.
44233
+ */
44234
+ (0, react.useEffect)(() => {
44235
+ if (!dragging) return;
44236
+ const onMove = (event) => {
44237
+ const drag = dragRef.current;
44238
+ if (drag === void 0) return;
44239
+ if ((event.buttons & 1) === 0) {
44240
+ dragRef.current = void 0;
44241
+ setDragging(false);
44242
+ return;
44243
+ }
44244
+ const next = resolveDrag(drag, event.clientX, event.clientY);
44245
+ if (drag.kind === "ball") setBall(next);
44246
+ else setRect(next);
44247
+ };
44248
+ const onUp = (event) => {
44249
+ const drag = dragRef.current;
44250
+ dragRef.current = void 0;
44251
+ setDragging(false);
44252
+ if (drag === void 0) return;
44253
+ const final = resolveDrag(drag, event.clientX, event.clientY);
44254
+ if (drag.kind === "ball") {
44255
+ const dropped = final;
44256
+ setBall(dropped);
44257
+ persistBall(dropped);
44258
+ if (isBallClick(event.clientX - drag.pointerX, event.clientY - drag.pointerY)) setCollapsed(false);
44259
+ return;
44260
+ }
44261
+ if (maximized) return;
44262
+ const nextRect = final;
44263
+ setRect(nextRect);
44264
+ persistRect(nextRect);
44265
+ };
44266
+ /**
44267
+ * A cancelled gesture must NOT be finalised from the event: Chromium
44268
+ * reports `clientX/clientY = 0` on pointercancel, which would clamp the
44269
+ * window to the top-left corner and PERSIST that. Keep the last painted
44270
+ * rect instead.
44271
+ */
44272
+ const onCancel = () => {
44273
+ dragRef.current = void 0;
44274
+ setDragging(false);
44275
+ };
44276
+ window.addEventListener("pointermove", onMove);
44277
+ window.addEventListener("pointerup", onUp);
44278
+ window.addEventListener("pointercancel", onCancel);
44279
+ return () => {
44280
+ window.removeEventListener("pointermove", onMove);
44281
+ window.removeEventListener("pointerup", onUp);
44282
+ window.removeEventListener("pointercancel", onCancel);
44283
+ };
44284
+ }, [
44285
+ dragging,
44286
+ maximized,
44287
+ persistRect,
44288
+ persistBall
44289
+ ]);
44290
+ /**
44291
+ * Ctrl/Cmd + wheel zooms the EDITOR TEXT, not the page.
44292
+ *
44293
+ * A native listener is required: React's `onWheel` is passive at the root, so
44294
+ * `preventDefault()` there would be ignored (the browser would page-zoom).
44295
+ * CodeMirror picks the new size up through its ResizeObserver; the dispatched
44296
+ * resize event is the belt for hosts where the observer does not fire.
44297
+ */
44298
+ (0, react.useEffect)(() => {
44299
+ const element = bodyRef.current;
44300
+ if (element === null) return;
44301
+ const onWheel = (event) => {
44302
+ if (!(event.ctrlKey || event.metaKey)) return;
44303
+ event.preventDefault();
44304
+ const next = stepFloatZoom(fontRef.current, event.deltaY);
44305
+ if (next === fontRef.current) return;
44306
+ fontRef.current = next;
44307
+ setFontSize(next);
44308
+ try {
44309
+ localStorage.setItem(FONT_KEY, String(next));
44310
+ } catch {}
44311
+ window.requestAnimationFrame(() => window.dispatchEvent(new Event("resize")));
44312
+ };
44313
+ element.addEventListener("wheel", onWheel, { passive: false });
44314
+ return () => element.removeEventListener("wheel", onWheel);
44315
+ }, [
44316
+ open,
44317
+ collapsed,
44318
+ rectReady
44319
+ ]);
44320
+ const beginDrag = (event, drag) => {
44321
+ if (event.button !== 0) return;
44322
+ event.preventDefault();
44323
+ try {
44324
+ event.currentTarget.setPointerCapture(event.pointerId);
44325
+ } catch {}
44326
+ dragRef.current = drag;
44327
+ setDragging(true);
44328
+ };
44329
+ const toggleMaximize = () => {
44330
+ if (rect === void 0) return;
44331
+ const next = readViewport();
44332
+ setViewport(next);
44333
+ if (maximized) {
44334
+ setMaximized(false);
44335
+ setCollapsed(false);
44336
+ setRect(clampFloatRect(restoreRectRef.current ?? defaultFloatRect(next), next));
44337
+ return;
44338
+ }
44339
+ restoreRectRef.current = rect;
44340
+ setMaximized(true);
44341
+ setCollapsed(false);
44342
+ setRect(maximizeFloatRect(next));
44343
+ };
44344
+ /**
44345
+ * Minimize to the floating ball.
44346
+ *
44347
+ * The ball appears at the window's own corner first (a minimize should not
44348
+ * teleport the affordance across the screen), and keeps wherever the user
44349
+ * drags it afterwards — persisted per browser profile.
44350
+ */
44351
+ const toggleCollapse = () => {
44352
+ if (!collapsed && rect !== void 0) setBall(ball === void 0 ? ballFromRect(rect, viewportRef.current) : clampBallPosition(ball, viewportRef.current));
44353
+ setCollapsed(!collapsed);
44354
+ };
44355
+ /**
44356
+ * The body subtree is memoised on `collapsed` alone so a drag/resize update
44357
+ * (one per pointermove) does not re-render the whole EditorPane through the
44358
+ * parent: the element identity stays stable while only the frame geometry
44359
+ * moves. The pane is still hidden, never unmounted, while minimized — it owns
44360
+ * the live CodeMirror buffer.
44361
+ *
44362
+ * HOOK ORDER: this must stay ABOVE the `!open || rect === undefined` early
44363
+ * return. Hooks cannot be skipped on a render, and the first open reaches
44364
+ * this line one render later than every previous render did — placing it
44365
+ * below the return throws "Rendered more hooks than during the previous
44366
+ * render" exactly when the feature is used (in production too; React's
44367
+ * error 310 is not dev-only).
44368
+ */
44369
+ const body = (0, react.useMemo)(() => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
44370
+ className: "rvs-float-body",
44371
+ ref: bodyRef,
44372
+ style: { display: collapsed ? "none" : "flex" },
44373
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EditorPane, { visible: !collapsed })
44374
+ }), [collapsed]);
44375
+ if (!open || rect === void 0) return null;
44376
+ /**
44377
+ * A minimized window is a BALL, not a full-width bar: it keeps a title-bar
44378
+ * strip's worth of information (that the editor is out, which file, whether it
44379
+ * is dirty) in one small circle the user can park anywhere.
44380
+ */
44381
+ const ballPoint = clampBallPosition(ball ?? ballFromRect(rect, viewport), viewport);
44382
+ const frameStyle = collapsed ? {
44383
+ left: ballPoint.x,
44384
+ top: ballPoint.y,
44385
+ width: 44,
44386
+ height: 44
44387
+ } : {
44388
+ left: rect.x,
44389
+ top: rect.y,
44390
+ width: rect.width,
44391
+ height: rect.height,
44392
+ ["--rvs-editor-font-size"]: `${fontSize}px`
44393
+ };
44394
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
44395
+ className: `rvs-root rvs-float-window${collapsed ? " rvs-float-mini" : ""}${dragging ? " dragging" : ""}`,
44396
+ style: frameStyle,
44397
+ "data-rvs-float": collapsed ? "collapsed" : "open",
44398
+ "data-rvs-float-maximized": maximized ? "true" : void 0,
44399
+ title: collapsed ? t("float.ballHint") : void 0,
44400
+ onPointerDown: collapsed ? (event) => {
44401
+ if (event.button !== 0) return;
44402
+ beginDrag(event, {
44403
+ kind: "ball",
44404
+ pointerX: event.clientX,
44405
+ pointerY: event.clientY,
44406
+ start: ballPoint
44407
+ });
44408
+ } : void 0,
44409
+ children: [
44410
+ collapsed ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
44411
+ className: "rvs-float-ball",
44412
+ role: "button",
44413
+ tabIndex: 0,
44414
+ "aria-label": t("float.ballHint"),
44415
+ title: t("float.ballHint"),
44416
+ onKeyDown: (event) => {
44417
+ if (event.key === "Enter" || event.key === " ") {
44418
+ event.preventDefault();
44419
+ setCollapsed(false);
44420
+ }
44421
+ },
44422
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
44423
+ className: "rvs-float-ball-glyph",
44424
+ "aria-hidden": "true",
44425
+ children: "▣"
44426
+ }), activeTab !== void 0 && activeTab.dirty && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
44427
+ className: "rvs-float-ball-dot",
44428
+ title: t("editor.unsaved"),
44429
+ "aria-hidden": "true"
44430
+ })]
44431
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
44432
+ className: "rvs-float-titlebar",
44433
+ onPointerDown: (event) => {
44434
+ if (rect === void 0 || maximized) return;
44435
+ if (event.target.closest("button") !== null) return;
44436
+ beginDrag(event, {
44437
+ kind: "move",
44438
+ pointerX: event.clientX,
44439
+ pointerY: event.clientY,
44440
+ rect
44441
+ });
44442
+ },
44443
+ onDoubleClick: (event) => {
44444
+ if (event.target.closest("button") !== null) return;
44445
+ toggleMaximize();
44446
+ },
44447
+ title: t("float.dragHint"),
44448
+ children: [
44449
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
44450
+ className: "rvs-float-glyph",
44451
+ "aria-hidden": "true",
44452
+ children: "▣"
44453
+ }),
44454
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
44455
+ className: "rvs-float-title",
44456
+ children: t("float.title")
44457
+ }),
44458
+ activeTab !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
44459
+ className: "rvs-float-file",
44460
+ title: activeTab.fullPath,
44461
+ children: activeTab.rel
44462
+ }),
44463
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "rvs-float-spacer" }),
44464
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
44465
+ type: "button",
44466
+ className: "rvs-float-btn",
44467
+ title: t("float.minimize"),
44468
+ "aria-label": t("float.minimize"),
44469
+ onClick: toggleCollapse,
44470
+ children: "—"
44471
+ }),
44472
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
44473
+ type: "button",
44474
+ className: "rvs-float-btn",
44475
+ title: maximized ? t("float.restore") : t("float.maximize"),
44476
+ "aria-label": maximized ? t("float.restore") : t("float.maximize"),
44477
+ onClick: toggleMaximize,
44478
+ children: maximized ? "❐" : "□"
44479
+ }),
44480
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
44481
+ type: "button",
44482
+ className: "rvs-float-btn rvs-float-close",
44483
+ title: t("float.dock"),
44484
+ "aria-label": t("float.dock"),
44485
+ onClick: () => {
44486
+ store.dockFloatingEditor();
44487
+ },
44488
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconClose, { size: 12 })
44489
+ })
44490
+ ]
44491
+ }),
44492
+ body,
44493
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DialogHost, {}),
44494
+ !collapsed && !maximized && RESIZE_HANDLES.map((handle) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
44495
+ className: `rvs-float-resize rvs-float-resize-${handle}`,
44496
+ "data-rvs-float-resize": handle,
44497
+ onPointerDown: (event) => {
44498
+ if (rect === void 0) return;
44499
+ beginDrag(event, {
44500
+ kind: "resize",
44501
+ handle,
44502
+ pointerX: event.clientX,
44503
+ pointerY: event.clientY,
44504
+ rect
44505
+ });
44506
+ }
44507
+ }, handle))
44508
+ ]
44509
+ });
44510
+ }
44511
+ //#endregion
44512
+ //#region src/client/float-tab-trigger.ts
44513
+ /**
44514
+ * Double-click on the shell's 「远程编辑器」 tab floats the editor.
44515
+ *
44516
+ * That tab belongs to the host's conversation-view header, not to us: the plugin
44517
+ * registers a VIEW into the shell's tab strip and never sees the tab row's DOM.
44518
+ * There is no public service for "the user double-clicked my tab" either, so the
44519
+ * only way to honour the gesture is a document-level `dblclick` listener that
44520
+ * recognises the tab the same way `reveal.ts` does for clicks — by its label,
44521
+ * skipping our own surfaces.
44522
+ *
44523
+ * Fail-safe by construction: a miss (another host build, a renamed tab, a
44524
+ * double-click somewhere else) simply does nothing. It never prevents the
44525
+ * default action, never throws, and does not steal double-clicks from the
44526
+ * plugin's own UI (`.rvs-root`) or from dialogs.
44527
+ */
44528
+ /** Elements that behave like the shell's view tabs. */
44529
+ const TAB_SELECTOR = "button, [role=\"tab\"], [role=\"button\"]";
44530
+ /**
44531
+ * Install the double-click trigger.
44532
+ *
44533
+ * @param label - the tab's visible label (the same localized string the view
44534
+ * registered with); matched against the tab's trimmed text.
44535
+ * @param onTrigger - called once per recognised double-click.
44536
+ * @returns disposer removing the listener.
44537
+ */
44538
+ function installFloatTabTrigger(label, onTrigger) {
44539
+ if (typeof document === "undefined" || label === "") return () => {};
44540
+ const handler = (event) => {
44541
+ if (event.button !== 0) return;
44542
+ const target = event.target;
44543
+ if (!(target instanceof Element)) return;
44544
+ if (target.closest(".rvs-root") !== null) return;
44545
+ const tab = target.closest(TAB_SELECTOR);
44546
+ if (tab === null) return;
44547
+ if ((tab.textContent ?? "").trim() !== label) return;
44548
+ try {
44549
+ onTrigger();
44550
+ } catch {}
44551
+ };
44552
+ document.addEventListener("dblclick", handler, true);
44553
+ return () => document.removeEventListener("dblclick", handler, true);
44554
+ }
44555
+ //#endregion
44556
+ //#region src/client/FloatingPlaceholder.tsx
44557
+ function FloatingEditorPlaceholder() {
44558
+ const store = useExplorer();
44559
+ const t = store.t;
44560
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
44561
+ className: "rvs-float-placeholder",
44562
+ children: [
44563
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
44564
+ className: "rvs-float-placeholder-title",
44565
+ children: t("float.placeholder")
44566
+ }),
44567
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
44568
+ className: "rvs-float-placeholder-text",
44569
+ children: t("float.placeholderHint")
44570
+ }),
44571
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Btn, {
44572
+ kind: "primary",
44573
+ onClick: () => {
44574
+ store.dockFloatingEditor();
44575
+ },
44576
+ children: t("float.dockBack")
44577
+ })
44578
+ ]
44579
+ });
44580
+ }
44581
+ //#endregion
43481
44582
  //#region src/fs-ops.ts
43482
44583
  /**
43483
44584
  * Remote file-manager verbs the SFTP route does not offer: recursive copy and
@@ -44658,6 +45759,13 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
44658
45759
  onSelect: () => openEntry(entry),
44659
45760
  separated: true
44660
45761
  });
45762
+ if (!entry.isDir) entries.push({
45763
+ label: t("files.openInFloat"),
45764
+ onSelect: () => {
45765
+ store.openInFloatEditor(entry.path);
45766
+ },
45767
+ disabled: !store.floatAvailable
45768
+ });
44661
45769
  if (!entry.isDir && isPreviewOnlyFile(entry.path)) entries.push({
44662
45770
  label: t("files.openAsText"),
44663
45771
  onSelect: () => store.openFile(entry.path)
@@ -45546,12 +46654,13 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
45546
46654
  };
45547
46655
  };
45548
46656
  /**
45549
- * Right-click on a name result: the four things one does with a found path.
46657
+ * Right-click on a name result: what one does with a found path.
45550
46658
  *
45551
46659
  * "Relative" is relative to the REPOSITORY when the workspace sits inside one
45552
46660
  * (the same relative form the tree's own menu copies), else absolute — which
45553
46661
  * is what makes the copied value paste straight into a shell that already ran
45554
- * `cd` into the repo.
46662
+ * `cd` into the repo. Directories only get the copy/reveal/terminal entries:
46663
+ * there is no file to open in the editor.
45555
46664
  */
45556
46665
  const openHitMenu = (event, hit) => {
45557
46666
  event.preventDefault();
@@ -45574,12 +46683,19 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
45574
46683
  }
45575
46684
  },
45576
46685
  {
45577
- label: t("search.revealInExplorer"),
46686
+ label: t("files.openInFloat"),
45578
46687
  onSelect: () => {
45579
- store.revealInExplorer(dir);
46688
+ store.openInFloatEditor(abs);
45580
46689
  },
46690
+ disabled: !store.floatAvailable || hit.isDir,
45581
46691
  separated: true
45582
46692
  },
46693
+ {
46694
+ label: t("search.revealInExplorer"),
46695
+ onSelect: () => {
46696
+ store.revealInExplorer(dir);
46697
+ }
46698
+ },
45583
46699
  {
45584
46700
  label: t("search.openInTerminal"),
45585
46701
  onSelect: () => {
@@ -46547,6 +47663,8 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
46547
47663
  url: ""
46548
47664
  });
46549
47665
  const [picker, setPicker] = (0, react.useState)(false);
47666
+ /** Right-click menu of one change row (the floating-editor entry). */
47667
+ const menu = useContextMenu();
46550
47668
  const fold = (id) => setFolded((prev) => ({
46551
47669
  ...prev,
46552
47670
  [id]: prev[id] !== true
@@ -46687,6 +47805,33 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
46687
47805
  }
46688
47806
  store.openDiff(entry.path, entry.staged, entry.status === "?");
46689
47807
  };
47808
+ /**
47809
+ * The same destination as `openChange`, but in the floating editor.
47810
+ *
47811
+ * Kept in step with `openChange` on purpose: a conflicted path and a
47812
+ * preview-only file are opened as FILES (there is no usable diff), everything
47813
+ * else as the working-tree diff the row click would show.
47814
+ */
47815
+ const openChangeInFloat = (entry) => {
47816
+ if (entry.path.endsWith("/")) return;
47817
+ const abs = `${repoRoot?.replace(/\/+$/, "") ?? store.root}/${entry.path}`;
47818
+ if (entry.status === "U" || isPreviewOnlyFile(entry.path)) {
47819
+ store.openInFloatEditor(abs);
47820
+ if (entry.status === "U") toast(t("scm.conflictOpen"), false);
47821
+ return;
47822
+ }
47823
+ store.openDiffInFloatEditor(entry.path, entry.staged, entry.status === "?");
47824
+ };
47825
+ /** Right-click on one change row: the floating-editor entry. */
47826
+ const openRowMenu = (event, entry) => {
47827
+ event.preventDefault();
47828
+ event.stopPropagation();
47829
+ menu.open(event.clientX, event.clientY, [{
47830
+ label: t("files.openInFloat"),
47831
+ onSelect: () => openChangeInFloat(entry),
47832
+ disabled: !store.floatAvailable || entry.path.endsWith("/")
47833
+ }]);
47834
+ };
46690
47835
  const doCommit = async () => {
46691
47836
  if (repoRoot === void 0 || message.trim() === "") return;
46692
47837
  await guard("commit", async () => {
@@ -46803,6 +47948,7 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
46803
47948
  style: depth === 0 ? void 0 : { paddingLeft: `${22 + depth * 12}px` },
46804
47949
  title: expandable ? `${entry.path}\n${t("scm.dirToggle")}` : `${entry.path}\n${t("scm.openHint")}`,
46805
47950
  onClick: activate,
47951
+ onContextMenu: (event) => openRowMenu(event, entry),
46806
47952
  onKeyDown: (e) => {
46807
47953
  if (e.key === "Enter" || e.key === " ") activate();
46808
47954
  },
@@ -47454,6 +48600,12 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
47454
48600
  })
47455
48601
  }))
47456
48602
  ] })]
48603
+ }),
48604
+ menu.menu !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ContextMenu, {
48605
+ x: menu.menu.x,
48606
+ y: menu.menu.y,
48607
+ entries: menu.menu.entries,
48608
+ onClose: menu.close
47457
48609
  })
47458
48610
  ]
47459
48611
  });
@@ -61378,9 +62530,14 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
61378
62530
  * that owns it was gone AND the centre tab mounted a second EditorPane on
61379
62531
  * the same store tab — two live buffers with independent undo state, the one
61380
62532
  * state this file declares must never happen.
62533
+ *
62534
+ * Only the CENTRE pixel surface triggers this redirect. While the editor
62535
+ * floats the sidebar keeps whatever view it had: the editor panel renders a
62536
+ * "docked elsewhere" placeholder (see panelFor), and on docking the user
62537
+ * lands back where they were.
61381
62538
  */
61382
62539
  (0, react.useEffect)(() => {
61383
- if (store.editorSurface !== "sidebar" && view === "editor") go("explorer");
62540
+ if (store.editorSurface === "center" && view === "editor") go("explorer");
61384
62541
  }, [
61385
62542
  store.editorSurface,
61386
62543
  view,
@@ -61474,7 +62631,9 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
61474
62631
  }
61475
62632
  })
61476
62633
  });
61477
- case "editor": return editorInSidebar ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EditorPane, { visible: panelLive("editor") }) : null;
62634
+ case "editor":
62635
+ if (editorInSidebar) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EditorPane, { visible: panelLive("editor") });
62636
+ return store.floatOpen ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FloatingEditorPlaceholder, {}) : null;
61478
62637
  default: return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RemoteExplorer, {
61479
62638
  showHidden,
61480
62639
  onToggleHidden: () => setShowHidden((v) => !v),
@@ -61799,7 +62958,7 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
61799
62958
  const original = side.openResource.bind(side);
61800
62959
  const wrapper = (address, options) => {
61801
62960
  try {
61802
- if (!isEditorCentered()) return original(address, options);
62961
+ if (!isEditorTakeoverActive()) return original(address, options);
61803
62962
  const parsed = parseFileAddress(address);
61804
62963
  if (parsed === void 0) return original(address, options);
61805
62964
  return interceptSync(ctx, parsed, address, options) === true ? void 0 : original(address, options);
@@ -61857,6 +63016,7 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
61857
63016
  className: "rvs-column",
61858
63017
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
61859
63018
  className: "rvs-globalbar",
63019
+ title: store.floatAvailable ? store.t("float.dockViewHint") : store.t("float.unavailable"),
61860
63020
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
61861
63021
  className: "rvs-select",
61862
63022
  value: store.workspace?.id ?? "",
@@ -61881,7 +63041,7 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
61881
63041
  store.head.behind > 0 && ` ↓${store.head.behind}`
61882
63042
  ]
61883
63043
  })]
61884
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EditorPane, { visible: true })]
63044
+ }), store.editorSurface === "center" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EditorPane, { visible: true }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FloatingEditorPlaceholder, {})]
61885
63045
  })
61886
63046
  })] });
61887
63047
  }
@@ -63058,6 +64218,28 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
63058
64218
  } catch {}
63059
64219
  };
63060
64220
  }, "dsh-remote-vs: conversation view (editor + diff)");
64221
+ ctx.effect(() => installFloatTabTrigger(viewT("view.editor"), () => {
64222
+ openFloatingEditor();
64223
+ }), "dsh-remote-vs: float on tab double-click");
64224
+ ctx.effect(() => {
64225
+ let dispose;
64226
+ try {
64227
+ const slots = ctx.slots;
64228
+ dispose = slots.inject("shell.overlay", () => slots.register({
64229
+ name: "shell.overlay",
64230
+ id: "remote-vs:floating-editor",
64231
+ order: 40
64232
+ }, () => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FloatingEditorWindow, { ctx })));
64233
+ } catch (error) {
64234
+ console.warn("[dsh-remote-vs] shell.overlay registration failed (floating editor unavailable):", error);
64235
+ return () => {};
64236
+ }
64237
+ return () => {
64238
+ try {
64239
+ dispose?.();
64240
+ } catch {}
64241
+ };
64242
+ }, "dsh-remote-vs: floating editor window");
63061
64243
  }
63062
64244
  //#endregion
63063
64245
  exports.apply = apply;