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/README.en.md +15 -2
- package/README.md +12 -1
- package/lib/client.js +1235 -53
- package/lib/types/client/CodeEditor.d.ts +27 -1
- package/lib/types/client/FloatingEditorWindow.d.ts +20 -0
- package/lib/types/client/FloatingPlaceholder.d.ts +9 -0
- package/lib/types/client/RemoteEditorView.d.ts +4 -0
- package/lib/types/client/float-editor.d.ts +123 -0
- package/lib/types/client/float-tab-trigger.d.ts +24 -0
- package/lib/types/client/live-editor.d.ts +94 -0
- package/lib/types/client/store.d.ts +95 -6
- package/package.json +1 -1
- package/src/client/CodeEditor.tsx +95 -8
- package/src/client/EditorPane.tsx +45 -24
- package/src/client/FloatingEditorWindow.tsx +494 -0
- package/src/client/FloatingPlaceholder.tsx +22 -0
- package/src/client/RemoteEditorView.tsx +7 -2
- package/src/client/RemoteExplorer.tsx +16 -0
- package/src/client/RemoteRoot.tsx +15 -6
- package/src/client/SearchView.tsx +10 -3
- package/src/client/float-editor.ts +271 -0
- package/src/client/float-tab-trigger.ts +51 -0
- package/src/client/i18n.ts +28 -0
- package/src/client/index.tsx +48 -1
- package/src/client/live-editor.ts +155 -0
- package/src/client/remote-file-open.ts +2 -2
- package/src/client/scm/ScmPanel.tsx +42 -1
- package/src/client/store.tsx +274 -16
- package/src/client/vsc/editor-theme.ts +6 -1
- package/src/client/vsc/vsc.css +235 -0
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
|
|
37625
|
-
|
|
37626
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
/**
|
|
41842
|
-
|
|
41843
|
-
|
|
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
|
-
|
|
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
|
|
43157
|
-
*
|
|
43158
|
-
*
|
|
43159
|
-
*
|
|
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
|
-
*
|
|
43168
|
-
*
|
|
43169
|
-
*
|
|
43170
|
-
*
|
|
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
|
-
|
|
43754
|
+
rememberEditorViewState(tabId, state);
|
|
43189
43755
|
}, []);
|
|
43190
43756
|
/**
|
|
43191
|
-
* Closed tabs may not keep their scroll + caret.
|
|
43192
|
-
* and
|
|
43193
|
-
*
|
|
43194
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
43318
|
-
children: "
|
|
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
|
-
...
|
|
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:
|
|
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("
|
|
46686
|
+
label: t("files.openInFloat"),
|
|
45578
46687
|
onSelect: () => {
|
|
45579
|
-
store.
|
|
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
|
|
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":
|
|
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 (!
|
|
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;
|