dsh-remote-vs 0.8.8 → 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.
Files changed (38) hide show
  1. package/README.en.md +15 -2
  2. package/README.md +12 -1
  3. package/lib/client.js +1507 -59
  4. package/lib/types/client/CodeEditor.d.ts +27 -1
  5. package/lib/types/client/FloatingEditorWindow.d.ts +20 -0
  6. package/lib/types/client/FloatingPlaceholder.d.ts +9 -0
  7. package/lib/types/client/RemoteEditorView.d.ts +4 -0
  8. package/lib/types/client/TerminalView.d.ts +3 -0
  9. package/lib/types/client/XTerm.d.ts +6 -0
  10. package/lib/types/client/float-editor.d.ts +123 -0
  11. package/lib/types/client/float-tab-trigger.d.ts +24 -0
  12. package/lib/types/client/live-editor.d.ts +94 -0
  13. package/lib/types/client/remote-file-open-target.d.ts +0 -22
  14. package/lib/types/client/shell.d.ts +29 -0
  15. package/lib/types/client/store.d.ts +160 -6
  16. package/package.json +1 -1
  17. package/src/client/CodeEditor.tsx +95 -8
  18. package/src/client/EditorPane.tsx +45 -24
  19. package/src/client/FloatingEditorWindow.tsx +494 -0
  20. package/src/client/FloatingPlaceholder.tsx +22 -0
  21. package/src/client/RemoteEditorView.tsx +7 -2
  22. package/src/client/RemoteExplorer.tsx +61 -0
  23. package/src/client/RemoteRoot.tsx +16 -7
  24. package/src/client/SearchView.tsx +49 -2
  25. package/src/client/TerminalView.tsx +53 -2
  26. package/src/client/XTerm.tsx +15 -0
  27. package/src/client/float-editor.ts +271 -0
  28. package/src/client/float-tab-trigger.ts +51 -0
  29. package/src/client/i18n.ts +32 -0
  30. package/src/client/index.tsx +48 -1
  31. package/src/client/live-editor.ts +155 -0
  32. package/src/client/remote-file-open-target.ts +1 -5
  33. package/src/client/remote-file-open.ts +2 -2
  34. package/src/client/scm/ScmPanel.tsx +42 -1
  35. package/src/client/shell.ts +35 -0
  36. package/src/client/store.tsx +367 -16
  37. package/src/client/vsc/editor-theme.ts +6 -1
  38. 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": "剪切",
@@ -1413,6 +1426,8 @@ window.__ModuleLoader__.load({
1413
1426
  "editor.tokenCount": "语法着色 token 数(0 = 没有语法包或加载的是旧 bundle)",
1414
1427
  "search.empty": "没有匹配的文件。",
1415
1428
  "search.truncated": "结果过多,仅显示前一批。",
1429
+ "search.revealInExplorer": "打开文件所在位置",
1430
+ "search.openInTerminal": "在终端中打开",
1416
1431
  "scm.title": "源代码管理",
1417
1432
  "scm.refresh": "刷新",
1418
1433
  "scm.changes": "更改",
@@ -1706,6 +1721,19 @@ window.__ModuleLoader__.load({
1706
1721
  "files.deleteConfirm": "Delete {target}? This cannot be undone on the remote host.",
1707
1722
  "files.open": "Open",
1708
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.",
1709
1737
  "files.rename": "Rename",
1710
1738
  "files.copy": "Copy",
1711
1739
  "files.cut": "Cut",
@@ -1745,6 +1773,8 @@ window.__ModuleLoader__.load({
1745
1773
  "editor.tokenCount": "Colored syntax tokens (0 = no grammar, or a stale client bundle)",
1746
1774
  "search.empty": "No matching files.",
1747
1775
  "search.truncated": "Too many hits; showing the first batch.",
1776
+ "search.revealInExplorer": "Open containing folder",
1777
+ "search.openInTerminal": "Open in terminal",
1748
1778
  "scm.title": "Source control",
1749
1779
  "scm.refresh": "Refresh",
1750
1780
  "scm.changes": "Changes",
@@ -35577,7 +35607,7 @@ window.__ModuleLoader__.load({
35577
35607
  "&": {
35578
35608
  backgroundColor: p.bg,
35579
35609
  color: p.fg,
35580
- fontSize: "13px",
35610
+ fontSize: "var(--rvs-editor-font-size, 13px)",
35581
35611
  height: "100%"
35582
35612
  },
35583
35613
  ".cm-content": {
@@ -37615,14 +37645,18 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
37615
37645
  (0, react.useImperativeHandle)(ref, () => ({
37616
37646
  getContent: () => viewRef.current?.state.doc.toString() ?? savedRef.current,
37617
37647
  isDirty: () => dirtyRef.current,
37618
- markSaved: () => {
37648
+ markSaved: (savedText, tabId) => {
37619
37649
  const view = viewRef.current;
37620
- if (view !== void 0) {
37621
- savedRef.current = view.state.doc.toString();
37622
- savedDocRef.current = view.state.doc;
37650
+ if (view === void 0) return;
37651
+ if (propsRef.current.tabId !== tabId) return;
37652
+ savedRef.current = savedText;
37653
+ const saved = EditorState.create({ doc: savedText }).doc;
37654
+ savedDocRef.current = saved;
37655
+ const dirty = view.state.doc !== saved && !view.state.doc.eq(saved);
37656
+ if (dirty !== dirtyRef.current) {
37657
+ dirtyRef.current = dirty;
37658
+ propsRef.current.onDirty(dirty);
37623
37659
  }
37624
- dirtyRef.current = false;
37625
- propsRef.current.onDirty(false);
37626
37660
  },
37627
37661
  getViewState: () => {
37628
37662
  const view = viewRef.current;
@@ -37667,6 +37701,33 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
37667
37701
  if (props.visible === false) return;
37668
37702
  viewRef.current?.requestMeasure();
37669
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]);
37670
37731
  const chatGate = props.chatEnabled === true && props.sessionId !== void 0 && props.remoteRoot !== void 0;
37671
37732
  (0, react.useEffect)(() => {
37672
37733
  if (!chatGate) popupRef.current.hide();
@@ -37852,7 +37913,17 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
37852
37913
  parent: host
37853
37914
  });
37854
37915
  viewRef.current = view;
37855
- savedDocRef.current = view.state.doc;
37916
+ const savedText = propsRef.current.savedText;
37917
+ const baseline = savedText !== void 0 ? EditorState.create({ doc: savedText }).doc : view.state.doc;
37918
+ savedDocRef.current = baseline;
37919
+ if (savedText !== void 0) {
37920
+ savedRef.current = savedText;
37921
+ const dirty = view.state.doc !== baseline && !view.state.doc.eq(baseline);
37922
+ if (dirty !== dirtyRef.current) {
37923
+ dirtyRef.current = dirty;
37924
+ propsRef.current.onDirty(dirty);
37925
+ }
37926
+ }
37856
37927
  setView(view);
37857
37928
  /**
37858
37929
  * Come back to where this file was left.
@@ -37990,7 +38061,7 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
37990
38061
  const primary = host.querySelectorAll("[class*=\"rvs-ck-\"]").length;
37991
38062
  const fallback = host.querySelectorAll("span[class^=\"rvs-t-\"]").length;
37992
38063
  const count = primary > 0 ? primary : fallback;
37993
- console.info(`[dsh-remote-vs] build 1025-0858 · ${source} · colored tokens: ${count} (${propsRef.current.path})`);
38064
+ console.info(`[dsh-remote-vs] build 1027-1757 · ${source} · colored tokens: ${count} (${propsRef.current.path})`);
37994
38065
  propsRef.current.onTokens?.(count);
37995
38066
  };
37996
38067
  const armFallback = () => {
@@ -38076,6 +38147,122 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
38076
38147
  ] });
38077
38148
  });
38078
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
38079
38266
  //#region src/client/use-remote-chat-target.ts
38080
38267
  /**
38081
38268
  * The session↔binding gate behind the remote `@file` buttons and the
@@ -40322,8 +40509,271 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
40322
40509
  }
40323
40510
  return next ?? current;
40324
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
+ }
40718
+ //#endregion
40719
+ //#region src/client/shell.ts
40720
+ /**
40721
+ * Shell quoting for the lines this plugin types into a remote terminal.
40722
+ *
40723
+ * One rule, one place: the client builds POSIX shell text in three spots (the
40724
+ * `cd` typed into a fresh terminal, the bounded suffix search for an unresolved
40725
+ * click target, and the terminal command a menu entry sends), and a second copy
40726
+ * of the quoting rule is how an injection bug reappears after it was fixed once.
40727
+ *
40728
+ * Single quotes are the only form that is safe for arbitrary paths — double
40729
+ * quotes still expand `$`, backticks and `\` — and the closing quote is the one
40730
+ * character they cannot carry, so it is spliced in as `'\''`.
40731
+ */
40732
+ /**
40733
+ * Quote one value for a POSIX shell, as a single word.
40734
+ *
40735
+ * @param value - the raw path or word.
40736
+ * @returns `'value'` with every embedded `'` spliced as `'\''`.
40737
+ */
40738
+ function shellQuote(value) {
40739
+ return `'${value.replace(/'/g, `'\\''`)}'`;
40740
+ }
40741
+ /**
40742
+ * The command typed into a fresh remote terminal to land in one directory.
40743
+ *
40744
+ * A trailing newline is NOT included: the caller writes the line and the
40745
+ * terminal's Enter, which keeps the same string reusable for display.
40746
+ *
40747
+ * @param dir - absolute remote directory.
40748
+ * @returns `cd '<dir>'`.
40749
+ */
40750
+ function cdCommand(dir) {
40751
+ return `cd ${shellQuote(dir)}`;
40752
+ }
40325
40753
  //#endregion
40326
40754
  //#region src/client/remote-file-open-target.ts
40755
+ /**
40756
+ * Where a clicked file reference should open when the owning session lives in
40757
+ * a remote workspace (pure — no React, no fetch, so the gate can drive it).
40758
+ *
40759
+ * DSH renders every file reference as a `dsh-resource://file/…` address and
40760
+ * opens it through its own right-sidebar preview. That preview resolves the
40761
+ * address path against the SESSION WORKSPACE ROOT. A model that quotes a path
40762
+ * relative to its CURRENT directory (e.g. `_xfer/reports/x.md` while working
40763
+ * in `<workspace>/<inner-repo>`) therefore misses by one segment, and a
40764
+ * remote session's workspace root only exists as an EMPTY local alias dir —
40765
+ * the click dies with the host-side ENOENT the user sees.
40766
+ *
40767
+ * This module answers "which binding, which remote paths to try" for the
40768
+ * client interceptor:
40769
+ * - relative paths are tried against the workspace root first and the
40770
+ * session's current working directory second (the model's usual writing
40771
+ * base), which is what makes the one-segment miss heal;
40772
+ * - absolute spellings (local alias, POSIX remote, drive-qualified alias)
40773
+ * are folded back onto the binding;
40774
+ * - anything that does not name a known binding answers `undefined`, so the
40775
+ * native preview keeps handling local sessions and unknown addresses.
40776
+ */
40327
40777
  const FILE_ADDRESS_PREFIX = "dsh-resource://file/";
40328
40778
  function stripQueryAndFragment(address) {
40329
40779
  const end = address.search(/[?#]/);
@@ -40648,10 +41098,6 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
40648
41098
  }
40649
41099
  return bindingForHostPath(table, path);
40650
41100
  }
40651
- /** POSIX single-quote a word for a remote `bash -c` style command line. */
40652
- function shellQuote(value) {
40653
- return `'${value.replace(/'/g, `'\\''`)}'`;
40654
- }
40655
41101
  /**
40656
41102
  * Bounded remote search for a clicked path that no candidate resolved.
40657
41103
  *
@@ -40923,8 +41369,12 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
40923
41369
  activeTabId: void 0,
40924
41370
  revision: 0,
40925
41371
  editorSurface: "sidebar",
41372
+ floatOpen: false,
41373
+ floatAvailable: false,
40926
41374
  view: readStoredView(),
40927
41375
  searchRequest: void 0,
41376
+ revealRequest: void 0,
41377
+ terminalRequest: void 0,
40928
41378
  untrackedDirs: /* @__PURE__ */ new Map()
40929
41379
  };
40930
41380
  /** Stable action identities — `build()` runs on every publish. */
@@ -40995,11 +41445,17 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
40995
41445
  activeTab: state.tabs.find((item) => item.id === state.activeTabId),
40996
41446
  activeTabId: state.activeTabId,
40997
41447
  revision: state.revision,
40998
- editorSurface: state.editorSurface,
41448
+ editorSurface: effectiveEditorSurface(state.editorSurface, state.floatOpen),
41449
+ floatOpen: state.floatOpen,
41450
+ floatAvailable: state.floatAvailable,
40999
41451
  view: state.view,
41000
41452
  searchRequest: state.searchRequest,
41453
+ revealRequest: state.revealRequest,
41454
+ terminalRequest: state.terminalRequest,
41001
41455
  setView,
41002
41456
  requestSearch,
41457
+ revealInExplorer,
41458
+ openTerminalAt,
41003
41459
  selectWorkspace,
41004
41460
  setRepoSubdir,
41005
41461
  discoverRepos,
@@ -41010,6 +41466,10 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
41010
41466
  untrackedDirs: state.untrackedDirs,
41011
41467
  toggleUntrackedDir,
41012
41468
  openFile,
41469
+ openInFloatEditor,
41470
+ openDiffInFloatEditor,
41471
+ openFloatingEditor,
41472
+ dockFloatingEditor,
41013
41473
  openDiff,
41014
41474
  openCommitFile,
41015
41475
  activateTab,
@@ -41136,6 +41596,51 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
41136
41596
  } });
41137
41597
  setView("search");
41138
41598
  }
41599
+ /** Monotonic ticket for "show this folder" requests (0 = none yet). */
41600
+ let revealSeq = 0;
41601
+ /**
41602
+ * Show one remote directory in the explorer.
41603
+ *
41604
+ * Two stages, because the tree is lazily loaded: this raises the request and
41605
+ * switches the panel, and the explorer itself expands the ancestors (loading
41606
+ * each level) and scrolls the row into view — it is the only place that knows
41607
+ * which directories are currently expanded.
41608
+ *
41609
+ * @param dir - absolute remote DIRECTORY (a file's parent for a file hit).
41610
+ */
41611
+ function revealInExplorer(dir) {
41612
+ revealSeq += 1;
41613
+ patch({ revealRequest: {
41614
+ id: revealSeq,
41615
+ path: dir
41616
+ } });
41617
+ setView("explorer");
41618
+ }
41619
+ /** Monotonic ticket for "open a terminal here" requests. */
41620
+ let terminalSeq = 0;
41621
+ /**
41622
+ * Open a fresh terminal tab whose shell lands in `dir` — on the CURRENT
41623
+ * workspace's host.
41624
+ *
41625
+ * The terminal panel consumes the request (it owns the tabs) and the shell gets
41626
+ * a quoted `cd` once its PTY reports ready — the host's terminal route has no
41627
+ * "start in this directory" parameter, so a typed command is the honest way.
41628
+ *
41629
+ * The alias travels IN the request. The panel otherwise opens new tabs on the
41630
+ * host it last used, which put the shell on a different machine than the path
41631
+ * belongs to and made the `cd` fail with "No such file or directory".
41632
+ *
41633
+ * @param dir - absolute remote directory.
41634
+ */
41635
+ function openTerminalAt(dir) {
41636
+ terminalSeq += 1;
41637
+ patch({ terminalRequest: {
41638
+ id: terminalSeq,
41639
+ alias: currentWorkspace()?.alias ?? "",
41640
+ dir
41641
+ } });
41642
+ setView("terminal");
41643
+ }
41139
41644
  /**
41140
41645
  * Select the binding every view works against — the USER's choice.
41141
41646
  *
@@ -41591,7 +42096,7 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
41591
42096
  * is told where the file opened instead of being left guessing.
41592
42097
  */
41593
42098
  function revealEditor() {
41594
- if (state.editorSurface !== "center") return;
42099
+ if (effectiveEditorSurface(state.editorSurface, state.floatOpen) !== "center") return;
41595
42100
  const label = t("view.editor");
41596
42101
  if (revealRetry !== void 0) {
41597
42102
  clearTimeout(revealRetry);
@@ -41610,6 +42115,49 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
41610
42115
  } : tab) });
41611
42116
  }
41612
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
+ /**
41613
42161
  * Deadline for one git round trip.
41614
42162
  *
41615
42163
  * Was a fixed 45 s shared with file reads, which made a cold `git status` on a
@@ -41644,6 +42192,7 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
41644
42192
  }
41645
42193
  /** Open (or focus) a remote file in the editor tab strip. */
41646
42194
  function openFile(fullPath, options = {}) {
42195
+ captureLiveBuffer();
41647
42196
  const workspace = currentWorkspace();
41648
42197
  if (workspace === void 0 || fullPath === "") return;
41649
42198
  const id = `file:${workspace.alias}:${fullPath}`;
@@ -41692,7 +42241,8 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
41692
42241
  loading: false,
41693
42242
  binary: read.binary,
41694
42243
  truncated: read.truncated,
41695
- text: read.binary ? "" : read.text ?? ""
42244
+ text: read.binary ? "" : read.text ?? "",
42245
+ ...read.binary ? {} : { baseline: read.text ?? "" }
41696
42246
  });
41697
42247
  } catch (error) {
41698
42248
  patchTab(id, {
@@ -41730,12 +42280,21 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
41730
42280
  openFile(remotePath, options);
41731
42281
  return true;
41732
42282
  }
41733
- /** Whether the editor tab is mounted in the centre conversation view. */
41734
- function isEditorCentered() {
41735
- return state.editorSurface === "center";
42283
+ /**
42284
+ * Whether the plugin's own rich editor is the place a remote file click should
42285
+ * land (the click interceptor's gate, see `remote-file-open.ts`).
42286
+ *
42287
+ * The centre conversation view is the normal case; while the editor floats it
42288
+ * is the floating window instead — the file still belongs in OUR editor, not in
42289
+ * the host's native preview. On the sidebar-fallback layout (`sidebar`) the
42290
+ * interceptor stays off, exactly as before this feature.
42291
+ */
42292
+ function isEditorTakeoverActive() {
42293
+ return effectiveEditorSurface(state.editorSurface, state.floatOpen) !== "sidebar";
41736
42294
  }
41737
42295
  /** Open (or focus) a working-tree diff of one repository path. */
41738
42296
  function openDiff(rel, staged, untracked = false) {
42297
+ captureLiveBuffer();
41739
42298
  const workspace = currentWorkspace();
41740
42299
  if (workspace === void 0 || state.repoRoot === void 0 || rel === "") {
41741
42300
  toast(t("git.notARepo"), true);
@@ -41784,6 +42343,7 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
41784
42343
  }
41785
42344
  /** Open (or focus) one file of one commit (`ref^` against `ref`). */
41786
42345
  function openCommitFile(hash, rel) {
42346
+ captureLiveBuffer();
41787
42347
  const workspace = currentWorkspace();
41788
42348
  const root = state.repoRoot;
41789
42349
  if (workspace === void 0 || root === void 0 || rel === "") return;
@@ -41829,6 +42389,7 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
41829
42389
  })();
41830
42390
  }
41831
42391
  function activateTab(id) {
42392
+ captureLiveBuffer();
41832
42393
  patch({ activeTabId: id });
41833
42394
  }
41834
42395
  function closeTab(id) {
@@ -41842,14 +42403,14 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
41842
42403
  function updateTab(id, next) {
41843
42404
  patchTab(id, next);
41844
42405
  }
41845
- /** Write one tab back to the remote host. */
42406
+ /** Write one tab back to the remote host. @returns true when the write landed. */
41846
42407
  async function saveTab(id, content) {
41847
42408
  const workspace = currentWorkspace();
41848
42409
  const tab = state.tabs.find((item) => item.id === id);
41849
- 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;
41850
42411
  if (typeof content !== "string") {
41851
42412
  toast(t("editor.needSource"), true);
41852
- return;
42413
+ return false;
41853
42414
  }
41854
42415
  const text = content;
41855
42416
  patchTab(id, {
@@ -41858,15 +42419,42 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
41858
42419
  });
41859
42420
  try {
41860
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;
41861
42445
  patchTab(id, {
41862
42446
  saving: false,
41863
- dirty: false
42447
+ baseline: text,
42448
+ ...liveEditor || newer ? {} : { dirty: false }
41864
42449
  });
42450
+ markLiveEditorSaved(id, text);
41865
42451
  toast(`${t("editor.saved")} · ${tab.rel}`, false);
41866
42452
  await afterWrite();
42453
+ return true;
41867
42454
  } catch (error) {
41868
42455
  patchTab(id, { saving: false });
41869
42456
  toast(error instanceof Error ? error.message : String(error), true);
42457
+ return false;
41870
42458
  }
41871
42459
  }
41872
42460
  /**
@@ -41900,7 +42488,8 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
41900
42488
  text: read.binary ? "" : read.text ?? "",
41901
42489
  binary: read.binary,
41902
42490
  truncated: read.truncated,
41903
- dirty: false
42491
+ dirty: false,
42492
+ ...read.binary ? {} : { baseline: read.text ?? "" }
41904
42493
  });
41905
42494
  patchTab(id, { reloadNonce: (tab.reloadNonce ?? 0) + 1 });
41906
42495
  } catch (error) {
@@ -41929,6 +42518,88 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
41929
42518
  patch({ editorSurface: surface });
41930
42519
  }
41931
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
+ /**
41932
42603
  * 换一份「本会话改了什么」的标记。
41933
42604
  *
41934
42605
  * `activeHunk` 归零是刻意的:数据换了一份,上一份里"跳到第 3 处"的下标没有任何
@@ -43045,23 +43716,26 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
43045
43716
  * buffer rather than the previous file's undo stack, which is what we want,
43046
43717
  * but it also reset the scroll and the caret.
43047
43718
  *
43048
- * The editor REPORTS its position (see `CodeEditor`'s `onViewState`) instead
43049
- * of this component reading it back through `editorRef`, because the ref is
43050
- * already `null` by the time any cleanup here could run: React clears a
43051
- * child's ref before running the parent's cleanup. Both earlier attempts
43052
- * failed on exactly that — a `useEffect` keyed on the active tab captured the
43053
- * INCOMING tab's editor (always scrollTop 0), and a `useLayoutEffect` cleanup
43054
- * found `editorRef.current === null`. `.spike/tabswitch-check.mjs` reproduces
43055
- * 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.
43056
43723
  *
43057
43724
  * Keyed by tab id alone, which is safe because only ONE `EditorPane` is ever
43058
43725
  * mounted: the centre surface when the host has a `conversation.view` slot,
43059
- * otherwise the sidebar fallback. With two live editors for the same tab the
43060
- * hidden one would report its untouched scrollTop 0 and overwrite the visible
43061
- * one's, snapping every switch back to the top — so `RemoteRoot` gates both
43062
- * mounts on `editorSurface` and must keep doing so.
43726
+ * the floating window while the editor floats, otherwise the sidebar
43727
+ * fallback. With two live editors for the same tab the hidden one would
43728
+ * report its untouched scrollTop 0 and overwrite the visible one's, snapping
43729
+ * every switch back to the top — so `RemoteRoot`, `RemoteEditorView` and the
43730
+ * floating window all gate their mounts on `editorSurface` and must keep
43731
+ * doing so.
43732
+ *
43733
+ * The editor REPORTS its position (see `CodeEditor`'s `onViewState`) instead
43734
+ * of this component reading it back through `editorRef`, because the ref is
43735
+ * already `null` by the time any cleanup here could run: React clears a
43736
+ * child's ref before running the parent's cleanup. `.spike/tabswitch-check.mjs`
43737
+ * reproduces the failed variants and pins the working wiring.
43063
43738
  */
43064
- const viewStates = (0, react.useRef)(/* @__PURE__ */ new Map());
43065
43739
  /**
43066
43740
  * Store a reading under the tab that produced it.
43067
43741
  *
@@ -43077,19 +43751,41 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
43077
43751
  * accept a report for any tab.
43078
43752
  */
43079
43753
  const rememberViewState = (0, react.useCallback)((tabId, state) => {
43080
- viewStates.current.set(tabId, state);
43754
+ rememberEditorViewState(tabId, state);
43081
43755
  }, []);
43082
43756
  /**
43083
- * Closed tabs may not keep their scroll + caret. This map is keyed by tab id
43084
- * and was only ever written, and the pane is mounted for the life of the
43085
- * page: a session that opened a few hundred files held a state object for
43086
- * every one of them, long after the tab was gone.
43757
+ * Closed tabs may not keep their scroll + caret. The map is keyed by tab id
43758
+ * and is only ever written: a session that opened a few hundred files would
43759
+ * otherwise hold a state object for every one of them, long after the tab is
43760
+ * gone.
43087
43761
  */
43088
43762
  (0, react.useEffect)(() => {
43089
- const live = new Set(tabs.map((tab) => tab.id));
43090
- for (const id of [...viewStates.current.keys()]) if (!live.has(id)) viewStates.current.delete(id);
43763
+ pruneEditorViewStates(new Set(tabs.map((tab) => tab.id)));
43091
43764
  }, [tabs]);
43092
- activeTab !== void 0 && activeTab.kind === "file" && !activeTab.image && !activeTab.binary && !activeTab.loading && activeTab.error;
43765
+ /**
43766
+ * True only while the CodeMirror instance for the active tab is mounted.
43767
+ *
43768
+ * The editor used to be conditionally unmounted in preview mode, which made
43769
+ * `getContent()` return undefined; the save path then fell back to the
43770
+ * tab's loaded text and wrote the PRE-EDIT bytes back to the remote file
43771
+ * while reporting success. Refusing to save without a live buffer is the
43772
+ * load-bearing half of that fix — the editor staying mounted (below) is the
43773
+ * other half, so switching preview no longer drops unsaved edits either.
43774
+ */
43775
+ const editorMounted = activeTab !== void 0 && activeTab.kind === "file" && !activeTab.image && !activeTab.binary && !activeTab.loading && activeTab.error === void 0;
43776
+ /**
43777
+ * Advertise the live editor so the store can snapshot it before a surface
43778
+ * flip (see `captureLiveEditor`): the floating window is a different mount of
43779
+ * the SAME pane, so switching to it unmounts this component.
43780
+ *
43781
+ * The getter is `() => editorRef.current`, not the handle itself: the handle
43782
+ * only exists after `CodeEditor` mounts, and it is cleared before the
43783
+ * cleanup that would otherwise run here.
43784
+ */
43785
+ (0, react.useEffect)(() => {
43786
+ if (!editorMounted || activeTab === void 0) return;
43787
+ return registerLiveEditor(activeTab.id, () => editorRef.current);
43788
+ }, [editorMounted, activeTab?.id]);
43093
43789
  const saveActive = async () => {
43094
43790
  const tab = activeTab;
43095
43791
  if (tab === void 0) return;
@@ -43206,8 +43902,8 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
43206
43902
  }),
43207
43903
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
43208
43904
  className: "rvs-build",
43209
- title: `dsh-remote-vs build 1025-0858`,
43210
- children: "1025-0858"
43905
+ title: `dsh-remote-vs build 1027-1757`,
43906
+ children: "1027-1757"
43211
43907
  }),
43212
43908
  tokenCount !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
43213
43909
  className: "rvs-build",
@@ -43331,6 +44027,7 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
43331
44027
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CodeEditor, {
43332
44028
  ref: editorRef,
43333
44029
  initial: activeTab.text,
44030
+ savedText: activeTab.baseline,
43334
44031
  path: activeTab.fullPath,
43335
44032
  tabId: activeTab.id,
43336
44033
  readOnly: activeTab.truncated,
@@ -43345,7 +44042,7 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
43345
44042
  ...activeTab.changeSet === void 0 ? {} : { changeSet: activeTab.changeSet },
43346
44043
  ...activeTab.revealHunkLine === void 0 ? {} : { revealHunkLine: activeTab.revealHunkLine },
43347
44044
  ...activeTab.revealHunkNonce === void 0 ? {} : { revealHunkNonce: activeTab.revealHunkNonce },
43348
- ...viewStates.current.get(activeTab.id) === void 0 ? {} : { viewState: viewStates.current.get(activeTab.id) },
44045
+ ...editorViewStateOf(activeTab.id) === void 0 ? {} : { viewState: editorViewStateOf(activeTab.id) },
43349
44046
  onTokens: setTokenCount,
43350
44047
  onViewState: rememberViewState,
43351
44048
  onStepHunk: (dir) => {
@@ -43370,6 +44067,518 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
43370
44067
  });
43371
44068
  }
43372
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
43373
44582
  //#region src/fs-ops.ts
43374
44583
  /**
43375
44584
  * Remote file-manager verbs the SFTP route does not offer: recursive copy and
@@ -43822,6 +45031,51 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
43822
45031
  });
43823
45032
  }
43824
45033
  }, [alias]);
45034
+ /**
45035
+ * "Show this folder in the tree" — raised by a search result's menu.
45036
+ *
45037
+ * Two stages, because the tree is lazy: expand every level from the root down
45038
+ * to the target (the listing of each level is fetched here) and then scroll
45039
+ * its row into view. The scroll is retried rather than delayed once: over SFTP
45040
+ * a deep level can take a moment, and the request carries a monotonic id so
45041
+ * asking for the SAME folder again still re-scrolls.
45042
+ */
45043
+ (0, react.useEffect)(() => {
45044
+ const request = store.revealRequest;
45045
+ if (request === void 0) return;
45046
+ const base = root.replace(/\/+$/, "");
45047
+ const target = request.path.replace(/\/+$/, "");
45048
+ if (target !== base && !target.startsWith(`${base}/`)) return;
45049
+ const chain = [];
45050
+ let walk = base;
45051
+ for (const part of target.slice(base.length).split("/")) {
45052
+ if (part === "") continue;
45053
+ walk = `${walk}/${part}`;
45054
+ chain.push(walk);
45055
+ }
45056
+ for (const dir of chain) loadDir(dir);
45057
+ if (chain.length > 0) setExpanded((previous) => {
45058
+ const next = new Set(previous);
45059
+ for (const dir of chain) next.add(dir);
45060
+ return next;
45061
+ });
45062
+ let attempts = 0;
45063
+ const timer = window.setInterval(() => {
45064
+ attempts += 1;
45065
+ const row = Array.from(document.querySelectorAll("[data-rvs-path]")).find((candidate) => candidate.dataset.rvsPath === target);
45066
+ if (row !== void 0) {
45067
+ window.clearInterval(timer);
45068
+ row.scrollIntoView({ block: "center" });
45069
+ return;
45070
+ }
45071
+ if (attempts > 20) window.clearInterval(timer);
45072
+ }, 150);
45073
+ return () => window.clearInterval(timer);
45074
+ }, [
45075
+ store.revealRequest,
45076
+ root,
45077
+ loadDir
45078
+ ]);
43825
45079
  const revision = store.revision;
43826
45080
  /**
43827
45081
  * The binding this tree was listed from and the revision it was listed at.
@@ -44505,6 +45759,13 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
44505
45759
  onSelect: () => openEntry(entry),
44506
45760
  separated: true
44507
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
+ });
44508
45769
  if (!entry.isDir && isPreviewOnlyFile(entry.path)) entries.push({
44509
45770
  label: t("files.openAsText"),
44510
45771
  onSelect: () => store.openFile(entry.path)
@@ -45256,7 +46517,7 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
45256
46517
  }
45257
46518
  function SearchView() {
45258
46519
  const store = useExplorer();
45259
- const { t, alias, root } = store;
46520
+ const { t, alias, root, repoRoot } = store;
45260
46521
  const [mode, setModeState] = (0, react.useState)(readMode);
45261
46522
  const [query, setQuery] = (0, react.useState)("");
45262
46523
  /** Absolute remote directory the search runs in ('' = workspace root). */
@@ -45271,6 +46532,7 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
45271
46532
  const [ran, setRan] = (0, react.useState)(false);
45272
46533
  const [error, setError] = (0, react.useState)(void 0);
45273
46534
  const seq = (0, react.useRef)(0);
46535
+ const menu = useContextMenu();
45274
46536
  const effectiveScope = scope === "" ? root : scope;
45275
46537
  const scopeLabel = (0, react.useMemo)(() => {
45276
46538
  if (effectiveScope === "" || root === "") return "";
@@ -45383,6 +46645,65 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
45383
46645
  const openHit = (path, line) => {
45384
46646
  store.openFile(path, line === void 0 ? void 0 : { line });
45385
46647
  };
46648
+ /** Absolute path of one NAME hit, plus the DIRECTORY a menu action should use. */
46649
+ const nameHitPaths = (hit) => {
46650
+ const abs = `${effectiveScope.replace(/\/+$/, "")}/${hit.path}`;
46651
+ return {
46652
+ abs,
46653
+ dir: hit.isDir ? abs : abs.slice(0, abs.lastIndexOf("/")) || "/"
46654
+ };
46655
+ };
46656
+ /**
46657
+ * Right-click on a name result: what one does with a found path.
46658
+ *
46659
+ * "Relative" is relative to the REPOSITORY when the workspace sits inside one
46660
+ * (the same relative form the tree's own menu copies), else absolute — which
46661
+ * is what makes the copied value paste straight into a shell that already ran
46662
+ * `cd` into the repo. Directories only get the copy/reveal/terminal entries:
46663
+ * there is no file to open in the editor.
46664
+ */
46665
+ const openHitMenu = (event, hit) => {
46666
+ event.preventDefault();
46667
+ event.stopPropagation();
46668
+ const { abs, dir } = nameHitPaths(hit);
46669
+ const rel = repoRelative(repoRoot, abs) || abs;
46670
+ menu.open(event.clientX, event.clientY, [
46671
+ {
46672
+ label: t("files.copyAbs"),
46673
+ onSelect: () => {
46674
+ copyText$1(abs);
46675
+ toast(t("files.copiedClipboard"));
46676
+ }
46677
+ },
46678
+ {
46679
+ label: t("files.copyRel"),
46680
+ onSelect: () => {
46681
+ copyText$1(rel);
46682
+ toast(t("files.copiedClipboard"));
46683
+ }
46684
+ },
46685
+ {
46686
+ label: t("files.openInFloat"),
46687
+ onSelect: () => {
46688
+ store.openInFloatEditor(abs);
46689
+ },
46690
+ disabled: !store.floatAvailable || hit.isDir,
46691
+ separated: true
46692
+ },
46693
+ {
46694
+ label: t("search.revealInExplorer"),
46695
+ onSelect: () => {
46696
+ store.revealInExplorer(dir);
46697
+ }
46698
+ },
46699
+ {
46700
+ label: t("search.openInTerminal"),
46701
+ onSelect: () => {
46702
+ store.openTerminalAt(dir);
46703
+ }
46704
+ }
46705
+ ]);
46706
+ };
45386
46707
  const groups = (0, react.useMemo)(() => groupHits(contentHits), [contentHits]);
45387
46708
  /**
45388
46709
  * The content query, compiled ONCE per (text, flags) instead of once per
@@ -45542,6 +46863,9 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
45542
46863
  className: "rvs-hit",
45543
46864
  title: hit.path,
45544
46865
  onClick: () => openHit(`${(effectiveScope === "" ? root : effectiveScope).replace(/\/+$/, "")}/${hit.path}`),
46866
+ onContextMenu: (event) => {
46867
+ openHitMenu(event, hit);
46868
+ },
45545
46869
  children: [
45546
46870
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
45547
46871
  className: "rvs-node-icon",
@@ -45614,6 +46938,12 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
45614
46938
  })]
45615
46939
  }, hit.line))] }, group.path))] })
45616
46940
  ]
46941
+ }),
46942
+ menu.menu !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ContextMenu, {
46943
+ x: menu.menu.x,
46944
+ y: menu.menu.y,
46945
+ entries: menu.menu.entries,
46946
+ onClose: menu.close
45617
46947
  })
45618
46948
  ]
45619
46949
  });
@@ -46333,6 +47663,8 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
46333
47663
  url: ""
46334
47664
  });
46335
47665
  const [picker, setPicker] = (0, react.useState)(false);
47666
+ /** Right-click menu of one change row (the floating-editor entry). */
47667
+ const menu = useContextMenu();
46336
47668
  const fold = (id) => setFolded((prev) => ({
46337
47669
  ...prev,
46338
47670
  [id]: prev[id] !== true
@@ -46473,6 +47805,33 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
46473
47805
  }
46474
47806
  store.openDiff(entry.path, entry.staged, entry.status === "?");
46475
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
+ };
46476
47835
  const doCommit = async () => {
46477
47836
  if (repoRoot === void 0 || message.trim() === "") return;
46478
47837
  await guard("commit", async () => {
@@ -46589,6 +47948,7 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
46589
47948
  style: depth === 0 ? void 0 : { paddingLeft: `${22 + depth * 12}px` },
46590
47949
  title: expandable ? `${entry.path}\n${t("scm.dirToggle")}` : `${entry.path}\n${t("scm.openHint")}`,
46591
47950
  onClick: activate,
47951
+ onContextMenu: (event) => openRowMenu(event, entry),
46592
47952
  onKeyDown: (e) => {
46593
47953
  if (e.key === "Enter" || e.key === " ") activate();
46594
47954
  },
@@ -47240,6 +48600,12 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
47240
48600
  })
47241
48601
  }))
47242
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
47243
48609
  })
47244
48610
  ]
47245
48611
  });
@@ -60401,6 +61767,8 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
60401
61767
  fit.fit();
60402
61768
  const ws = new WebSocket(propsRef.current.wsUrl);
60403
61769
  let opened = false;
61770
+ /** The seeded line is typed at most once per mount (a reconnect re-mounts). */
61771
+ let seeded = false;
60404
61772
  let exitReported = false;
60405
61773
  const reportExit = (code, message) => {
60406
61774
  if (exitReported) return;
@@ -60469,8 +61837,17 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
60469
61837
  } catch {
60470
61838
  return;
60471
61839
  }
60472
- if (frame.t === "ready") propsRef.current.onReady(frame.id);
60473
- else if (frame.t === "out") term.write(frame.d);
61840
+ if (frame.t === "ready") {
61841
+ propsRef.current.onReady(frame.id);
61842
+ const seed = propsRef.current.initialInput;
61843
+ if (!seeded && seed !== void 0 && seed !== "") {
61844
+ seeded = true;
61845
+ send(ws, {
61846
+ t: "in",
61847
+ d: `${seed}\n`
61848
+ });
61849
+ }
61850
+ } else if (frame.t === "out") term.write(frame.d);
60474
61851
  else if (frame.t === "exit") {
60475
61852
  reportExit(frame.code);
60476
61853
  try {
@@ -60617,6 +61994,14 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
60617
61994
  function newId() {
60618
61995
  return `t${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`;
60619
61996
  }
61997
+ /**
61998
+ * The last "open a terminal here" request this page already acted on.
61999
+ *
62000
+ * Module scope, not a component ref: the panel is normally kept mounted, but a
62001
+ * remount would reset a ref and re-open a tab for the SAME old request. A new
62002
+ * request always carries a larger id, so one counter serves both goals.
62003
+ */
62004
+ let openedTerminalRequest = 0;
60620
62005
  function TerminalView(props) {
60621
62006
  const t = props.t;
60622
62007
  const [alias, setAlias] = (0, react.useState)(readAlias);
@@ -60647,17 +62032,48 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
60647
62032
  ...next
60648
62033
  } : tab));
60649
62034
  }, []);
60650
- const openTab = (0, react.useCallback)((host) => {
62035
+ const openTab = (0, react.useCallback)((host, initialCommand) => {
60651
62036
  if (host === "") return;
60652
62037
  const tab = {
60653
62038
  id: newId(),
60654
62039
  alias: host,
60655
62040
  nonce: 0,
60656
- connected: true
62041
+ connected: true,
62042
+ ...initialCommand === void 0 ? {} : { initialCommand }
60657
62043
  };
60658
62044
  setTabs((prev) => [...prev, tab]);
60659
62045
  setActiveId(tab.id);
60660
62046
  }, []);
62047
+ /**
62048
+ * "Open a terminal here" (a search result's menu): open one tab whose shell
62049
+ * lands in that folder, on the host the folder BELONGS to.
62050
+ *
62051
+ * The alias must come from the request, not from `effectiveAlias`: the panel
62052
+ * remembers whatever host the user last ran a shell on, so a menu action on a
62053
+ * workspace bound elsewhere used to open the shell on the wrong machine and
62054
+ * the `cd` failed with "No such file or directory" for a path that is fine on
62055
+ * its own host.
62056
+ *
62057
+ * The folder is reached by TYPING `cd` once the PTY reports ready (see
62058
+ * `XTerm`'s `initialInput`) — the host's terminal route has no "start in this
62059
+ * directory" parameter, and inventing one would mean a host-side change for a
62060
+ * display feature. The request id makes a repeat for the same folder still
62061
+ * open a second tab.
62062
+ */
62063
+ (0, react.useEffect)(() => {
62064
+ const request = props.request;
62065
+ if (request === void 0 || request.id === openedTerminalRequest) return;
62066
+ openedTerminalRequest = request.id;
62067
+ const target = request.alias !== "" && props.hosts.some((host) => host.alias === request.alias) ? request.alias : effectiveAlias;
62068
+ if (target !== alias) setAlias(target);
62069
+ openTab(target, cdCommand(request.dir));
62070
+ }, [
62071
+ props.request,
62072
+ props.hosts,
62073
+ effectiveAlias,
62074
+ alias,
62075
+ openTab
62076
+ ]);
60661
62077
  const closeTab = (0, react.useCallback)(async (tab) => {
60662
62078
  if (!await confirmDialog({
60663
62079
  title: t("term.closeTitle"),
@@ -60796,6 +62212,7 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
60796
62212
  rows: 24
60797
62213
  }),
60798
62214
  visible: props.visible && tab.id === active?.id,
62215
+ ...tab.initialCommand === void 0 ? {} : { initialInput: tab.initialCommand },
60799
62216
  onReady: (id) => patchTab(tab.id, {
60800
62217
  session: id,
60801
62218
  connected: true,
@@ -61113,9 +62530,14 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
61113
62530
  * that owns it was gone AND the centre tab mounted a second EditorPane on
61114
62531
  * the same store tab — two live buffers with independent undo state, the one
61115
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.
61116
62538
  */
61117
62539
  (0, react.useEffect)(() => {
61118
- if (store.editorSurface !== "sidebar" && view === "editor") go("explorer");
62540
+ if (store.editorSurface === "center" && view === "editor") go("explorer");
61119
62541
  }, [
61120
62542
  store.editorSurface,
61121
62543
  view,
@@ -61191,7 +62613,8 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
61191
62613
  case "terminal": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TerminalView, {
61192
62614
  t,
61193
62615
  hosts: store.hosts,
61194
- visible: panelLive("terminal")
62616
+ visible: panelLive("terminal"),
62617
+ request: store.terminalRequest
61195
62618
  });
61196
62619
  case "workspaces": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
61197
62620
  className: "rvs-viewpad",
@@ -61208,7 +62631,9 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
61208
62631
  }
61209
62632
  })
61210
62633
  });
61211
- case "editor": return editorInSidebar ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EditorPane, { visible: panelLive("editor") }) : null;
62634
+ case "editor":
62635
+ if (editorInSidebar) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EditorPane, { visible: panelLive("editor") });
62636
+ return store.floatOpen ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FloatingEditorPlaceholder, {}) : null;
61212
62637
  default: return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RemoteExplorer, {
61213
62638
  showHidden,
61214
62639
  onToggleHidden: () => setShowHidden((v) => !v),
@@ -61533,7 +62958,7 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
61533
62958
  const original = side.openResource.bind(side);
61534
62959
  const wrapper = (address, options) => {
61535
62960
  try {
61536
- if (!isEditorCentered()) return original(address, options);
62961
+ if (!isEditorTakeoverActive()) return original(address, options);
61537
62962
  const parsed = parseFileAddress(address);
61538
62963
  if (parsed === void 0) return original(address, options);
61539
62964
  return interceptSync(ctx, parsed, address, options) === true ? void 0 : original(address, options);
@@ -61591,6 +63016,7 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
61591
63016
  className: "rvs-column",
61592
63017
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
61593
63018
  className: "rvs-globalbar",
63019
+ title: store.floatAvailable ? store.t("float.dockViewHint") : store.t("float.unavailable"),
61594
63020
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
61595
63021
  className: "rvs-select",
61596
63022
  value: store.workspace?.id ?? "",
@@ -61615,7 +63041,7 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
61615
63041
  store.head.behind > 0 && ` ↓${store.head.behind}`
61616
63042
  ]
61617
63043
  })]
61618
- }), /* @__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, {})]
61619
63045
  })
61620
63046
  })] });
61621
63047
  }
@@ -62792,6 +64218,28 @@ Integer Float Double Long Int32 Int64 Uint8 uint32_t int32_t uint64_t int64_t si
62792
64218
  } catch {}
62793
64219
  };
62794
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");
62795
64243
  }
62796
64244
  //#endregion
62797
64245
  exports.apply = apply;