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.
- package/README.en.md +15 -2
- package/README.md +12 -1
- package/lib/client.js +1507 -59
- package/lib/types/client/CodeEditor.d.ts +27 -1
- package/lib/types/client/FloatingEditorWindow.d.ts +20 -0
- package/lib/types/client/FloatingPlaceholder.d.ts +9 -0
- package/lib/types/client/RemoteEditorView.d.ts +4 -0
- package/lib/types/client/TerminalView.d.ts +3 -0
- package/lib/types/client/XTerm.d.ts +6 -0
- package/lib/types/client/float-editor.d.ts +123 -0
- package/lib/types/client/float-tab-trigger.d.ts +24 -0
- package/lib/types/client/live-editor.d.ts +94 -0
- package/lib/types/client/remote-file-open-target.d.ts +0 -22
- package/lib/types/client/shell.d.ts +29 -0
- package/lib/types/client/store.d.ts +160 -6
- package/package.json +1 -1
- package/src/client/CodeEditor.tsx +95 -8
- package/src/client/EditorPane.tsx +45 -24
- package/src/client/FloatingEditorWindow.tsx +494 -0
- package/src/client/FloatingPlaceholder.tsx +22 -0
- package/src/client/RemoteEditorView.tsx +7 -2
- package/src/client/RemoteExplorer.tsx +61 -0
- package/src/client/RemoteRoot.tsx +16 -7
- package/src/client/SearchView.tsx +49 -2
- package/src/client/TerminalView.tsx +53 -2
- package/src/client/XTerm.tsx +15 -0
- package/src/client/float-editor.ts +271 -0
- package/src/client/float-tab-trigger.ts +51 -0
- package/src/client/i18n.ts +32 -0
- package/src/client/index.tsx +48 -1
- package/src/client/live-editor.ts +155 -0
- package/src/client/remote-file-open-target.ts +1 -5
- package/src/client/remote-file-open.ts +2 -2
- package/src/client/scm/ScmPanel.tsx +42 -1
- package/src/client/shell.ts +35 -0
- package/src/client/store.tsx +367 -16
- package/src/client/vsc/editor-theme.ts +6 -1
- package/src/client/vsc/vsc.css +235 -0
package/lib/client.js
CHANGED
|
@@ -11,7 +11,7 @@ window.__ModuleLoader__.load({
|
|
|
11
11
|
let react_jsx_runtime = require("react/jsx-runtime");
|
|
12
12
|
let react_dom = require("react-dom");
|
|
13
13
|
//#region \0dsh-css:E:\work_base\dsh_remote_vs\src\client\vsc\vsc.css.mjs
|
|
14
|
-
const css$3 = "/**\n * VS Code-style design system for the remote explorer. Every surface below\n * is scoped under `.rvs-root` so the sheet can never leak into the host GUI,\n * and every host token carries a literal fallback: the plugin also renders in\n * the native right-Sidebar fallback and on hosts whose alias tokens differ.\n *\n * Palette and metrics are aligned with the reference implementation\n * (dsh-solution-explorer): 13px/20px panel type, 5px/10px tree rows with a\n * 10px radius, a 30px activity button with the badge tucked into the icon's\n * bottom-right corner, GitHub-Dark token colors, and the\n * `--e2b714 / #4ec9b0 / #f14c4c` modified/added/deleted triple.\n */\n\n.rvs-root {\n --rvs-fg: var(--dsw-alias-label-primary, #e6e6e6);\n --rvs-fg2: var(--dsw-alias-label-secondary, #b9b9b9);\n --rvs-fg3: var(--dsw-alias-label-tertiary, #8b8b8b);\n --rvs-hover: var(--dsw-alias-interactive-bg-hover, rgba(127, 127, 127, .14));\n --rvs-active: var(--dsw-alias-interactive-bg-active, rgba(77, 107, 254, .22));\n --rvs-border: var(--dsw-alias-border-l1, rgba(127, 127, 127, .25));\n --rvs-border2: var(--dsw-alias-border-l2, rgba(127, 127, 127, .38));\n --rvs-input: var(--dsw-alias-bg-input, rgba(127, 127, 127, .12));\n --rvs-overlay: var(--dsw-alias-bg-overlay, #24262e);\n --rvs-layer3: var(--dsw-alias-bg-layer-3, rgba(127, 127, 127, .16));\n --rvs-brand: var(--dsw-alias-brand-primary, #4d6bfe);\n --rvs-accent: var(--dsw-alias-button-info-fill, #4d6bfe);\n --rvs-danger: var(--dsw-alias-state-error-primary, #e05555);\n --rvs-git-m: #e2b714;\n --rvs-git-a: #4ec9b0;\n --rvs-git-d: #f14c4c;\n --rvs-git-r: #4ec9b0;\n --rvs-git-c: #58a6ff;\n --rvs-git-u: #f14c4c;\n --rvs-git-x: #6e6e6e;\n --rvs-add-bg: rgba(78, 201, 176, .15);\n --rvs-del-bg: rgba(241, 76, 76, .15);\n /* Opaque equivalents, for elements that must not show what scrolls under\n them (the pinned diff line numbers). */\n --rvs-add-solid: #e4f6f1;\n --rvs-del-solid: #fdeaea;\n --rvs-mono: 'Cascadia Code', 'Fira Code', 'JetBrains Mono', Consolas, 'Courier New', monospace;\n /* Opaque code surfaces (Light+ default; the dark block below flips them). */\n --rvs-code-bg: #ffffff;\n --rvs-code-fg: #1f1f1f;\n --rvs-code-gutter-fg: #237893;\n\n display: flex;\n flex-direction: row;\n height: 100%;\n min-height: 0;\n overflow: hidden;\n color: var(--rvs-fg);\n font-size: 13px;\n line-height: 20px;\n}\n\n/**\n * Code surfaces are OPAQUE, in the editor's own palette rather than the DSH\n * skin: syntax colors need a definite background to stay readable, and the\n * app shell marks its resolved scheme on <body> (same signal theme.ts reads).\n */\nbody[data-ds-dark-theme] .rvs-root {\n --rvs-code-bg: #1e1e1e;\n --rvs-code-fg: #d4d4d4;\n --rvs-code-gutter-fg: #858585;\n --rvs-add-solid: #16332e;\n --rvs-del-solid: #3b2325;\n}\n\n.rvs-edge {\n background: var(--rvs-code-bg);\n color: var(--rvs-code-fg);\n}\n\n.rvs-root *,\n.rvs-portal * {\n box-sizing: border-box;\n}\n\n/* ------------------------------------------------------------ activity rail */\n\n.rvs-rail {\n flex: none;\n width: 38px;\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 4px;\n padding: 8px 0;\n border-right: 1px solid var(--rvs-border);\n background: var(--dsw-specific-sidebar-fill, transparent);\n}\n\n.rvs-rail-btn {\n position: relative;\n flex: none;\n width: 30px;\n height: 30px;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 0;\n border: none;\n border-radius: 6px;\n background: transparent;\n color: var(--rvs-fg3);\n cursor: pointer;\n}\n\n.rvs-rail-btn:hover {\n color: var(--rvs-fg);\n background: var(--rvs-hover);\n}\n\n.rvs-rail-btn.active {\n color: var(--rvs-fg);\n background: var(--rvs-hover);\n box-shadow: inset 2px 0 0 var(--rvs-brand);\n}\n\n.rvs-rail-btn.danger:hover {\n color: var(--rvs-danger);\n}\n\n.rvs-rail-badge {\n position: absolute;\n bottom: 1px;\n right: 1px;\n min-width: 14px;\n height: 14px;\n padding: 0 3px;\n border-radius: 7px;\n background: var(--rvs-accent);\n color: #fff;\n font-size: 9px;\n font-weight: 700;\n line-height: 14px;\n text-align: center;\n pointer-events: none;\n}\n\n.rvs-rail-sep {\n flex: none;\n width: 20px;\n height: 1px;\n margin: 4px 0;\n background: var(--rvs-border);\n}\n\n/* -------------------------------------------------------- panel + editor */\n\n.rvs-column {\n flex: 1;\n min-width: 0;\n min-height: 0;\n display: flex;\n flex-direction: column;\n}\n\n.rvs-pane {\n display: flex;\n flex-direction: column;\n min-height: 0;\n min-width: 0;\n overflow: hidden;\n}\n\n/**\n * One lazily-mounted panel slot: a visited panel stays mounted and is merely\n * hidden, so returning to the explorer (or the terminal) neither refetches nor\n * flashes a reload.\n */\n.rvs-viewslot {\n flex: 1;\n min-height: 0;\n min-width: 0;\n flex-direction: column;\n height: 100%;\n overflow: hidden;\n}\n\n/**\n * Padding for the legacy views (workspaces / terminal) that draw no chrome.\n *\n * It MUST be a flex column: the workspaces view scroller is its child and gets\n * its bounded height only from a flex parent (`flex:1; min-height:0`). As a\n * plain block it grew with its content instead, so the panel clipped at the\n * viewslot's `overflow:hidden` and the binding list could not scroll at all —\n * a seventh workspace (and the wizard under it) was simply unreachable.\n */\n.rvs-viewpad {\n display: flex;\n flex-direction: column;\n min-height: 0;\n padding: 10px 12px 14px;\n box-sizing: border-box;\n}\n\n.rvs-pane.grow {\n flex: 1;\n}\n\n.rvs-header {\n flex: none;\n display: flex;\n align-items: center;\n gap: 6px;\n height: 34px;\n padding: 0 8px 0 10px;\n border-bottom: 1px solid var(--rvs-border);\n}\n\n.rvs-header .rvs-title {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n font-size: 11px;\n font-weight: 700;\n letter-spacing: .04em;\n text-transform: uppercase;\n color: var(--rvs-fg3);\n}\n\n.rvs-hbtn {\n flex: none;\n width: 22px;\n height: 22px;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 0;\n border: none;\n border-radius: 5px;\n background: transparent;\n color: var(--rvs-fg3);\n cursor: pointer;\n}\n\n.rvs-hbtn:hover {\n color: var(--rvs-fg);\n background: var(--rvs-hover);\n}\n\n.rvs-hbtn.danger:hover {\n color: #fff;\n background: var(--rvs-danger);\n}\n\n.rvs-hbtn:disabled {\n opacity: .4;\n cursor: default;\n}\n\n.rvs-scroll {\n flex: 1;\n min-height: 0;\n overflow: auto;\n scrollbar-width: thin;\n}\n\n.rvs-globalbar {\n flex: none;\n display: flex;\n align-items: center;\n gap: 6px;\n padding: 4px 8px;\n border-bottom: 1px solid var(--rvs-border);\n font-size: 11px;\n}\n\n.rvs-scroll::-webkit-scrollbar {\n width: 8px;\n height: 8px;\n}\n\n.rvs-scroll::-webkit-scrollbar-thumb {\n background: var(--dsw-alias-scrollbar-bg-l2, rgba(127, 127, 127, .4));\n border-radius: 4px;\n}\n\n.rvs-scroll::-webkit-scrollbar-thumb:hover {\n background: var(--dsw-alias-scrollbar-hover-l2, rgba(127, 127, 127, .6));\n}\n\n.rvs-scroll::-webkit-scrollbar-track {\n background: transparent;\n}\n\n.rvs-vsplit-handle {\n flex: none;\n position: relative;\n height: 7px;\n cursor: row-resize;\n touch-action: none;\n}\n\n.rvs-vsplit-handle::after {\n content: '';\n position: absolute;\n left: 0;\n right: 0;\n top: 3px;\n height: 1px;\n background: var(--rvs-border);\n}\n\n.rvs-vsplit-handle:hover::after,\n.rvs-vsplit-handle[data-dragging='true']::after {\n top: 2px;\n height: 3px;\n border-radius: 2px;\n background: var(--rvs-brand);\n}\n\n/* ------------------------------------------------------------------ tree */\n\n.rvs-tree {\n padding: 4px 6px 12px;\n min-width: 0;\n}\n\n.rvs-node {\n display: flex;\n align-items: center;\n gap: 6px;\n min-width: 0;\n width: 100%;\n padding: 4px 8px;\n border: none;\n background: transparent;\n border-radius: 8px;\n color: var(--rvs-fg);\n font: inherit;\n font-size: 13px;\n line-height: 18px;\n text-align: left;\n cursor: pointer;\n}\n\n.rvs-node:hover {\n background: var(--rvs-hover);\n}\n\n.rvs-node.selected {\n background: var(--rvs-active);\n}\n\n.rvs-node.cut {\n opacity: .5;\n}\n\n.rvs-node.drop-target {\n background: var(--rvs-active);\n outline: 1px solid var(--rvs-brand);\n outline-offset: -1px;\n}\n\n/* Where a file drag will land: a slip must not silently drop a tree into the\n wrong directory. */\n.rvs-drop-hint {\n margin: 2px 8px 4px;\n padding: 3px 8px;\n border: 1px dashed var(--rvs-brand);\n border-radius: 3px;\n font-size: 11px;\n color: var(--rvs-brand);\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n direction: rtl;\n unicode-bidi: plaintext;\n}\n/* Upload progress: one line under the header. A multi-minute transfer with no\n feedback is indistinguishable from a hung explorer, so the row names the\n count and the file on the wire. */\n.rvs-upload-bar {\n flex: none;\n display: flex;\n align-items: center;\n gap: 6px;\n padding: 3px 10px 4px;\n font-size: 11px;\n color: var(--dsw-alias-text-secondary, rgba(127, 127, 127, .95));\n}\n\n.rvs-upload-text {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n direction: rtl;\n unicode-bidi: plaintext;\n}\n\n.rvs-node-icon {\n flex: none;\n width: 16px;\n height: 16px;\n display: flex;\n align-items: center;\n justify-content: center;\n color: var(--rvs-fg3);\n}\n\n.rvs-node-name {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.rvs-node-actions {\n flex: none;\n display: none;\n align-items: center;\n gap: 2px;\n}\n\n.rvs-node:hover .rvs-node-actions {\n display: flex;\n}\n\n.rvs-git-letter {\n flex: none;\n width: 14px;\n margin-left: 2px;\n font-size: 11px;\n font-weight: 700;\n text-align: center;\n}\n\n.rvs-git-M, .rvs-letter-M { color: var(--rvs-git-m); }\n.rvs-git-A, .rvs-letter-A { color: var(--rvs-git-a); }\n.rvs-git-D, .rvs-letter-D { color: var(--rvs-git-d); }\n.rvs-git-R, .rvs-letter-R { color: var(--rvs-git-r); }\n.rvs-git-C, .rvs-letter-C { color: var(--rvs-git-c); }\n.rvs-git-U, .rvs-letter-U { color: var(--rvs-git-u); }\n.rvs-git-q, .rvs-letter-q, .rvs-git-\\\\? { color: var(--rvs-git-a); }\n.rvs-git-x, .rvs-letter-x { color: var(--rvs-git-x); }\n.rvs-git-T, .rvs-letter-T { color: var(--rvs-git-m); }\n\n.rvs-rename-input {\n flex: 1;\n min-width: 0;\n padding: 0 4px;\n border: 1px solid var(--rvs-brand);\n border-radius: 4px;\n background: var(--rvs-input);\n color: var(--rvs-fg);\n font: inherit;\n font-size: 13px;\n line-height: 18px;\n outline: none;\n}\n\n.rvs-empty {\n padding: 18px 14px;\n color: var(--rvs-fg3);\n font-size: 12px;\n text-align: center;\n line-height: 1.7;\n}\n\n.rvs-banner {\n margin: 6px 8px;\n padding: 6px 9px;\n border-radius: 6px;\n font-size: 12px;\n line-height: 1.5;\n word-break: break-word;\n white-space: pre-wrap;\n}\n\n.rvs-banner.info {\n background: var(--rvs-hover);\n color: var(--rvs-fg2);\n}\n\n.rvs-banner.error {\n background: rgba(200, 60, 60, .16);\n color: var(--rvs-danger);\n}\n\n.rvs-banner.warn {\n background: rgba(226, 183, 20, .14);\n color: var(--rvs-git-m);\n}\n\n/* --------------------------------------------------------- source control */\n\n.rvs-scm-section {\n border-bottom: 1px solid var(--rvs-border);\n}\n\n.rvs-scm-head {\n display: flex;\n align-items: center;\n gap: 4px;\n height: 28px;\n padding: 0 6px 0 4px;\n cursor: pointer;\n user-select: none;\n font-size: 11px;\n font-weight: 700;\n letter-spacing: .04em;\n text-transform: uppercase;\n color: var(--rvs-fg2);\n}\n\n.rvs-scm-head:hover {\n background: var(--rvs-hover);\n}\n\n.rvs-scm-head .rvs-count {\n margin-left: 6px;\n font-size: 11px;\n font-weight: 600;\n letter-spacing: 0;\n color: var(--rvs-fg3);\n}\n\n.rvs-scm-head-actions {\n margin-left: auto;\n display: flex;\n align-items: center;\n gap: 2px;\n}\n\n.rvs-scm-section.collapsed > *:not(.rvs-scm-head) {\n display: none !important;\n}\n\n.rvs-chevron {\n flex: none;\n transition: transform .12s ease;\n}\n\n.rvs-chevron.open {\n transform: rotate(90deg);\n}\n\n.rvs-scm-item {\n display: flex;\n align-items: center;\n gap: 6px;\n width: 100%;\n padding: 3px 8px 3px 22px;\n border: none;\n background: transparent;\n color: var(--rvs-fg);\n font: inherit;\n font-size: 12.5px;\n line-height: 19px;\n text-align: left;\n cursor: pointer;\n}\n\n.rvs-scm-item:hover {\n background: var(--rvs-hover);\n}\n\n.rvs-scm-item.selected {\n background: var(--rvs-active);\n}\n\n.rvs-scm-path {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.rvs-scm-dir {\n flex: none;\n color: var(--rvs-fg3);\n}\n\n.rvs-scm-actions {\n flex: none;\n display: none;\n align-items: center;\n gap: 1px;\n}\n\n.rvs-scm-item:hover .rvs-scm-actions {\n display: flex;\n}\n\n.rvs-commit-box {\n flex: none;\n padding: 8px 10px;\n border-bottom: 1px solid var(--rvs-border);\n}\n\n.rvs-commit-input {\n width: 100%;\n min-height: 58px;\n max-height: 180px;\n padding: 6px 8px;\n border: 1px solid var(--rvs-border2);\n border-radius: 8px;\n background: var(--rvs-input);\n color: var(--rvs-fg);\n font: inherit;\n font-size: 12.5px;\n line-height: 18px;\n resize: vertical;\n outline: none;\n}\n\n.rvs-commit-input:focus {\n border-color: var(--rvs-brand);\n}\n\n.rvs-commit-row {\n display: flex;\n gap: 6px;\n margin-top: 6px;\n}\n\n.rvs-commit-btn {\n flex: 1;\n padding: 5px 8px;\n border: none;\n border-radius: 6px;\n background: var(--rvs-accent);\n color: #fff;\n font: inherit;\n font-size: 12.5px;\n font-weight: 600;\n cursor: pointer;\n}\n\n.rvs-commit-btn:disabled {\n opacity: .45;\n cursor: default;\n}\n\n.rvs-repo-item {\n display: flex;\n align-items: center;\n gap: 6px;\n padding: 5px 10px 5px 12px;\n cursor: pointer;\n font-size: 12.5px;\n}\n\n.rvs-repo-item:hover {\n background: var(--rvs-hover);\n}\n\n.rvs-repo-item.active {\n background: var(--rvs-active);\n}\n\n.rvs-repo-name {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.rvs-repo-branch {\n flex: none;\n font-size: 11px;\n color: var(--rvs-fg3);\n}\n\n.rvs-pill {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n padding: 1px 8px;\n border-radius: 999px;\n background: var(--rvs-hover);\n font-size: 11px;\n font-weight: 600;\n color: var(--rvs-fg);\n}\n\n.rvs-subpanel {\n margin: 6px 8px;\n padding: 8px;\n border: 1px solid var(--rvs-border2);\n border-radius: 8px;\n font-size: 12px;\n}\n\n.rvs-subpanel-head {\n display: flex;\n align-items: center;\n gap: 6px;\n margin-bottom: 6px;\n color: var(--rvs-fg2);\n font-weight: 600;\n}\n\n.rvs-chip {\n flex: none;\n padding: 2px 8px;\n border: 1px solid var(--rvs-border2);\n border-radius: 6px;\n background: transparent;\n color: var(--rvs-fg);\n font: inherit;\n font-size: 11.5px;\n cursor: pointer;\n}\n\n.rvs-chip:hover {\n background: var(--rvs-hover);\n}\n\n.rvs-chip.primary {\n background: var(--rvs-accent);\n border-color: transparent;\n color: #fff;\n}\n\n.rvs-chip.danger:hover {\n background: var(--rvs-danger);\n border-color: transparent;\n color: #fff;\n}\n\n.rvs-input {\n flex: 1;\n min-width: 0;\n padding: 4px 8px;\n border: 1px solid var(--rvs-border2);\n border-radius: 6px;\n background: var(--rvs-input);\n color: var(--rvs-fg);\n font: inherit;\n font-size: 12px;\n outline: none;\n}\n\n.rvs-input:focus {\n border-color: var(--rvs-brand);\n}\n\n.rvs-select {\n flex: 1;\n min-width: 0;\n padding: 3px 6px;\n border: 1px solid var(--rvs-border2);\n border-radius: 6px;\n background: var(--rvs-input);\n color: var(--rvs-fg);\n font: inherit;\n font-size: 12px;\n outline: none;\n color-scheme: light dark;\n}\n\n.rvs-select option {\n background: Canvas;\n color: CanvasText;\n}\n\n/* ------------------------------------------------- conversation-view mount */\n\n/**\n * Bound the center surface to the height the host gave the slot.\n *\n * `flex: 1 1 0` is the load-bearing declaration, and BOTH halves of it matter.\n * The host's own slot rules are:\n *\n * .wSkVaW_viewArea { flex: 1; min-height: 0; overflow: hidden }\n * [data-phase=active] .viewArea { flex: 1 0 auto; min-height: auto }\n * .wSkVaW_root { height: 100%; position: relative }\n *\n * While a conversation is active the slot's flex-basis is `auto`, so the slot's\n * height is driven by its child's INTRINSIC size. A full-height editor then\n * pushed it to 5510px for a 300-line file, the outer scroller took over, and\n * the tab strip and file info bar scrolled off the top with the code.\n *\n * `flex-basis: 0` breaks that cycle: the item stops contributing its content\n * height, so the slot stays the size the host intended and `.cm-scroller`\n * becomes the only scroller. Measured in the fixture (.spike/layout-fixture.html,\n * which mirrors the slot rules above) — only the bottom row fits:\n *\n * flex: 1 1 0 + min-height: 0 -> 736px OK\n * flex: 1 1 auto (i.e. `flex: 1`) -> 5510px broken\n * height: 0 + min-height: 100% -> 5510px broken\n * max-height: 100% -> 5510px broken\n * height: 100vh -> 5510px broken\n *\n * `height: 100%` is deliberately NOT set here any more. It resolves to `auto`\n * against a slot whose height is not definite — it reads like the fix but\n * measures as a no-op, which is how this regressed once already.\n *\n * NOT `position: absolute` either — that was another failed attempt: `.viewArea`\n * is not itself positioned, so `inset: 0` escaped to the host's `.wSkVaW_root`\n * (which is `position: relative`) and the plugin's global bar floated on top of\n * the host's 对话 / 轨迹 / 远程编辑器 tab row.\n *\n * The sidebar's `.rvs-viewslot` is deliberately not covered: it has a bounding\n * parent and this bug is specific to the conversation slot's `min-height: auto`\n * phase.\n */\n.rvs-center {\n flex: 1 1 0;\n display: flex;\n flex-direction: column;\n min-height: 0;\n min-width: 0;\n overflow: hidden;\n}\n\n/* ---------------------------------------------------------------- editor */\n\n.rvs-etabs {\n flex: none;\n display: flex;\n align-items: stretch;\n gap: 2px;\n height: 32px;\n padding: 0 4px;\n overflow-x: auto;\n overflow-y: hidden;\n border-bottom: 1px solid var(--rvs-border);\n scrollbar-width: none;\n}\n\n.rvs-etabs::-webkit-scrollbar {\n height: 0;\n}\n\n.rvs-etab {\n flex: none;\n display: flex;\n align-items: center;\n gap: 5px;\n max-width: 190px;\n margin: 4px 0;\n padding: 0 4px 0 8px;\n border: none;\n border-radius: 6px;\n background: transparent;\n color: var(--rvs-fg3);\n font: inherit;\n font-size: 12px;\n cursor: pointer;\n}\n\n.rvs-etab:hover {\n background: var(--rvs-hover);\n color: var(--rvs-fg);\n}\n\n.rvs-etab.active {\n background: var(--rvs-layer3);\n color: var(--rvs-fg);\n}\n\n.rvs-etab-name {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.rvs-etab-dot {\n flex: none;\n font-size: 9px;\n color: var(--rvs-git-m);\n}\n\n.rvs-etab-close {\n flex: none;\n width: 16px;\n height: 16px;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 0;\n border: none;\n border-radius: 4px;\n background: transparent;\n color: inherit;\n cursor: pointer;\n opacity: .65;\n}\n\n.rvs-etab-close:hover {\n background: var(--rvs-hover);\n opacity: 1;\n}\n\n.rvs-einfo {\n flex: none;\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 2px 8px 2px 10px;\n border-bottom: 1px solid var(--rvs-border);\n font-size: 11px;\n line-height: 18px;\n color: var(--rvs-fg3);\n}\n\n.rvs-einfo-path {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n /* Truncate from the LEFT (keep the tail) without reordering the text:\n `plaintext` gives the content its own LTR base direction, which is what\n stops a leading \"/\" from rendering as a trailing one. */\n direction: rtl;\n unicode-bidi: plaintext;\n text-align: left;\n}\n\n.rvs-einfo-spacer {\n margin-left: auto;\n}\n\n.rvs-status.dirty { color: var(--rvs-git-m); }\n.rvs-status.saved { color: var(--rvs-git-a); }\n.rvs-status.saving { color: var(--rvs-fg2); }\n\n.rvs-body {\n flex: 1;\n min-height: 0;\n min-width: 0;\n display: flex;\n flex-direction: column;\n overflow: hidden;\n /* Anchors portalled overlays (the find widget) outside every scroller. */\n position: relative;\n background: var(--rvs-code-bg);\n color: var(--rvs-code-fg);\n}\n\n.rvs-editor-host {\n flex: 1;\n min-height: 0;\n overflow: hidden;\n background: var(--rvs-code-bg);\n}\n\n/* Keep a panel mounted but out of layout (and out of the a11y tree). Used by\n the editor host in preview mode: unmounting it destroyed the buffer and the\n undo stack, hiding it preserves both. */\n.rvs-hidden {\n display: none !important;\n}\n\n/* Find-match marks. Painted by our own StateField (`findMatchHighlight`), and\n deliberately the SAME colours as the `.cm-searchMatch` rules below so the\n two paths cannot drift apart. */\n.rvs-find-match {\n background-color: rgba(226, 183, 20, .30);\n outline: 1px solid rgba(226, 183, 20, .55);\n border-radius: 2px;\n}\n\n.rvs-find-match.current {\n background-color: rgba(226, 183, 20, .48);\n}\n\n/* ------------------------------------------------------- preview surfaces */\n\n.rvs-preview-img {\n flex: 1;\n min-height: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n overflow: auto;\n background-color: var(--rvs-code-bg);\n background-image:\n linear-gradient(45deg, rgba(127, 127, 127, .08) 25%, transparent 25%),\n linear-gradient(-45deg, rgba(127, 127, 127, .08) 25%, transparent 25%),\n linear-gradient(45deg, transparent 75%, rgba(127, 127, 127, .08) 75%),\n linear-gradient(-45deg, transparent 75%, rgba(127, 127, 127, .08) 75%);\n background-size: 20px 20px, 20px 20px, 20px 20px, 20px 20px;\n background-position: 0 0, 0 10px, 10px -10px, -10px 0;\n}\n\n.rvs-preview-img img {\n max-width: 100%;\n max-height: 100%;\n object-fit: contain;\n image-rendering: auto;\n}\n\n.rvs-markdown {\n flex: 1;\n min-height: 0;\n overflow: auto;\n padding: 12px 14px 24px;\n font-size: 13px;\n line-height: 1.7;\n color: var(--rvs-code-fg);\n background: var(--rvs-code-bg);\n user-select: text;\n}\n\n.rvs-markdown h1, .rvs-markdown h2, .rvs-markdown h3, .rvs-markdown h4 {\n margin: 14px 0 6px;\n line-height: 1.35;\n}\n\n.rvs-markdown h1 { font-size: 19px; border-bottom: 1px solid var(--rvs-border); padding-bottom: 5px; }\n.rvs-markdown h2 { font-size: 16px; border-bottom: 1px solid var(--rvs-border); padding-bottom: 4px; }\n.rvs-markdown h3 { font-size: 14px; }\n.rvs-markdown p { margin: 6px 0; }\n.rvs-markdown ul, .rvs-markdown ol { margin: 6px 0; padding-left: 22px; }\n.rvs-markdown li { margin: 2px 0; }\n.rvs-markdown code {\n padding: 1px 5px;\n border-radius: 4px;\n background: var(--rvs-hover);\n font-family: var(--rvs-mono);\n font-size: 12px;\n}\n.rvs-markdown pre {\n margin: 8px 0;\n padding: 9px 11px;\n border-radius: 8px;\n background: var(--rvs-input);\n overflow-x: auto;\n}\n.rvs-markdown pre code { padding: 0; background: transparent; }\n.rvs-markdown blockquote {\n margin: 8px 0;\n padding: 2px 12px;\n border-left: 3px solid var(--rvs-border2);\n color: var(--rvs-fg2);\n}\n.rvs-markdown a { color: var(--dsw-alias-label-link, #58a6ff); text-decoration: none; }\n.rvs-markdown a:hover { text-decoration: underline; }\n.rvs-markdown table { border-collapse: collapse; margin: 8px 0; font-size: 12px; }\n.rvs-markdown th, .rvs-markdown td { border: 1px solid var(--rvs-border2); padding: 4px 8px; }\n.rvs-markdown hr { border: none; border-top: 1px solid var(--rvs-border); margin: 12px 0; }\n\n.rvs-html-frame {\n flex: 1;\n min-height: 0;\n width: 100%;\n border: none;\n background: #fff;\n}\n\n/* ------------------------------------------------------------------ diff */\n\n.rvs-diff {\n flex: 1;\n min-height: 0;\n display: flex;\n flex-direction: column;\n overflow: hidden;\n}\n\n.rvs-diff-toolbar {\n flex: none;\n display: flex;\n align-items: center;\n gap: 6px;\n padding: 3px 8px;\n border-bottom: 1px solid var(--rvs-border);\n font-size: 11px;\n color: var(--rvs-fg3);\n}\n\n/**\n * Prev/next change, in the toolbar's free space.\n *\n * Two chevrons around a tabular counter: the pair then reads as \"jump to change\n * n of N\" instead of two anonymous arrows, and the counter doubles as the \"where\n * am I\" cue while scrolling by hand.\n */\n.rvs-hunknav {\n display: inline-flex;\n align-items: center;\n gap: 1px;\n margin-right: 2px;\n}\n\n.rvs-hunknav-pos {\n min-width: 30px;\n text-align: center;\n font-size: 11px;\n font-variant-numeric: tabular-nums;\n color: var(--rvs-fg3);\n user-select: none;\n}\n\n.rvs-seg {\n display: inline-flex;\n border: 1px solid var(--rvs-border2);\n border-radius: 6px;\n overflow: hidden;\n}\n\n.rvs-seg button {\n padding: 2px 9px;\n border: none;\n background: transparent;\n color: var(--rvs-fg2);\n font: inherit;\n font-size: 11px;\n cursor: pointer;\n}\n\n.rvs-seg button.active {\n background: var(--rvs-active);\n color: var(--rvs-fg);\n}\n\n.rvs-seg button:hover:not(.active) {\n background: var(--rvs-hover);\n}\n\n.rvs-diff-grid {\n flex: 1;\n min-height: 0;\n overflow-y: auto;\n overflow-x: hidden;\n font-family: var(--rvs-mono);\n font-size: 12px;\n line-height: 18px;\n scrollbar-width: thin;\n background: var(--rvs-code-bg);\n color: var(--rvs-code-fg);\n}\n\n/**\n * Top horizontal scrollbar.\n *\n * The two panes scroll horizontally on their own — so a long line can never\n * widen the layout — and this proxy bar drives both of them from the TOP of\n * the view, instead of forcing a scroll to the bottom of the file to reach a\n * horizontal bar. Its inner spacer is sized to the widest line, and the\n * component only renders it when a line really is wider than the grid.\n *\n * Sized and coloured to be SEEN: as a 13 px band with `scrollbar-width: thin`\n * and a faint thumb, it read as \"there is no horizontal scrollbar at all\".\n */\n.rvs-hbar {\n flex: none;\n height: 15px;\n overflow-x: auto;\n overflow-y: hidden;\n border-bottom: 1px solid var(--rvs-border);\n background: var(--rvs-hover);\n}\n\n.rvs-hbar-inner {\n height: 1px;\n}\n\n.rvs-hbar::-webkit-scrollbar {\n height: 11px;\n}\n\n.rvs-hbar::-webkit-scrollbar-thumb {\n background: var(--dsw-alias-scrollbar-bg-l2, rgba(150, 160, 180, .75));\n border-radius: 6px;\n border: 2px solid transparent;\n background-clip: padding-box;\n}\n\n.rvs-hbar::-webkit-scrollbar-thumb:hover {\n background: var(--dsw-alias-scrollbar-hover-l2, rgba(170, 180, 200, .95));\n background-clip: padding-box;\n}\n\n.rvs-hbar::-webkit-scrollbar-track {\n background: transparent;\n}\n\n.rvs-diff-cols {\n display: flex;\n align-items: stretch;\n min-width: 0;\n}\n\n/**\n * One diff pane. `flex: 1 1 0` keeps the panes EXACTLY equal however long a\n * line is (the previous row markup let a long line widen its own pane, which\n * is what broke the symmetry). Each pane keeps its own horizontal scroll, and\n * its scrollbar is now VISIBLE: the top proxy remains the convenient control,\n * but the pane's own bar is what a user reaches for first, and it sits right\n * under the content when the file is shorter than the view.\n */\n.rvs-diff-col {\n flex: 1 1 0;\n min-width: 0;\n overflow-x: auto;\n overflow-y: hidden;\n}\n\n.rvs-diff-col::-webkit-scrollbar {\n height: 9px;\n width: 0;\n}\n\n.rvs-diff-col::-webkit-scrollbar-thumb {\n background: var(--dsw-alias-scrollbar-bg-l2, rgba(150, 160, 180, .6));\n border-radius: 5px;\n}\n\n.rvs-diff-col::-webkit-scrollbar-thumb:hover {\n background: var(--dsw-alias-scrollbar-hover-l2, rgba(170, 180, 200, .85));\n}\n\n.rvs-diff-col::-webkit-scrollbar-track {\n background: transparent;\n}\n\n.rvs-dline {\n display: flex;\n align-items: flex-start;\n height: 18px;\n line-height: 18px;\n white-space: pre;\n}\n\n.rvs-dline-num {\n /* Pinned while the line scrolls sideways, so a long line never takes the\n line numbers away with it. */\n position: sticky;\n left: 0;\n z-index: 1;\n flex: none;\n width: 3.4em;\n padding-right: 6px;\n text-align: right;\n color: var(--rvs-fg3);\n opacity: .85;\n user-select: none;\n background: var(--rvs-code-bg);\n}\n\n.rvs-row-del .rvs-dline-num {\n background: var(--rvs-del-solid);\n}\n\n.rvs-row-add .rvs-dline-num {\n background: var(--rvs-add-solid);\n}\n\n.rvs-dline-text {\n flex: 1;\n padding-right: 10px;\n user-select: text;\n}\n\n.rvs-gutter-col {\n flex: none;\n width: 30px;\n border-left: 1px solid var(--rvs-border);\n border-right: 1px solid var(--rvs-border);\n user-select: none;\n}\n\n.rvs-gutter-line {\n height: 18px;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n.rvs-diff-grid::-webkit-scrollbar {\n width: 9px;\n height: 0;\n}\n\n.rvs-diff-grid::-webkit-scrollbar-thumb {\n background: var(--dsw-alias-scrollbar-bg-l2, rgba(127, 127, 127, .4));\n border-radius: 4px;\n}\n\n.rvs-num {\n flex: none;\n display: inline-block;\n width: 3.2em;\n padding-right: 6px;\n text-align: right;\n color: var(--rvs-fg3);\n opacity: .6;\n user-select: none;\n}\n\n.rvs-row-del {\n background: var(--rvs-del-bg);\n}\n\n.rvs-row-del .rvs-dline-text,\n.rvs-row-del .rvs-dline-num,\n.rvs-row-del .rvs-num {\n color: #f14c4c;\n}\n\n.rvs-row-add {\n background: var(--rvs-add-bg);\n}\n\n.rvs-row-add .rvs-dline-text,\n.rvs-row-add .rvs-dline-num,\n.rvs-row-add .rvs-num {\n color: #4ec9b0;\n}\n\n.rvs-row-empty {\n color: var(--rvs-fg3);\n opacity: .35;\n}\n\n.rvs-gutter {\n flex: none;\n width: 30px;\n display: flex;\n align-items: center;\n justify-content: center;\n border-left: 1px solid var(--rvs-border);\n border-right: 1px solid var(--rvs-border);\n user-select: none;\n}\n\n.rvs-gutter-dot {\n width: 6px;\n height: 6px;\n border-radius: 50%;\n opacity: .7;\n}\n\n.rvs-hunk-btns {\n display: flex;\n gap: 2px;\n}\n\n.rvs-hunk-btn {\n width: 13px;\n height: 15px;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 0;\n border: none;\n border-radius: 3px;\n background: transparent;\n color: var(--rvs-fg3);\n font-size: 11px;\n line-height: 1;\n cursor: pointer;\n}\n\n.rvs-hunk-btn:hover {\n background: var(--rvs-hover);\n color: var(--rvs-fg);\n}\n\n.rvs-hunk-btn.stage:hover {\n color: var(--rvs-git-a);\n}\n\n.rvs-hunk-btn.revert:hover {\n color: var(--rvs-git-d);\n}\n\n/**\n * Token colors: VS Code Dark+ / Light+, matching the CodeMirror theme in\n * `vsc/editor-theme.ts`. Keeping ONE palette for both surfaces means a file\n * does not change colors when the diff is switched to its editable side.\n */\n.rvs-t-comment { color: #6a9955; }\n.rvs-t-string { color: #ce9178; }\n.rvs-t-keyword { color: #569cd6; }\n.rvs-t-number { color: #b5cea8; }\n.rvs-t-func { color: #dcdcaa; }\n.rvs-t-type { color: #4ec9b0; }\n.rvs-t-attr { color: #9cdcfe; }\n.rvs-t-variable { color: #9cdcfe; }\n.rvs-t-operator { color: #d4d4d4; }\n.rvs-t-punct { color: #d4d4d4; }\n.rvs-t-tag { color: #569cd6; }\n.rvs-t-meta { color: #c586c0; }\n.rvs-t-insert { color: #4ec9b0; }\n.rvs-t-delete { color: #f14c4c; }\n\nbody:not([data-ds-dark-theme]) .rvs-t-comment { color: #008000; }\nbody:not([data-ds-dark-theme]) .rvs-t-string { color: #a31515; }\nbody:not([data-ds-dark-theme]) .rvs-t-keyword { color: #0000ff; }\nbody:not([data-ds-dark-theme]) .rvs-t-number { color: #098658; }\nbody:not([data-ds-dark-theme]) .rvs-t-func { color: #795e26; }\nbody:not([data-ds-dark-theme]) .rvs-t-type { color: #267f99; }\nbody:not([data-ds-dark-theme]) .rvs-t-attr,\nbody:not([data-ds-dark-theme]) .rvs-t-variable { color: #001080; }\nbody:not([data-ds-dark-theme]) .rvs-t-operator,\nbody:not([data-ds-dark-theme]) .rvs-t-punct { color: #1f1f1f; }\nbody:not([data-ds-dark-theme]) .rvs-t-meta { color: #af00db; }\n\n/**\n * CodeMirror token colors, applied through FIXED class names emitted by the\n * highlight style (`vsc/editor-palette.ts`). Fixed names mean the colors come\n * from this stylesheet — the same one that paints the rest of the UI — so a\n * code file can never go monochrome because a generated class name changed or\n * an injected style failed. Same Dark+ / Light+ palette as the diff panes\n * above, so a file keeps its colors when switching between the two views.\n */\n.rvs-ck-comment { color: #6a9955; }\n.rvs-ck-keyword { color: #569cd6; }\n.rvs-ck-control { color: #c586c0; }\n.rvs-ck-string { color: #ce9178; }\n.rvs-ck-regexp { color: #d16969; }\n.rvs-ck-number { color: #b5cea8; }\n.rvs-ck-constant { color: #569cd6; }\n.rvs-ck-func { color: #dcdcaa; }\n.rvs-ck-type { color: #4ec9b0; }\n.rvs-ck-variable { color: #9cdcfe; }\n.rvs-ck-attr { color: #9cdcfe; }\n.rvs-ck-meta { color: #c586c0; }\n.rvs-ck-punct { color: #d4d4d4; }\n.rvs-ck-op { color: #d4d4d4; }\n.rvs-ck-heading { color: #569cd6; font-weight: 700; }\n.rvs-ck-em { font-style: italic; }\n.rvs-ck-link { color: #9cdcfe; text-decoration: underline; }\n.rvs-ck-strike { text-decoration: line-through; }\n.rvs-ck-invalid { color: #f44747; }\n\nbody:not([data-ds-dark-theme]) .rvs-ck-comment { color: #008000; }\nbody:not([data-ds-dark-theme]) .rvs-ck-keyword { color: #0000ff; }\nbody:not([data-ds-dark-theme]) .rvs-ck-control { color: #af00db; }\nbody:not([data-ds-dark-theme]) .rvs-ck-string { color: #a31515; }\nbody:not([data-ds-dark-theme]) .rvs-ck-regexp { color: #811f3f; }\nbody:not([data-ds-dark-theme]) .rvs-ck-number { color: #098658; }\nbody:not([data-ds-dark-theme]) .rvs-ck-constant { color: #0070c1; }\nbody:not([data-ds-dark-theme]) .rvs-ck-func { color: #795e26; }\nbody:not([data-ds-dark-theme]) .rvs-ck-type { color: #267f99; }\nbody:not([data-ds-dark-theme]) .rvs-ck-variable { color: #001080; }\nbody:not([data-ds-dark-theme]) .rvs-ck-attr { color: #e50000; }\nbody:not([data-ds-dark-theme]) .rvs-ck-meta { color: #0000ff; }\nbody:not([data-ds-dark-theme]) .rvs-ck-punct,\nbody:not([data-ds-dark-theme]) .rvs-ck-op { color: #1f1f1f; }\nbody:not([data-ds-dark-theme]) .rvs-ck-heading { color: #0000ff; }\nbody:not([data-ds-dark-theme]) .rvs-ck-link { color: #001080; }\nbody:not([data-ds-dark-theme]) .rvs-ck-invalid { color: #cd3131; }\n\n/**\n * Opaque editor surface, also as plain CSS: the CodeMirror theme paints it too\n * (following the scheme through a Compartment), but a transparent editor is\n * the one failure a user notices immediately, so the container states it as\n * well.\n */\n.rvs-edge .cm-editor,\n.rvs-edge .cm-scroller,\n.rvs-edge .cm-gutters {\n background-color: var(--rvs-code-bg);\n}\n\n.rvs-edge .cm-editor {\n color: var(--rvs-code-fg);\n}\n\n.rvs-edge .cm-gutters {\n color: var(--rvs-code-gutter-fg, var(--rvs-fg3));\n border-right: 1px solid var(--rvs-border);\n}\n\n.rvs-edge .cm-activeLine {\n background-color: rgba(127, 127, 127, .08);\n}\n\n.rvs-edge .cm-activeLineGutter {\n background-color: rgba(127, 127, 127, .12);\n}\n\n/**\n * The code editor's own scrollbars.\n *\n * These had NO rule at all: CodeMirror's scroller is `.cm-scroller`, which is\n * none of `.rvs-scroll` / `.rvs-diff-grid` / `.rvs-hbar`, so the editor fell\n * back to the browser default — too thin to hit with a mouse.\n *\n * Both notations are needed and must stay in step: Chromium honours\n * `scrollbar-width` over `::-webkit-scrollbar` when both apply, so setting\n * only the webkit half would silently have no effect. Keep the numbers equal.\n */\n.rvs-edge .cm-scroller {\n scrollbar-width: auto;\n}\n\n.rvs-edge .cm-scroller::-webkit-scrollbar {\n width: 14px;\n height: 14px;\n}\n\n.rvs-edge .cm-scroller::-webkit-scrollbar-track {\n background: transparent;\n}\n\n.rvs-edge .cm-scroller::-webkit-scrollbar-thumb {\n /* Opaque, not a translucent token: a thin wash over code is exactly the\n low-contrast target that is hard to grab. */\n background: rgba(121, 121, 121, .8);\n border: 3px solid transparent;\n border-radius: 7px;\n background-clip: padding-box;\n min-height: 36px;\n}\n\n.rvs-edge .cm-scroller::-webkit-scrollbar-thumb:hover {\n background: rgba(150, 150, 150, .95);\n border: 3px solid transparent;\n background-clip: padding-box;\n}\n\n.rvs-edge .cm-scroller::-webkit-scrollbar-corner {\n background: transparent;\n}\n\n/* ------------------------------------------------------------ find widget */\n\n/**\n * \"You landed here\": the transient highlight on the line a jump landed on\n * (search hit, @-reference).\n *\n * Deliberately NOT a warm/amber wash: over the dark editor that muddies into a\n * brown-black band that reads as a shadow and swallows annotation colors. This\n * is the accent blue instead — a tinted band with a bright edge on the left and\n * hairlines top/bottom, so it reads as a marked region while every token keeps\n * its own colour and contrast.\n */\n.rvs-reveal-line {\n background-color: rgba(88, 166, 255, .22);\n box-shadow:\n inset 3px 0 0 0 #58a6ff,\n inset 0 1px 0 0 rgba(88, 166, 255, .38),\n inset 0 -1px 0 0 rgba(88, 166, 255, .38);\n}\n\nbody:not([data-ds-dark-theme]) .rvs-reveal-line {\n background-color: rgba(77, 107, 254, .14);\n box-shadow:\n inset 3px 0 0 0 #4d6bfe,\n inset 0 1px 0 0 rgba(77, 107, 254, .35),\n inset 0 -1px 0 0 rgba(77, 107, 254, .35);\n}\n\n/* --------------------------------------------------------- session changes */\n\n/**\n * 「本会话改了什么」:代码行背景 + 左侧改动栏(Cursor 风格)。\n *\n * 三种形态必须**一眼分得开**,光靠绿/红两个色相不够(色弱用户看新增行和删除行\n * 会糊成同一片),所以每种的左侧色条宽度/花样的语义也不同:\n * - 新增:实心绿条;\n * - 修改:实心蓝条(不是绿:修改块里既有留下的行也有新写的行,用\"中性的已变更\"\n * 色不会误导读者以为整块都是新内容);\n * - 删除:**斜纹**红底 + 更粗的红条 —— 这一行其实已经不存在了,只在中止位置上\n * 留个锚点,斜纹是这一层\"虚\"的信息里最不容易被当成真内容的花样。\n *\n * 选择器带上 `.cm-line`:`.cm-activeLine` 由 CodeMirror 的主题注入(运行时\n * `<style>`,通常排在本文档之后),只写 `.rvs-change-*` 两者同权重时会输给它,\n * 光标所在的那一行改动就会消失。\n */\n.rvs-edge .cm-line.rvs-change-add {\n background-color: rgba(46, 160, 67, .17);\n box-shadow:\n inset 3px 0 0 0 #2ea043,\n inset 0 -1px 0 0 rgba(46, 160, 67, .28);\n}\n\n.rvs-edge .cm-line.rvs-change-mod {\n background-color: rgba(56, 139, 253, .15);\n box-shadow:\n inset 3px 0 0 0 #388bfd,\n inset 0 -1px 0 0 rgba(56, 139, 253, .28);\n}\n\n.rvs-edge .cm-line.rvs-change-del {\n background-color: rgba(248, 81, 73, .09);\n background-image: repeating-linear-gradient(135deg, rgba(248, 81, 73, .17) 0 6px, transparent 6px 12px);\n box-shadow: inset 4px 0 0 0 #f85149;\n}\n\n/* 改动栏本身:符号 + 一列与行背景同色的窄条,读起来才是一条\"改动边\"。 */\n.rvs-edge .cm-gutterElement.rvs-change-gutter-add {\n box-shadow: inset 2px 0 0 0 #2ea043;\n}\n\n.rvs-edge .cm-gutterElement.rvs-change-gutter-mod {\n box-shadow: inset 2px 0 0 0 #388bfd;\n}\n\n.rvs-edge .cm-gutterElement.rvs-change-gutter-del {\n box-shadow: inset 2px 0 0 0 #f85149;\n}\n\n.rvs-edge .rvs-change-mark {\n display: inline-block;\n width: 12px;\n font-family: var(--rvs-mono);\n font-size: 12px;\n line-height: 1;\n text-align: center;\n user-select: none;\n}\n\n.rvs-edge .rvs-change-mark-add { color: #2ea043; }\n.rvs-edge .rvs-change-mark-mod { color: #388bfd; }\n.rvs-edge .rvs-change-mark-del { color: #f85149; }\n\n/**\n * 行内(字符级)差异:上面那三种底色回答\"哪几行变了\",这两条回答\"那一行的哪几个\n * 字变了\"。只改一个词的编辑,整行同色时完全看不出改了什么。\n *\n * `rvs-change-word-add` 刻意**只给背景、不给 `color`**:代码有自己的语法着色,\n * 覆盖前景色会把关键字/字符串/注释染成同一种颜色,比不标还难读。透明度压在行\n * 底色之上一点点,叠起来仍读得出\"整块改动\"的边界,又能看出块内哪几个字是新的。\n */\n.rvs-edge .rvs-change-word-add {\n background-color: rgba(46, 160, 67, .22);\n border-radius: 2px;\n}\n\n/**\n * 被删掉、在新文件里已经不存在的内容:零宽 widget,红底 + 删除线。\n *\n * 删除线画在 chip 自己身上(chip 是独立元素,不会波及周围代码);`white-space:\n * pre` 保留被删文本自己的缩进 —— 截断/去空白都会让 chip 谎报\"删了什么\"。\n * 字号略小 + 降一点不透明度,是让它读起来像\"残留物\"而不是一行真代码。\n */\n.rvs-edge .rvs-change-word-del {\n padding: 0 3px;\n margin: 0 1px;\n border-radius: 3px;\n background-color: rgba(248, 81, 73, .18);\n font-size: .92em;\n text-decoration: line-through;\n opacity: .85;\n white-space: pre;\n}\n\n/**\n * 撑宽度的隐形符号:只用它占位,绝不显示(见 `change-marks.ts` 的 SpacerMarker,\n * CodeMirror 会给这个 spacer 元素加 `visibility: hidden`,这里不重复声明)。\n */\n.rvs-edge .rvs-change-spacer {\n opacity: 0;\n}\n\n/* 浅色主题:同样的色相,压暗 + 提高不透明度,在近白底上才看得清。 */\nbody:not([data-ds-dark-theme]) .rvs-edge .cm-line.rvs-change-add {\n background-color: rgba(31, 136, 61, .13);\n box-shadow:\n inset 3px 0 0 0 #1f883d,\n inset 0 -1px 0 0 rgba(31, 136, 61, .22);\n}\n\nbody:not([data-ds-dark-theme]) .rvs-edge .cm-line.rvs-change-mod {\n background-color: rgba(9, 105, 218, .12);\n box-shadow:\n inset 3px 0 0 0 #0969da,\n inset 0 -1px 0 0 rgba(9, 105, 218, .22);\n}\n\nbody:not([data-ds-dark-theme]) .rvs-edge .cm-line.rvs-change-del {\n background-color: rgba(207, 34, 46, .07);\n background-image: repeating-linear-gradient(135deg, rgba(207, 34, 46, .14) 0 6px, transparent 6px 12px);\n box-shadow: inset 4px 0 0 0 #cf222e;\n}\n\nbody:not([data-ds-dark-theme]) .rvs-edge .cm-gutterElement.rvs-change-gutter-add {\n box-shadow: inset 2px 0 0 0 #1f883d;\n}\n\nbody:not([data-ds-dark-theme]) .rvs-edge .cm-gutterElement.rvs-change-gutter-mod {\n box-shadow: inset 2px 0 0 0 #0969da;\n}\n\nbody:not([data-ds-dark-theme]) .rvs-edge .cm-gutterElement.rvs-change-gutter-del {\n box-shadow: inset 2px 0 0 0 #cf222e;\n}\n\nbody:not([data-ds-dark-theme]) .rvs-edge .rvs-change-mark-add { color: #1f883d; }\nbody:not([data-ds-dark-theme]) .rvs-edge .rvs-change-mark-mod { color: #0969da; }\nbody:not([data-ds-dark-theme]) .rvs-edge .rvs-change-mark-del { color: #cf222e; }\n\n/* 浅色主题的行内差异:同样的色相压暗,在近白底上才看得清(暗色那套会糊成一片)。 */\nbody:not([data-ds-dark-theme]) .rvs-edge .rvs-change-word-add {\n background-color: rgba(26, 127, 55, .18);\n}\n\nbody:not([data-ds-dark-theme]) .rvs-edge .rvs-change-word-del {\n background-color: rgba(207, 34, 46, .14);\n}\n\n/**\n * 改动跳转按钮:浮在编辑器右上角。\n *\n * 放在编辑器上而不是面板里 —— 文件打开后光标就在这里,而面板可能被收起、被滚走。\n * 容器 `.rvs-body` 已经是 `position: relative`(它本来用于挂 find 浮层),所以这里\n * 只做绝对定位,不引入新的定位上下文。`pointer-events` 只开在按钮上:这块区域压在\n * 编辑器文字上方,整块可点会把右上角那几行变成死区。\n */\n.rvs-change-nav {\n position: absolute;\n top: 8px;\n right: 14px;\n z-index: 5;\n display: flex;\n align-items: center;\n gap: 2px;\n padding: 3px 4px;\n border: 1px solid var(--rvs-border2);\n border-radius: 8px;\n background: Canvas;\n box-shadow: 0 2px 8px rgba(0, 0, 0, .28);\n pointer-events: none;\n}\n\n/* The counter is what makes this read as \"3 of 7 changes\" rather than two\n anonymous arrows — and it keeps the buttons from looking like decoration. */\n.rvs-change-nav-count {\n padding: 0 6px 0 4px;\n color: var(--rvs-fg2);\n font-size: 11px;\n font-variant-numeric: tabular-nums;\n line-height: 22px;\n white-space: nowrap;\n}\n\n.rvs-change-nav-btn {\n width: 26px;\n height: 22px;\n padding: 0;\n border: 1px solid transparent;\n border-radius: 5px;\n background: transparent;\n color: var(--rvs-fg);\n font-size: 13px;\n font-weight: 600;\n line-height: 20px;\n text-align: center;\n cursor: pointer;\n pointer-events: auto;\n}\n\n.rvs-change-nav-btn:hover {\n border-color: var(--rvs-border2);\n background: var(--rvs-hover);\n color: var(--rvs-brand);\n}\n\n.rvs-change-nav-btn:active { background: var(--rvs-active); }\n\n/**\n * 悬浮在改动行上出现的「撤销这处改动」按钮。\n *\n * 它**不在编辑器的 DOM 里**:`undo-affordance.ts` 把它当 `position: fixed` 的元素挂到\n * `document.body`(和「添加到对话」那个浮层同一套做法),所以这里的选择器不带\n * `.rvs-edge` —— 带上了就一条都不生效。固定定位 + 高 z-index 是刻意的:宿主容器有\n * `overflow: hidden`,挂在里面会被 CodeMirror 的滚动层裁掉。\n *\n * 颜色按\"危险动作\"给:默认红边红字、悬停实心红。底色不透明(不用半透明的 overlay\n * token),否则后面的代码文字会透出来。\n */\n.rvs-undo-affordance {\n position: fixed;\n z-index: 2100;\n display: inline-flex;\n align-items: center;\n gap: 4px;\n max-width: 220px;\n padding: 3px 9px;\n border: 1px solid #f85149;\n border-radius: 6px;\n background-color: #1e2229;\n color: #ffb4ae;\n /* 按钮挂在 body 上,没有 .rvs-edge 的字体上下文:跟随宿主页面字体。 */\n font-family: inherit;\n font-size: 12px;\n line-height: 1.5;\n white-space: nowrap;\n cursor: pointer;\n box-shadow: 0 6px 18px rgba(0, 0, 0, .45);\n /* 纵向以悬停行的中线对齐(横向坐标由 JS 按行尾算好)。 */\n transform: translateY(-50%);\n}\n\n.rvs-undo-affordance:hover {\n background-color: #f85149;\n border-color: #f85149;\n color: #fff;\n}\n\n.rvs-undo-affordance:active {\n transform: translateY(-50%) scale(.97);\n}\n\n.rvs-undo-affordance:focus-visible {\n outline: 2px solid #58a6ff;\n outline-offset: 1px;\n}\n\n/* 删除锚点:这一行其实已经不在了,虚线边框把它和\"真实存在的行\"分开。 */\n.rvs-undo-affordance[data-kind=\"del\"] {\n border-style: dashed;\n}\n\nbody:not([data-ds-dark-theme]) .rvs-undo-affordance {\n border-color: #cf222e;\n background-color: #ffffff;\n color: #a40e26;\n box-shadow: 0 6px 18px rgba(31, 35, 40, .18);\n}\n\nbody:not([data-ds-dark-theme]) .rvs-undo-affordance:hover {\n background-color: #cf222e;\n border-color: #cf222e;\n color: #fff;\n}\n\nbody:not([data-ds-dark-theme]) .rvs-undo-affordance:focus-visible {\n outline-color: #4d6bfe;\n}\n\n/**\n * CodeMirror's own search decorations, also stated here: the editor's theme is\n * injected by the library, and these must stay readable even when only this\n * stylesheet is in play.\n */\n.rvs-edge .cm-searchMatch {\n background-color: rgba(226, 183, 20, .30);\n outline: 1px solid rgba(226, 183, 20, .55);\n}\n\n.rvs-edge .cm-searchMatch-selected {\n background-color: rgba(226, 183, 20, .48);\n}\n\n.rvs-edge .cm-selectionBackground,\n.rvs-edge .cm-content ::selection {\n background-color: rgba(58, 110, 165, .55) !important;\n}\n\n/** Build stamp in the editor info bar: identifies the loaded client bundle. */\n.rvs-build {\n flex: none;\n padding: 0 5px;\n border: 1px solid var(--rvs-border);\n border-radius: 4px;\n color: var(--rvs-fg3);\n font-size: 10px;\n font-family: var(--rvs-mono);\n opacity: .85;\n user-select: text;\n}\n\n.rvs-find {\n position: absolute;\n top: 6px;\n right: 18px;\n z-index: 30;\n display: flex;\n align-items: center;\n gap: 3px;\n padding: 3px 4px 3px 7px;\n border: 1px solid var(--rvs-border2);\n border-radius: 8px;\n background-color: Canvas;\n background-image: linear-gradient(var(--dsw-alias-bg-overlay, transparent), var(--dsw-alias-bg-overlay, transparent));\n color: var(--rvs-fg);\n box-shadow: 0 4px 14px rgba(0, 0, 0, .28);\n font-size: 12px;\n}\n\n.rvs-find-input {\n width: 170px;\n padding: 2px 6px;\n border: 1px solid var(--rvs-border2);\n border-radius: 5px;\n background: var(--rvs-input);\n color: var(--rvs-fg);\n font: inherit;\n font-size: 12px;\n outline: none;\n}\n\n.rvs-find-input:focus {\n border-color: var(--rvs-brand);\n}\n\n.rvs-find-count {\n flex: none;\n min-width: 54px;\n text-align: center;\n color: var(--rvs-fg3);\n font-size: 11px;\n white-space: nowrap;\n}\n\n.rvs-find-count.none {\n color: var(--rvs-git-d);\n}\n\n.rvs-code {\n flex: 1;\n min-height: 0;\n overflow: auto;\n margin: 0;\n padding: 6px 0 16px;\n font-family: var(--rvs-mono);\n font-size: 12.5px;\n line-height: 18px;\n white-space: pre;\n scrollbar-width: thin;\n background: var(--rvs-code-bg);\n color: var(--rvs-code-fg);\n}\n\n.rvs-code-line {\n display: flex;\n align-items: flex-start;\n min-width: max-content;\n}\n\n.rvs-code-line:hover {\n background: var(--rvs-hover);\n}\n\n.rvs-code-text {\n flex: 1;\n padding-right: 10px;\n white-space: pre-wrap;\n word-break: break-word;\n user-select: text;\n}\n\n/* ---------------------------------------------------------- history list */\n\n.rvs-commit-item {\n display: flex;\n align-items: center;\n gap: 6px;\n width: 100%;\n padding: 4px 8px 4px 4px;\n border: none;\n background: transparent;\n color: var(--rvs-fg);\n font: inherit;\n font-size: 12.5px;\n line-height: 18px;\n text-align: left;\n cursor: pointer;\n}\n\n.rvs-commit-item:hover {\n background: var(--rvs-hover);\n}\n\n.rvs-commit-item.selected {\n background: var(--rvs-active);\n}\n\n.rvs-graph-svg {\n flex: none;\n display: block;\n}\n\n.rvs-commit-hash {\n flex: none;\n font-family: var(--rvs-mono);\n font-size: 11px;\n color: var(--rvs-fg3);\n}\n\n.rvs-commit-msg {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.rvs-commit-date {\n flex: none;\n font-size: 11px;\n color: var(--rvs-fg3);\n}\n\n.rvs-ref-badge {\n flex: none;\n padding: 0 5px;\n border-radius: 8px;\n border: 1px solid var(--rvs-border2);\n font-size: 10px;\n line-height: 15px;\n color: var(--rvs-fg2);\n}\n\n.rvs-ref-badge.head {\n border-color: var(--rvs-brand);\n background: rgba(77, 107, 254, .2);\n color: var(--rvs-fg);\n}\n\n.rvs-ref-badge.tag {\n border-color: rgba(226, 183, 20, .55);\n background: rgba(226, 183, 20, .16);\n color: var(--rvs-git-m);\n}\n\n.rvs-detail {\n margin: 0 6px 8px;\n padding: 8px 10px;\n border: 1px solid var(--rvs-border2);\n border-left: 3px solid var(--rvs-accent);\n border-radius: 8px;\n background: var(--rvs-layer3);\n font-size: 12px;\n}\n\n.rvs-detail-meta {\n color: var(--rvs-fg3);\n font-size: 11px;\n margin-bottom: 6px;\n white-space: pre-wrap;\n}\n\n.rvs-file-row {\n display: flex;\n align-items: center;\n gap: 6px;\n padding: 2px 4px;\n border-radius: 5px;\n cursor: pointer;\n}\n\n.rvs-file-row:hover {\n background: var(--rvs-hover);\n}\n\n.rvs-file-path {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n /* Same tail-first truncation as the info bar; `plaintext` keeps the path's\n own LTR order (see .rvs-einfo-path). */\n direction: rtl;\n unicode-bidi: plaintext;\n text-align: left;\n}\n\n.rvs-file-status {\n flex: none;\n width: 16px;\n text-align: center;\n font-weight: 700;\n font-size: 11px;\n}\n\n/* --------------------------------------------------------- context menu */\n\n.rvs-portal {\n font-family: inherit;\n}\n\n/**\n * Floating surfaces (menu / modal / toast) paint an OPAQUE base first and then\n * lay the skin's overlay token on top. The token is frequently translucent, so\n * using it alone let the panel's own text bleed through the menu and overlap\n * the entries; `Canvas` follows the resolved light/dark scheme, so the base is\n * correct in either theme.\n */\n.rvs-menu,\n.rvs-modal-box,\n.rvs-toast {\n background-color: Canvas;\n background-image: linear-gradient(var(--dsw-alias-bg-overlay, transparent), var(--dsw-alias-bg-overlay, transparent));\n color: var(--rvs-fg);\n}\n\n.rvs-menu {\n position: fixed;\n z-index: 1000;\n min-width: 172px;\n max-height: calc(100vh - 16px);\n overflow-y: auto;\n padding: 4px;\n border: 1px solid var(--rvs-border2);\n border-radius: 8px;\n box-shadow: 0 6px 20px rgba(0, 0, 0, .35);\n font-size: 12.5px;\n line-height: 18px;\n}\n\n.rvs-menu-sep {\n height: 1px;\n margin: 4px 6px;\n background: var(--rvs-border);\n}\n\n.rvs-menu-item {\n display: flex;\n align-items: center;\n gap: 8px;\n width: 100%;\n padding: 5px 10px;\n border: none;\n border-radius: 6px;\n background: transparent;\n color: inherit;\n font: inherit;\n text-align: left;\n cursor: pointer;\n}\n\n.rvs-menu-item:hover {\n background: var(--rvs-hover);\n}\n\n.rvs-menu-item.danger {\n color: var(--vsc-danger, #f14c4c);\n}\n\n.rvs-menu-item.danger:hover {\n background: rgba(241, 76, 76, .16);\n}\n\n.rvs-menu-item:disabled {\n opacity: .45;\n cursor: default;\n}\n\n.rvs-menu-item:disabled:hover {\n background: transparent;\n}\n\n.rvs-menu-hint {\n margin-left: auto;\n font-size: 11px;\n opacity: .6;\n}\n\n/* ------------------------------------------------------- modal + toast */\n\n.rvs-modal-mask {\n position: fixed;\n inset: 0;\n z-index: 100000;\n display: flex;\n align-items: center;\n justify-content: center;\n background: rgba(0, 0, 0, .45);\n backdrop-filter: blur(2px);\n animation: rvsFade .12s ease;\n}\n\n.rvs-modal-box {\n width: min(420px, calc(100vw - 40px));\n padding: 16px 18px;\n border: 1px solid var(--rvs-border2);\n border-radius: 10px;\n color: var(--rvs-fg);\n box-shadow: 0 12px 40px rgba(0, 0, 0, .38);\n font-size: 13px;\n animation: rvsPop .14s ease;\n}\n\n.rvs-modal-title {\n margin-bottom: 8px;\n font-size: 14px;\n font-weight: 600;\n}\n\n.rvs-modal-text {\n margin-bottom: 12px;\n color: var(--rvs-fg2);\n font-size: 12.5px;\n line-height: 1.6;\n word-break: break-word;\n white-space: pre-wrap;\n}\n\n.rvs-modal-row {\n display: flex;\n justify-content: flex-end;\n gap: 8px;\n margin-top: 14px;\n}\n\n.rvs-modal-btn {\n padding: 5px 14px;\n border: 1px solid var(--rvs-border2);\n border-radius: 7px;\n background: transparent;\n color: var(--rvs-fg);\n font: inherit;\n font-size: 12.5px;\n cursor: pointer;\n}\n\n.rvs-modal-btn:hover {\n background: var(--rvs-hover);\n}\n\n.rvs-modal-btn.primary {\n border-color: transparent;\n background: var(--rvs-accent);\n color: #fff;\n}\n\n.rvs-modal-btn.danger {\n border-color: transparent;\n background: var(--rvs-danger);\n color: #fff;\n}\n\n.rvs-toasts {\n position: fixed;\n right: 16px;\n bottom: 18px;\n z-index: 100001;\n display: flex;\n flex-direction: column;\n gap: 8px;\n align-items: flex-end;\n pointer-events: none;\n}\n\n.rvs-toast {\n max-width: 340px;\n padding: 7px 12px;\n border: 1px solid var(--rvs-border2);\n border-radius: 8px;\n color: var(--rvs-fg);\n box-shadow: 0 6px 20px rgba(0, 0, 0, .35);\n font-size: 12.5px;\n line-height: 1.5;\n word-break: break-word;\n white-space: pre-wrap;\n animation: rvsPop .14s ease;\n}\n\n.rvs-toast.error {\n border-color: rgba(241, 76, 76, .6);\n background-image: linear-gradient(rgba(58, 31, 34, .92), rgba(58, 31, 34, .92));\n}\n\n/* ------------------------------------------------------------- terminal */\n\n.rvs-term-tabs {\n flex: none;\n display: flex;\n align-items: stretch;\n gap: 2px;\n height: 32px;\n padding: 0 4px;\n overflow-x: auto;\n overflow-y: hidden;\n border-bottom: 1px solid var(--rvs-border);\n scrollbar-width: none;\n}\n\n.rvs-term-tabs::-webkit-scrollbar {\n height: 0;\n}\n\n.rvs-term-tab {\n flex: none;\n display: flex;\n align-items: center;\n gap: 6px;\n max-width: 180px;\n margin: 4px 0;\n padding: 0 6px 0 9px;\n border: none;\n border-radius: 6px;\n background: transparent;\n color: var(--rvs-fg3);\n font: inherit;\n font-size: 12px;\n cursor: pointer;\n}\n\n/* Wrapper that pairs a tab with its close button. The two are siblings so the\n ✕ can be a real focusable <button> (a nested one is invalid HTML and left\n keyboard users no way to close a tab). */\n.rvs-term-tabwrap {\n position: relative;\n flex: none;\n display: flex;\n align-items: center;\n}\n\n.rvs-term-tabwrap .rvs-term-tab {\n padding-right: 22px;\n}\n\n.rvs-term-tabwrap .rvs-etab-close {\n position: absolute;\n right: 3px;\n opacity: 0;\n}\n\n.rvs-term-tabwrap:hover .rvs-etab-close,\n.rvs-term-tabwrap:focus-within .rvs-etab-close {\n opacity: .8;\n}\n\n.rvs-term-tab:hover {\n background: var(--rvs-hover);\n color: var(--rvs-fg);\n}\n\n.rvs-term-tab.active {\n background: var(--rvs-layer3);\n color: var(--rvs-fg);\n}\n\n.rvs-term-tab.dead {\n color: var(--rvs-git-d);\n}\n\n.rvs-term-name {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.rvs-term-body {\n flex: 1;\n min-height: 0;\n position: relative;\n overflow: hidden;\n}\n\n.rvs-term-slot {\n position: absolute;\n inset: 0;\n}\n\n.rvs-term-dead {\n height: 100%;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 8px;\n background: var(--rvs-code-bg);\n color: var(--rvs-code-fg);\n}\n\n/* `.rvs-toast.error` is styled with the overlay form further up, so the popup\n opacity convention (an opaque Canvas base plus a token layer) still governs\n it. A flat `background: #3a1f22` shorthand sat here and overrode that chain —\n same opaque result, but outside the convention. */\n\n@keyframes rvsFade {\n from { opacity: 0; }\n to { opacity: 1; }\n}\n\n@keyframes rvsPop {\n from { opacity: 0; transform: scale(.97) translateY(3px); }\n to { opacity: 1; transform: none; }\n}\n\n.rvs-search-row {\n display: flex;\n gap: 6px;\n padding: 6px 8px;\n border-bottom: 1px solid var(--rvs-border);\n}\n\n/* --------------------------------------------------------- search options */\n\n.rvs-toggles {\n flex: none;\n display: inline-flex;\n border: 1px solid var(--rvs-border2);\n border-radius: 6px;\n overflow: hidden;\n}\n\n.rvs-toggle {\n min-width: 26px;\n height: 22px;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 0 5px;\n border: none;\n border-right: 1px solid var(--rvs-border);\n background: transparent;\n color: var(--rvs-fg3);\n font: inherit;\n cursor: pointer;\n}\n\n.rvs-toggle:last-child {\n border-right: none;\n}\n\n.rvs-toggle:hover {\n background: var(--rvs-hover);\n color: var(--rvs-fg);\n}\n\n.rvs-toggle.active {\n background: var(--rvs-active);\n color: var(--rvs-fg);\n box-shadow: inset 0 0 0 1px var(--rvs-brand);\n}\n\n.rvs-glyph {\n font-family: var(--rvs-mono);\n font-weight: 600;\n line-height: 1;\n letter-spacing: -0.02em;\n}\n\n.rvs-glyph-underline {\n text-decoration: underline;\n}\n\n.rvs-scope-label {\n flex: none;\n align-self: center;\n font-size: 11px;\n color: var(--rvs-fg3);\n white-space: nowrap;\n}\n\n/* --------------------------------------------------------- search results */\n\n.rvs-hit {\n display: flex;\n align-items: center;\n gap: 6px;\n width: 100%;\n padding: 3px 10px;\n border: none;\n background: transparent;\n color: var(--rvs-fg);\n font: inherit;\n font-size: 12.5px;\n text-align: left;\n cursor: pointer;\n}\n\n.rvs-hit:hover {\n background: var(--rvs-hover);\n}\n\n.rvs-hit-file {\n font-weight: 600;\n}\n\n.rvs-hit-line {\n padding-left: 30px;\n align-items: flex-start;\n font-family: var(--rvs-mono);\n font-size: 12px;\n color: var(--rvs-fg2);\n}\n\n.rvs-hit-count {\n flex: none;\n min-width: 18px;\n padding: 0 5px;\n border-radius: 9px;\n background: var(--rvs-hover);\n font-size: 10.5px;\n line-height: 15px;\n text-align: center;\n color: var(--rvs-fg3);\n}\n\n.rvs-hit-lineno {\n flex: none;\n width: 3.2em;\n text-align: right;\n color: var(--rvs-fg3);\n opacity: .8;\n font-size: 11px;\n}\n\n.rvs-hit-text {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: pre;\n direction: ltr;\n}\n\n.rvs-mark {\n padding: 0 1px;\n border-radius: 2px;\n background: rgba(226, 183, 20, .3);\n color: inherit;\n}\n\n.rvs-hit-dir {\n flex: none;\n color: var(--rvs-fg3);\n font-size: 11px;\n}\n\n.rvs-hit-name {\n flex: none;\n font-weight: 600;\n}\n\n.rvs-kbd {\n padding: 0 4px;\n border: 1px solid var(--rvs-border2);\n border-radius: 4px;\n font-size: 10px;\n color: var(--rvs-fg3);\n}\n\n/* --- SCM: repository directory row (git runs in THIS directory) ------------- */\n\n.rvs-repo-bar {\n display: flex;\n align-items: center;\n gap: 4px;\n padding: 4px 8px 4px 10px;\n border-bottom: 1px solid var(--rvs-border);\n background: var(--rvs-bg2, var(--dsw-alias-bg-layer-1, #1b1b1b));\n font-size: 11px;\n flex: none;\n}\n\n.rvs-repo-bar-label {\n color: var(--rvs-fg3);\n white-space: nowrap;\n}\n\n.rvs-repo-bar-path {\n flex: 1 1 auto;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n direction: rtl;\n unicode-bidi: plaintext;\n text-align: left;\n padding: 2px 6px;\n border: 1px solid var(--rvs-border2);\n border-radius: 4px;\n background: var(--rvs-bg3, rgba(255, 255, 255, .04));\n color: var(--rvs-fg);\n font-family: var(--rvs-mono, ui-monospace, Consolas, monospace);\n font-size: 11px;\n cursor: pointer;\n}\n\n.rvs-repo-bar-path:hover {\n border-color: var(--rvs-brand);\n color: var(--rvs-brand-fg, var(--rvs-fg));\n}\n\n.rvs-repo-bar-fix {\n margin-top: 10px;\n display: flex;\n justify-content: center;\n}\n\n/* --- SCM: repository directory picker -------------------------------------- */\n\n.rvs-repopick {\n width: min(560px, calc(100vw - 40px));\n display: flex;\n flex-direction: column;\n gap: 8px;\n}\n\n.rvs-repopick .rvs-modal-title {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 8px;\n}\n\n.rvs-repopick-bar {\n display: flex;\n align-items: center;\n flex-wrap: wrap;\n gap: 2px;\n min-height: 22px;\n font-family: var(--rvs-mono, ui-monospace, Consolas, monospace);\n font-size: 12px;\n}\n\n.rvs-crumb-wrap {\n display: inline-flex;\n align-items: center;\n gap: 2px;\n color: var(--rvs-fg3);\n}\n\n.rvs-crumb {\n border: none;\n background: none;\n padding: 1px 3px;\n border-radius: 3px;\n color: var(--rvs-brand-fg, var(--rvs-fg));\n font: inherit;\n cursor: pointer;\n}\n\n.rvs-crumb:hover {\n background: var(--rvs-row-hover, rgba(255, 255, 255, .07));\n}\n\n.rvs-repopick-probe {\n padding: 5px 8px;\n border-radius: 4px;\n font-size: 11.5px;\n border: 1px solid var(--rvs-border2);\n background: var(--rvs-bg3, rgba(255, 255, 255, .04));\n color: var(--rvs-fg2);\n}\n\n.rvs-repopick-probe.root {\n border-color: var(--rvs-ok, #2ea043);\n color: var(--rvs-ok, #2ea043);\n}\n\n.rvs-repopick-probe.inside {\n border-color: var(--rvs-brand);\n color: var(--rvs-brand-fg, var(--rvs-fg));\n}\n\n.rvs-repopick-found {\n display: flex;\n align-items: center;\n flex-wrap: wrap;\n gap: 4px;\n padding-bottom: 2px;\n}\n\n.rvs-repopick-found-label {\n color: var(--rvs-fg3);\n font-size: 11px;\n white-space: nowrap;\n}\n\n.rvs-repopick-list {\n flex: 1 1 auto;\n min-height: 150px;\n max-height: 42vh;\n overflow: auto;\n border: 1px solid var(--rvs-border);\n border-radius: 5px;\n background: var(--rvs-bg-layer, rgba(0, 0, 0, .18));\n padding: 2px;\n}\n\n.rvs-repopick-row {\n display: flex;\n align-items: center;\n gap: 6px;\n width: 100%;\n padding: 3px 6px;\n border: none;\n border-radius: 3px;\n background: none;\n color: var(--rvs-fg);\n font: inherit;\n font-size: 12px;\n text-align: left;\n cursor: pointer;\n}\n\n.rvs-repopick-row:hover {\n background: var(--rvs-row-hover, rgba(255, 255, 255, .07));\n}\n\n.rvs-repopick-row span {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.rvs-repopick-dim {\n color: var(--rvs-fg3);\n font-size: 11.5px;\n padding: 6px;\n}\n\n.rvs-repopick-type {\n display: flex;\n gap: 6px;\n align-items: center;\n}\n\n.rvs-repopick-type .rvs-input {\n flex: 1 1 auto;\n}\n\n.rvs-repopick-error {\n color: var(--rvs-error, #f85149);\n font-size: 11.5px;\n}\n";
|
|
14
|
+
const css$3 = "/**\n * VS Code-style design system for the remote explorer. Every surface below\n * is scoped under `.rvs-root` so the sheet can never leak into the host GUI,\n * and every host token carries a literal fallback: the plugin also renders in\n * the native right-Sidebar fallback and on hosts whose alias tokens differ.\n *\n * Palette and metrics are aligned with the reference implementation\n * (dsh-solution-explorer): 13px/20px panel type, 5px/10px tree rows with a\n * 10px radius, a 30px activity button with the badge tucked into the icon's\n * bottom-right corner, GitHub-Dark token colors, and the\n * `--e2b714 / #4ec9b0 / #f14c4c` modified/added/deleted triple.\n */\n\n.rvs-root {\n --rvs-fg: var(--dsw-alias-label-primary, #e6e6e6);\n --rvs-fg2: var(--dsw-alias-label-secondary, #b9b9b9);\n --rvs-fg3: var(--dsw-alias-label-tertiary, #8b8b8b);\n --rvs-hover: var(--dsw-alias-interactive-bg-hover, rgba(127, 127, 127, .14));\n --rvs-active: var(--dsw-alias-interactive-bg-active, rgba(77, 107, 254, .22));\n --rvs-border: var(--dsw-alias-border-l1, rgba(127, 127, 127, .25));\n --rvs-border2: var(--dsw-alias-border-l2, rgba(127, 127, 127, .38));\n --rvs-input: var(--dsw-alias-bg-input, rgba(127, 127, 127, .12));\n --rvs-overlay: var(--dsw-alias-bg-overlay, #24262e);\n --rvs-layer3: var(--dsw-alias-bg-layer-3, rgba(127, 127, 127, .16));\n --rvs-brand: var(--dsw-alias-brand-primary, #4d6bfe);\n --rvs-accent: var(--dsw-alias-button-info-fill, #4d6bfe);\n --rvs-danger: var(--dsw-alias-state-error-primary, #e05555);\n --rvs-git-m: #e2b714;\n --rvs-git-a: #4ec9b0;\n --rvs-git-d: #f14c4c;\n --rvs-git-r: #4ec9b0;\n --rvs-git-c: #58a6ff;\n --rvs-git-u: #f14c4c;\n --rvs-git-x: #6e6e6e;\n --rvs-add-bg: rgba(78, 201, 176, .15);\n --rvs-del-bg: rgba(241, 76, 76, .15);\n /* Opaque equivalents, for elements that must not show what scrolls under\n them (the pinned diff line numbers). */\n --rvs-add-solid: #e4f6f1;\n --rvs-del-solid: #fdeaea;\n --rvs-mono: 'Cascadia Code', 'Fira Code', 'JetBrains Mono', Consolas, 'Courier New', monospace;\n /* Opaque code surfaces (Light+ default; the dark block below flips them). */\n --rvs-code-bg: #ffffff;\n --rvs-code-fg: #1f1f1f;\n --rvs-code-gutter-fg: #237893;\n\n display: flex;\n flex-direction: row;\n height: 100%;\n min-height: 0;\n overflow: hidden;\n color: var(--rvs-fg);\n font-size: 13px;\n line-height: 20px;\n}\n\n/**\n * Code surfaces are OPAQUE, in the editor's own palette rather than the DSH\n * skin: syntax colors need a definite background to stay readable, and the\n * app shell marks its resolved scheme on <body> (same signal theme.ts reads).\n */\nbody[data-ds-dark-theme] .rvs-root {\n --rvs-code-bg: #1e1e1e;\n --rvs-code-fg: #d4d4d4;\n --rvs-code-gutter-fg: #858585;\n --rvs-add-solid: #16332e;\n --rvs-del-solid: #3b2325;\n}\n\n.rvs-edge {\n background: var(--rvs-code-bg);\n color: var(--rvs-code-fg);\n}\n\n.rvs-root *,\n.rvs-portal * {\n box-sizing: border-box;\n}\n\n/* ------------------------------------------------------------ activity rail */\n\n.rvs-rail {\n flex: none;\n width: 38px;\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 4px;\n padding: 8px 0;\n border-right: 1px solid var(--rvs-border);\n background: var(--dsw-specific-sidebar-fill, transparent);\n}\n\n.rvs-rail-btn {\n position: relative;\n flex: none;\n width: 30px;\n height: 30px;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 0;\n border: none;\n border-radius: 6px;\n background: transparent;\n color: var(--rvs-fg3);\n cursor: pointer;\n}\n\n.rvs-rail-btn:hover {\n color: var(--rvs-fg);\n background: var(--rvs-hover);\n}\n\n.rvs-rail-btn.active {\n color: var(--rvs-fg);\n background: var(--rvs-hover);\n box-shadow: inset 2px 0 0 var(--rvs-brand);\n}\n\n.rvs-rail-btn.danger:hover {\n color: var(--rvs-danger);\n}\n\n.rvs-rail-badge {\n position: absolute;\n bottom: 1px;\n right: 1px;\n min-width: 14px;\n height: 14px;\n padding: 0 3px;\n border-radius: 7px;\n background: var(--rvs-accent);\n color: #fff;\n font-size: 9px;\n font-weight: 700;\n line-height: 14px;\n text-align: center;\n pointer-events: none;\n}\n\n.rvs-rail-sep {\n flex: none;\n width: 20px;\n height: 1px;\n margin: 4px 0;\n background: var(--rvs-border);\n}\n\n/* -------------------------------------------------------- panel + editor */\n\n.rvs-column {\n flex: 1;\n min-width: 0;\n min-height: 0;\n display: flex;\n flex-direction: column;\n}\n\n.rvs-pane {\n display: flex;\n flex-direction: column;\n min-height: 0;\n min-width: 0;\n overflow: hidden;\n}\n\n/**\n * One lazily-mounted panel slot: a visited panel stays mounted and is merely\n * hidden, so returning to the explorer (or the terminal) neither refetches nor\n * flashes a reload.\n */\n.rvs-viewslot {\n flex: 1;\n min-height: 0;\n min-width: 0;\n flex-direction: column;\n height: 100%;\n overflow: hidden;\n}\n\n/**\n * Padding for the legacy views (workspaces / terminal) that draw no chrome.\n *\n * It MUST be a flex column: the workspaces view scroller is its child and gets\n * its bounded height only from a flex parent (`flex:1; min-height:0`). As a\n * plain block it grew with its content instead, so the panel clipped at the\n * viewslot's `overflow:hidden` and the binding list could not scroll at all —\n * a seventh workspace (and the wizard under it) was simply unreachable.\n */\n.rvs-viewpad {\n display: flex;\n flex-direction: column;\n min-height: 0;\n padding: 10px 12px 14px;\n box-sizing: border-box;\n}\n\n.rvs-pane.grow {\n flex: 1;\n}\n\n.rvs-header {\n flex: none;\n display: flex;\n align-items: center;\n gap: 6px;\n height: 34px;\n padding: 0 8px 0 10px;\n border-bottom: 1px solid var(--rvs-border);\n}\n\n.rvs-header .rvs-title {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n font-size: 11px;\n font-weight: 700;\n letter-spacing: .04em;\n text-transform: uppercase;\n color: var(--rvs-fg3);\n}\n\n.rvs-hbtn {\n flex: none;\n width: 22px;\n height: 22px;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 0;\n border: none;\n border-radius: 5px;\n background: transparent;\n color: var(--rvs-fg3);\n cursor: pointer;\n}\n\n.rvs-hbtn:hover {\n color: var(--rvs-fg);\n background: var(--rvs-hover);\n}\n\n.rvs-hbtn.danger:hover {\n color: #fff;\n background: var(--rvs-danger);\n}\n\n.rvs-hbtn:disabled {\n opacity: .4;\n cursor: default;\n}\n\n.rvs-scroll {\n flex: 1;\n min-height: 0;\n overflow: auto;\n scrollbar-width: thin;\n}\n\n.rvs-globalbar {\n flex: none;\n display: flex;\n align-items: center;\n gap: 6px;\n padding: 4px 8px;\n border-bottom: 1px solid var(--rvs-border);\n font-size: 11px;\n}\n\n.rvs-scroll::-webkit-scrollbar {\n width: 8px;\n height: 8px;\n}\n\n.rvs-scroll::-webkit-scrollbar-thumb {\n background: var(--dsw-alias-scrollbar-bg-l2, rgba(127, 127, 127, .4));\n border-radius: 4px;\n}\n\n.rvs-scroll::-webkit-scrollbar-thumb:hover {\n background: var(--dsw-alias-scrollbar-hover-l2, rgba(127, 127, 127, .6));\n}\n\n.rvs-scroll::-webkit-scrollbar-track {\n background: transparent;\n}\n\n.rvs-vsplit-handle {\n flex: none;\n position: relative;\n height: 7px;\n cursor: row-resize;\n touch-action: none;\n}\n\n.rvs-vsplit-handle::after {\n content: '';\n position: absolute;\n left: 0;\n right: 0;\n top: 3px;\n height: 1px;\n background: var(--rvs-border);\n}\n\n.rvs-vsplit-handle:hover::after,\n.rvs-vsplit-handle[data-dragging='true']::after {\n top: 2px;\n height: 3px;\n border-radius: 2px;\n background: var(--rvs-brand);\n}\n\n/* ------------------------------------------------------------------ tree */\n\n.rvs-tree {\n padding: 4px 6px 12px;\n min-width: 0;\n}\n\n.rvs-node {\n display: flex;\n align-items: center;\n gap: 6px;\n min-width: 0;\n width: 100%;\n padding: 4px 8px;\n border: none;\n background: transparent;\n border-radius: 8px;\n color: var(--rvs-fg);\n font: inherit;\n font-size: 13px;\n line-height: 18px;\n text-align: left;\n cursor: pointer;\n}\n\n.rvs-node:hover {\n background: var(--rvs-hover);\n}\n\n.rvs-node.selected {\n background: var(--rvs-active);\n}\n\n.rvs-node.cut {\n opacity: .5;\n}\n\n.rvs-node.drop-target {\n background: var(--rvs-active);\n outline: 1px solid var(--rvs-brand);\n outline-offset: -1px;\n}\n\n/* Where a file drag will land: a slip must not silently drop a tree into the\n wrong directory. */\n.rvs-drop-hint {\n margin: 2px 8px 4px;\n padding: 3px 8px;\n border: 1px dashed var(--rvs-brand);\n border-radius: 3px;\n font-size: 11px;\n color: var(--rvs-brand);\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n direction: rtl;\n unicode-bidi: plaintext;\n}\n/* Upload progress: one line under the header. A multi-minute transfer with no\n feedback is indistinguishable from a hung explorer, so the row names the\n count and the file on the wire. */\n.rvs-upload-bar {\n flex: none;\n display: flex;\n align-items: center;\n gap: 6px;\n padding: 3px 10px 4px;\n font-size: 11px;\n color: var(--dsw-alias-text-secondary, rgba(127, 127, 127, .95));\n}\n\n.rvs-upload-text {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n direction: rtl;\n unicode-bidi: plaintext;\n}\n\n.rvs-node-icon {\n flex: none;\n width: 16px;\n height: 16px;\n display: flex;\n align-items: center;\n justify-content: center;\n color: var(--rvs-fg3);\n}\n\n.rvs-node-name {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.rvs-node-actions {\n flex: none;\n display: none;\n align-items: center;\n gap: 2px;\n}\n\n.rvs-node:hover .rvs-node-actions {\n display: flex;\n}\n\n.rvs-git-letter {\n flex: none;\n width: 14px;\n margin-left: 2px;\n font-size: 11px;\n font-weight: 700;\n text-align: center;\n}\n\n.rvs-git-M, .rvs-letter-M { color: var(--rvs-git-m); }\n.rvs-git-A, .rvs-letter-A { color: var(--rvs-git-a); }\n.rvs-git-D, .rvs-letter-D { color: var(--rvs-git-d); }\n.rvs-git-R, .rvs-letter-R { color: var(--rvs-git-r); }\n.rvs-git-C, .rvs-letter-C { color: var(--rvs-git-c); }\n.rvs-git-U, .rvs-letter-U { color: var(--rvs-git-u); }\n.rvs-git-q, .rvs-letter-q, .rvs-git-\\\\? { color: var(--rvs-git-a); }\n.rvs-git-x, .rvs-letter-x { color: var(--rvs-git-x); }\n.rvs-git-T, .rvs-letter-T { color: var(--rvs-git-m); }\n\n.rvs-rename-input {\n flex: 1;\n min-width: 0;\n padding: 0 4px;\n border: 1px solid var(--rvs-brand);\n border-radius: 4px;\n background: var(--rvs-input);\n color: var(--rvs-fg);\n font: inherit;\n font-size: 13px;\n line-height: 18px;\n outline: none;\n}\n\n.rvs-empty {\n padding: 18px 14px;\n color: var(--rvs-fg3);\n font-size: 12px;\n text-align: center;\n line-height: 1.7;\n}\n\n.rvs-banner {\n margin: 6px 8px;\n padding: 6px 9px;\n border-radius: 6px;\n font-size: 12px;\n line-height: 1.5;\n word-break: break-word;\n white-space: pre-wrap;\n}\n\n.rvs-banner.info {\n background: var(--rvs-hover);\n color: var(--rvs-fg2);\n}\n\n.rvs-banner.error {\n background: rgba(200, 60, 60, .16);\n color: var(--rvs-danger);\n}\n\n.rvs-banner.warn {\n background: rgba(226, 183, 20, .14);\n color: var(--rvs-git-m);\n}\n\n/* --------------------------------------------------------- source control */\n\n.rvs-scm-section {\n border-bottom: 1px solid var(--rvs-border);\n}\n\n.rvs-scm-head {\n display: flex;\n align-items: center;\n gap: 4px;\n height: 28px;\n padding: 0 6px 0 4px;\n cursor: pointer;\n user-select: none;\n font-size: 11px;\n font-weight: 700;\n letter-spacing: .04em;\n text-transform: uppercase;\n color: var(--rvs-fg2);\n}\n\n.rvs-scm-head:hover {\n background: var(--rvs-hover);\n}\n\n.rvs-scm-head .rvs-count {\n margin-left: 6px;\n font-size: 11px;\n font-weight: 600;\n letter-spacing: 0;\n color: var(--rvs-fg3);\n}\n\n.rvs-scm-head-actions {\n margin-left: auto;\n display: flex;\n align-items: center;\n gap: 2px;\n}\n\n.rvs-scm-section.collapsed > *:not(.rvs-scm-head) {\n display: none !important;\n}\n\n.rvs-chevron {\n flex: none;\n transition: transform .12s ease;\n}\n\n.rvs-chevron.open {\n transform: rotate(90deg);\n}\n\n.rvs-scm-item {\n display: flex;\n align-items: center;\n gap: 6px;\n width: 100%;\n padding: 3px 8px 3px 22px;\n border: none;\n background: transparent;\n color: var(--rvs-fg);\n font: inherit;\n font-size: 12.5px;\n line-height: 19px;\n text-align: left;\n cursor: pointer;\n}\n\n.rvs-scm-item:hover {\n background: var(--rvs-hover);\n}\n\n.rvs-scm-item.selected {\n background: var(--rvs-active);\n}\n\n.rvs-scm-path {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.rvs-scm-dir {\n flex: none;\n color: var(--rvs-fg3);\n}\n\n.rvs-scm-actions {\n flex: none;\n display: none;\n align-items: center;\n gap: 1px;\n}\n\n.rvs-scm-item:hover .rvs-scm-actions {\n display: flex;\n}\n\n.rvs-commit-box {\n flex: none;\n padding: 8px 10px;\n border-bottom: 1px solid var(--rvs-border);\n}\n\n.rvs-commit-input {\n width: 100%;\n min-height: 58px;\n max-height: 180px;\n padding: 6px 8px;\n border: 1px solid var(--rvs-border2);\n border-radius: 8px;\n background: var(--rvs-input);\n color: var(--rvs-fg);\n font: inherit;\n font-size: 12.5px;\n line-height: 18px;\n resize: vertical;\n outline: none;\n}\n\n.rvs-commit-input:focus {\n border-color: var(--rvs-brand);\n}\n\n.rvs-commit-row {\n display: flex;\n gap: 6px;\n margin-top: 6px;\n}\n\n.rvs-commit-btn {\n flex: 1;\n padding: 5px 8px;\n border: none;\n border-radius: 6px;\n background: var(--rvs-accent);\n color: #fff;\n font: inherit;\n font-size: 12.5px;\n font-weight: 600;\n cursor: pointer;\n}\n\n.rvs-commit-btn:disabled {\n opacity: .45;\n cursor: default;\n}\n\n.rvs-repo-item {\n display: flex;\n align-items: center;\n gap: 6px;\n padding: 5px 10px 5px 12px;\n cursor: pointer;\n font-size: 12.5px;\n}\n\n.rvs-repo-item:hover {\n background: var(--rvs-hover);\n}\n\n.rvs-repo-item.active {\n background: var(--rvs-active);\n}\n\n.rvs-repo-name {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.rvs-repo-branch {\n flex: none;\n font-size: 11px;\n color: var(--rvs-fg3);\n}\n\n.rvs-pill {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n padding: 1px 8px;\n border-radius: 999px;\n background: var(--rvs-hover);\n font-size: 11px;\n font-weight: 600;\n color: var(--rvs-fg);\n}\n\n.rvs-subpanel {\n margin: 6px 8px;\n padding: 8px;\n border: 1px solid var(--rvs-border2);\n border-radius: 8px;\n font-size: 12px;\n}\n\n.rvs-subpanel-head {\n display: flex;\n align-items: center;\n gap: 6px;\n margin-bottom: 6px;\n color: var(--rvs-fg2);\n font-weight: 600;\n}\n\n.rvs-chip {\n flex: none;\n padding: 2px 8px;\n border: 1px solid var(--rvs-border2);\n border-radius: 6px;\n background: transparent;\n color: var(--rvs-fg);\n font: inherit;\n font-size: 11.5px;\n cursor: pointer;\n}\n\n.rvs-chip:hover {\n background: var(--rvs-hover);\n}\n\n.rvs-chip.primary {\n background: var(--rvs-accent);\n border-color: transparent;\n color: #fff;\n}\n\n.rvs-chip.danger:hover {\n background: var(--rvs-danger);\n border-color: transparent;\n color: #fff;\n}\n\n.rvs-input {\n flex: 1;\n min-width: 0;\n padding: 4px 8px;\n border: 1px solid var(--rvs-border2);\n border-radius: 6px;\n background: var(--rvs-input);\n color: var(--rvs-fg);\n font: inherit;\n font-size: 12px;\n outline: none;\n}\n\n.rvs-input:focus {\n border-color: var(--rvs-brand);\n}\n\n.rvs-select {\n flex: 1;\n min-width: 0;\n padding: 3px 6px;\n border: 1px solid var(--rvs-border2);\n border-radius: 6px;\n background: var(--rvs-input);\n color: var(--rvs-fg);\n font: inherit;\n font-size: 12px;\n outline: none;\n color-scheme: light dark;\n}\n\n.rvs-select option {\n background: Canvas;\n color: CanvasText;\n}\n\n/* ------------------------------------------------- conversation-view mount */\n\n/**\n * Bound the center surface to the height the host gave the slot.\n *\n * `flex: 1 1 0` is the load-bearing declaration, and BOTH halves of it matter.\n * The host's own slot rules are:\n *\n * .wSkVaW_viewArea { flex: 1; min-height: 0; overflow: hidden }\n * [data-phase=active] .viewArea { flex: 1 0 auto; min-height: auto }\n * .wSkVaW_root { height: 100%; position: relative }\n *\n * While a conversation is active the slot's flex-basis is `auto`, so the slot's\n * height is driven by its child's INTRINSIC size. A full-height editor then\n * pushed it to 5510px for a 300-line file, the outer scroller took over, and\n * the tab strip and file info bar scrolled off the top with the code.\n *\n * `flex-basis: 0` breaks that cycle: the item stops contributing its content\n * height, so the slot stays the size the host intended and `.cm-scroller`\n * becomes the only scroller. Measured in the fixture (.spike/layout-fixture.html,\n * which mirrors the slot rules above) — only the bottom row fits:\n *\n * flex: 1 1 0 + min-height: 0 -> 736px OK\n * flex: 1 1 auto (i.e. `flex: 1`) -> 5510px broken\n * height: 0 + min-height: 100% -> 5510px broken\n * max-height: 100% -> 5510px broken\n * height: 100vh -> 5510px broken\n *\n * `height: 100%` is deliberately NOT set here any more. It resolves to `auto`\n * against a slot whose height is not definite — it reads like the fix but\n * measures as a no-op, which is how this regressed once already.\n *\n * NOT `position: absolute` either — that was another failed attempt: `.viewArea`\n * is not itself positioned, so `inset: 0` escaped to the host's `.wSkVaW_root`\n * (which is `position: relative`) and the plugin's global bar floated on top of\n * the host's 对话 / 轨迹 / 远程编辑器 tab row.\n *\n * The sidebar's `.rvs-viewslot` is deliberately not covered: it has a bounding\n * parent and this bug is specific to the conversation slot's `min-height: auto`\n * phase.\n */\n.rvs-center {\n flex: 1 1 0;\n display: flex;\n flex-direction: column;\n min-height: 0;\n min-width: 0;\n overflow: hidden;\n}\n\n/* ---------------------------------------------------------------- editor */\n\n.rvs-etabs {\n flex: none;\n display: flex;\n align-items: stretch;\n gap: 2px;\n height: 32px;\n padding: 0 4px;\n overflow-x: auto;\n overflow-y: hidden;\n border-bottom: 1px solid var(--rvs-border);\n scrollbar-width: none;\n}\n\n.rvs-etabs::-webkit-scrollbar {\n height: 0;\n}\n\n.rvs-etab {\n flex: none;\n display: flex;\n align-items: center;\n gap: 5px;\n max-width: 190px;\n margin: 4px 0;\n padding: 0 4px 0 8px;\n border: none;\n border-radius: 6px;\n background: transparent;\n color: var(--rvs-fg3);\n font: inherit;\n font-size: 12px;\n cursor: pointer;\n}\n\n.rvs-etab:hover {\n background: var(--rvs-hover);\n color: var(--rvs-fg);\n}\n\n.rvs-etab.active {\n background: var(--rvs-layer3);\n color: var(--rvs-fg);\n}\n\n.rvs-etab-name {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.rvs-etab-dot {\n flex: none;\n font-size: 9px;\n color: var(--rvs-git-m);\n}\n\n.rvs-etab-close {\n flex: none;\n width: 16px;\n height: 16px;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 0;\n border: none;\n border-radius: 4px;\n background: transparent;\n color: inherit;\n cursor: pointer;\n opacity: .65;\n}\n\n.rvs-etab-close:hover {\n background: var(--rvs-hover);\n opacity: 1;\n}\n\n.rvs-einfo {\n flex: none;\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 2px 8px 2px 10px;\n border-bottom: 1px solid var(--rvs-border);\n font-size: 11px;\n line-height: 18px;\n color: var(--rvs-fg3);\n}\n\n.rvs-einfo-path {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n /* Truncate from the LEFT (keep the tail) without reordering the text:\n `plaintext` gives the content its own LTR base direction, which is what\n stops a leading \"/\" from rendering as a trailing one. */\n direction: rtl;\n unicode-bidi: plaintext;\n text-align: left;\n}\n\n.rvs-einfo-spacer {\n margin-left: auto;\n}\n\n.rvs-status.dirty { color: var(--rvs-git-m); }\n.rvs-status.saved { color: var(--rvs-git-a); }\n.rvs-status.saving { color: var(--rvs-fg2); }\n\n.rvs-body {\n flex: 1;\n min-height: 0;\n min-width: 0;\n display: flex;\n flex-direction: column;\n overflow: hidden;\n /* Anchors portalled overlays (the find widget) outside every scroller. */\n position: relative;\n background: var(--rvs-code-bg);\n color: var(--rvs-code-fg);\n}\n\n.rvs-editor-host {\n flex: 1;\n min-height: 0;\n overflow: hidden;\n background: var(--rvs-code-bg);\n}\n\n/* Keep a panel mounted but out of layout (and out of the a11y tree). Used by\n the editor host in preview mode: unmounting it destroyed the buffer and the\n undo stack, hiding it preserves both. */\n.rvs-hidden {\n display: none !important;\n}\n\n/* Find-match marks. Painted by our own StateField (`findMatchHighlight`), and\n deliberately the SAME colours as the `.cm-searchMatch` rules below so the\n two paths cannot drift apart. */\n.rvs-find-match {\n background-color: rgba(226, 183, 20, .30);\n outline: 1px solid rgba(226, 183, 20, .55);\n border-radius: 2px;\n}\n\n.rvs-find-match.current {\n background-color: rgba(226, 183, 20, .48);\n}\n\n/* ------------------------------------------------------- preview surfaces */\n\n.rvs-preview-img {\n flex: 1;\n min-height: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n overflow: auto;\n background-color: var(--rvs-code-bg);\n background-image:\n linear-gradient(45deg, rgba(127, 127, 127, .08) 25%, transparent 25%),\n linear-gradient(-45deg, rgba(127, 127, 127, .08) 25%, transparent 25%),\n linear-gradient(45deg, transparent 75%, rgba(127, 127, 127, .08) 75%),\n linear-gradient(-45deg, transparent 75%, rgba(127, 127, 127, .08) 75%);\n background-size: 20px 20px, 20px 20px, 20px 20px, 20px 20px;\n background-position: 0 0, 0 10px, 10px -10px, -10px 0;\n}\n\n.rvs-preview-img img {\n max-width: 100%;\n max-height: 100%;\n object-fit: contain;\n image-rendering: auto;\n}\n\n.rvs-markdown {\n flex: 1;\n min-height: 0;\n overflow: auto;\n padding: 12px 14px 24px;\n font-size: 13px;\n line-height: 1.7;\n color: var(--rvs-code-fg);\n background: var(--rvs-code-bg);\n user-select: text;\n}\n\n.rvs-markdown h1, .rvs-markdown h2, .rvs-markdown h3, .rvs-markdown h4 {\n margin: 14px 0 6px;\n line-height: 1.35;\n}\n\n.rvs-markdown h1 { font-size: 19px; border-bottom: 1px solid var(--rvs-border); padding-bottom: 5px; }\n.rvs-markdown h2 { font-size: 16px; border-bottom: 1px solid var(--rvs-border); padding-bottom: 4px; }\n.rvs-markdown h3 { font-size: 14px; }\n.rvs-markdown p { margin: 6px 0; }\n.rvs-markdown ul, .rvs-markdown ol { margin: 6px 0; padding-left: 22px; }\n.rvs-markdown li { margin: 2px 0; }\n.rvs-markdown code {\n padding: 1px 5px;\n border-radius: 4px;\n background: var(--rvs-hover);\n font-family: var(--rvs-mono);\n font-size: 12px;\n}\n.rvs-markdown pre {\n margin: 8px 0;\n padding: 9px 11px;\n border-radius: 8px;\n background: var(--rvs-input);\n overflow-x: auto;\n}\n.rvs-markdown pre code { padding: 0; background: transparent; }\n.rvs-markdown blockquote {\n margin: 8px 0;\n padding: 2px 12px;\n border-left: 3px solid var(--rvs-border2);\n color: var(--rvs-fg2);\n}\n.rvs-markdown a { color: var(--dsw-alias-label-link, #58a6ff); text-decoration: none; }\n.rvs-markdown a:hover { text-decoration: underline; }\n.rvs-markdown table { border-collapse: collapse; margin: 8px 0; font-size: 12px; }\n.rvs-markdown th, .rvs-markdown td { border: 1px solid var(--rvs-border2); padding: 4px 8px; }\n.rvs-markdown hr { border: none; border-top: 1px solid var(--rvs-border); margin: 12px 0; }\n\n.rvs-html-frame {\n flex: 1;\n min-height: 0;\n width: 100%;\n border: none;\n background: #fff;\n}\n\n/* ------------------------------------------------------------------ diff */\n\n.rvs-diff {\n flex: 1;\n min-height: 0;\n display: flex;\n flex-direction: column;\n overflow: hidden;\n}\n\n.rvs-diff-toolbar {\n flex: none;\n display: flex;\n align-items: center;\n gap: 6px;\n padding: 3px 8px;\n border-bottom: 1px solid var(--rvs-border);\n font-size: 11px;\n color: var(--rvs-fg3);\n}\n\n/**\n * Prev/next change, in the toolbar's free space.\n *\n * Two chevrons around a tabular counter: the pair then reads as \"jump to change\n * n of N\" instead of two anonymous arrows, and the counter doubles as the \"where\n * am I\" cue while scrolling by hand.\n */\n.rvs-hunknav {\n display: inline-flex;\n align-items: center;\n gap: 1px;\n margin-right: 2px;\n}\n\n.rvs-hunknav-pos {\n min-width: 30px;\n text-align: center;\n font-size: 11px;\n font-variant-numeric: tabular-nums;\n color: var(--rvs-fg3);\n user-select: none;\n}\n\n.rvs-seg {\n display: inline-flex;\n border: 1px solid var(--rvs-border2);\n border-radius: 6px;\n overflow: hidden;\n}\n\n.rvs-seg button {\n padding: 2px 9px;\n border: none;\n background: transparent;\n color: var(--rvs-fg2);\n font: inherit;\n font-size: 11px;\n cursor: pointer;\n}\n\n.rvs-seg button.active {\n background: var(--rvs-active);\n color: var(--rvs-fg);\n}\n\n.rvs-seg button:hover:not(.active) {\n background: var(--rvs-hover);\n}\n\n.rvs-diff-grid {\n flex: 1;\n min-height: 0;\n overflow-y: auto;\n overflow-x: hidden;\n font-family: var(--rvs-mono);\n font-size: 12px;\n line-height: 18px;\n scrollbar-width: thin;\n background: var(--rvs-code-bg);\n color: var(--rvs-code-fg);\n}\n\n/**\n * Top horizontal scrollbar.\n *\n * The two panes scroll horizontally on their own — so a long line can never\n * widen the layout — and this proxy bar drives both of them from the TOP of\n * the view, instead of forcing a scroll to the bottom of the file to reach a\n * horizontal bar. Its inner spacer is sized to the widest line, and the\n * component only renders it when a line really is wider than the grid.\n *\n * Sized and coloured to be SEEN: as a 13 px band with `scrollbar-width: thin`\n * and a faint thumb, it read as \"there is no horizontal scrollbar at all\".\n */\n.rvs-hbar {\n flex: none;\n height: 15px;\n overflow-x: auto;\n overflow-y: hidden;\n border-bottom: 1px solid var(--rvs-border);\n background: var(--rvs-hover);\n}\n\n.rvs-hbar-inner {\n height: 1px;\n}\n\n.rvs-hbar::-webkit-scrollbar {\n height: 11px;\n}\n\n.rvs-hbar::-webkit-scrollbar-thumb {\n background: var(--dsw-alias-scrollbar-bg-l2, rgba(150, 160, 180, .75));\n border-radius: 6px;\n border: 2px solid transparent;\n background-clip: padding-box;\n}\n\n.rvs-hbar::-webkit-scrollbar-thumb:hover {\n background: var(--dsw-alias-scrollbar-hover-l2, rgba(170, 180, 200, .95));\n background-clip: padding-box;\n}\n\n.rvs-hbar::-webkit-scrollbar-track {\n background: transparent;\n}\n\n.rvs-diff-cols {\n display: flex;\n align-items: stretch;\n min-width: 0;\n}\n\n/**\n * One diff pane. `flex: 1 1 0` keeps the panes EXACTLY equal however long a\n * line is (the previous row markup let a long line widen its own pane, which\n * is what broke the symmetry). Each pane keeps its own horizontal scroll, and\n * its scrollbar is now VISIBLE: the top proxy remains the convenient control,\n * but the pane's own bar is what a user reaches for first, and it sits right\n * under the content when the file is shorter than the view.\n */\n.rvs-diff-col {\n flex: 1 1 0;\n min-width: 0;\n overflow-x: auto;\n overflow-y: hidden;\n}\n\n.rvs-diff-col::-webkit-scrollbar {\n height: 9px;\n width: 0;\n}\n\n.rvs-diff-col::-webkit-scrollbar-thumb {\n background: var(--dsw-alias-scrollbar-bg-l2, rgba(150, 160, 180, .6));\n border-radius: 5px;\n}\n\n.rvs-diff-col::-webkit-scrollbar-thumb:hover {\n background: var(--dsw-alias-scrollbar-hover-l2, rgba(170, 180, 200, .85));\n}\n\n.rvs-diff-col::-webkit-scrollbar-track {\n background: transparent;\n}\n\n.rvs-dline {\n display: flex;\n align-items: flex-start;\n height: 18px;\n line-height: 18px;\n white-space: pre;\n}\n\n.rvs-dline-num {\n /* Pinned while the line scrolls sideways, so a long line never takes the\n line numbers away with it. */\n position: sticky;\n left: 0;\n z-index: 1;\n flex: none;\n width: 3.4em;\n padding-right: 6px;\n text-align: right;\n color: var(--rvs-fg3);\n opacity: .85;\n user-select: none;\n background: var(--rvs-code-bg);\n}\n\n.rvs-row-del .rvs-dline-num {\n background: var(--rvs-del-solid);\n}\n\n.rvs-row-add .rvs-dline-num {\n background: var(--rvs-add-solid);\n}\n\n.rvs-dline-text {\n flex: 1;\n padding-right: 10px;\n user-select: text;\n}\n\n.rvs-gutter-col {\n flex: none;\n width: 30px;\n border-left: 1px solid var(--rvs-border);\n border-right: 1px solid var(--rvs-border);\n user-select: none;\n}\n\n.rvs-gutter-line {\n height: 18px;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n.rvs-diff-grid::-webkit-scrollbar {\n width: 9px;\n height: 0;\n}\n\n.rvs-diff-grid::-webkit-scrollbar-thumb {\n background: var(--dsw-alias-scrollbar-bg-l2, rgba(127, 127, 127, .4));\n border-radius: 4px;\n}\n\n.rvs-num {\n flex: none;\n display: inline-block;\n width: 3.2em;\n padding-right: 6px;\n text-align: right;\n color: var(--rvs-fg3);\n opacity: .6;\n user-select: none;\n}\n\n.rvs-row-del {\n background: var(--rvs-del-bg);\n}\n\n.rvs-row-del .rvs-dline-text,\n.rvs-row-del .rvs-dline-num,\n.rvs-row-del .rvs-num {\n color: #f14c4c;\n}\n\n.rvs-row-add {\n background: var(--rvs-add-bg);\n}\n\n.rvs-row-add .rvs-dline-text,\n.rvs-row-add .rvs-dline-num,\n.rvs-row-add .rvs-num {\n color: #4ec9b0;\n}\n\n.rvs-row-empty {\n color: var(--rvs-fg3);\n opacity: .35;\n}\n\n.rvs-gutter {\n flex: none;\n width: 30px;\n display: flex;\n align-items: center;\n justify-content: center;\n border-left: 1px solid var(--rvs-border);\n border-right: 1px solid var(--rvs-border);\n user-select: none;\n}\n\n.rvs-gutter-dot {\n width: 6px;\n height: 6px;\n border-radius: 50%;\n opacity: .7;\n}\n\n.rvs-hunk-btns {\n display: flex;\n gap: 2px;\n}\n\n.rvs-hunk-btn {\n width: 13px;\n height: 15px;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 0;\n border: none;\n border-radius: 3px;\n background: transparent;\n color: var(--rvs-fg3);\n font-size: 11px;\n line-height: 1;\n cursor: pointer;\n}\n\n.rvs-hunk-btn:hover {\n background: var(--rvs-hover);\n color: var(--rvs-fg);\n}\n\n.rvs-hunk-btn.stage:hover {\n color: var(--rvs-git-a);\n}\n\n.rvs-hunk-btn.revert:hover {\n color: var(--rvs-git-d);\n}\n\n/**\n * Token colors: VS Code Dark+ / Light+, matching the CodeMirror theme in\n * `vsc/editor-theme.ts`. Keeping ONE palette for both surfaces means a file\n * does not change colors when the diff is switched to its editable side.\n */\n.rvs-t-comment { color: #6a9955; }\n.rvs-t-string { color: #ce9178; }\n.rvs-t-keyword { color: #569cd6; }\n.rvs-t-number { color: #b5cea8; }\n.rvs-t-func { color: #dcdcaa; }\n.rvs-t-type { color: #4ec9b0; }\n.rvs-t-attr { color: #9cdcfe; }\n.rvs-t-variable { color: #9cdcfe; }\n.rvs-t-operator { color: #d4d4d4; }\n.rvs-t-punct { color: #d4d4d4; }\n.rvs-t-tag { color: #569cd6; }\n.rvs-t-meta { color: #c586c0; }\n.rvs-t-insert { color: #4ec9b0; }\n.rvs-t-delete { color: #f14c4c; }\n\nbody:not([data-ds-dark-theme]) .rvs-t-comment { color: #008000; }\nbody:not([data-ds-dark-theme]) .rvs-t-string { color: #a31515; }\nbody:not([data-ds-dark-theme]) .rvs-t-keyword { color: #0000ff; }\nbody:not([data-ds-dark-theme]) .rvs-t-number { color: #098658; }\nbody:not([data-ds-dark-theme]) .rvs-t-func { color: #795e26; }\nbody:not([data-ds-dark-theme]) .rvs-t-type { color: #267f99; }\nbody:not([data-ds-dark-theme]) .rvs-t-attr,\nbody:not([data-ds-dark-theme]) .rvs-t-variable { color: #001080; }\nbody:not([data-ds-dark-theme]) .rvs-t-operator,\nbody:not([data-ds-dark-theme]) .rvs-t-punct { color: #1f1f1f; }\nbody:not([data-ds-dark-theme]) .rvs-t-meta { color: #af00db; }\n\n/**\n * CodeMirror token colors, applied through FIXED class names emitted by the\n * highlight style (`vsc/editor-palette.ts`). Fixed names mean the colors come\n * from this stylesheet — the same one that paints the rest of the UI — so a\n * code file can never go monochrome because a generated class name changed or\n * an injected style failed. Same Dark+ / Light+ palette as the diff panes\n * above, so a file keeps its colors when switching between the two views.\n */\n.rvs-ck-comment { color: #6a9955; }\n.rvs-ck-keyword { color: #569cd6; }\n.rvs-ck-control { color: #c586c0; }\n.rvs-ck-string { color: #ce9178; }\n.rvs-ck-regexp { color: #d16969; }\n.rvs-ck-number { color: #b5cea8; }\n.rvs-ck-constant { color: #569cd6; }\n.rvs-ck-func { color: #dcdcaa; }\n.rvs-ck-type { color: #4ec9b0; }\n.rvs-ck-variable { color: #9cdcfe; }\n.rvs-ck-attr { color: #9cdcfe; }\n.rvs-ck-meta { color: #c586c0; }\n.rvs-ck-punct { color: #d4d4d4; }\n.rvs-ck-op { color: #d4d4d4; }\n.rvs-ck-heading { color: #569cd6; font-weight: 700; }\n.rvs-ck-em { font-style: italic; }\n.rvs-ck-link { color: #9cdcfe; text-decoration: underline; }\n.rvs-ck-strike { text-decoration: line-through; }\n.rvs-ck-invalid { color: #f44747; }\n\nbody:not([data-ds-dark-theme]) .rvs-ck-comment { color: #008000; }\nbody:not([data-ds-dark-theme]) .rvs-ck-keyword { color: #0000ff; }\nbody:not([data-ds-dark-theme]) .rvs-ck-control { color: #af00db; }\nbody:not([data-ds-dark-theme]) .rvs-ck-string { color: #a31515; }\nbody:not([data-ds-dark-theme]) .rvs-ck-regexp { color: #811f3f; }\nbody:not([data-ds-dark-theme]) .rvs-ck-number { color: #098658; }\nbody:not([data-ds-dark-theme]) .rvs-ck-constant { color: #0070c1; }\nbody:not([data-ds-dark-theme]) .rvs-ck-func { color: #795e26; }\nbody:not([data-ds-dark-theme]) .rvs-ck-type { color: #267f99; }\nbody:not([data-ds-dark-theme]) .rvs-ck-variable { color: #001080; }\nbody:not([data-ds-dark-theme]) .rvs-ck-attr { color: #e50000; }\nbody:not([data-ds-dark-theme]) .rvs-ck-meta { color: #0000ff; }\nbody:not([data-ds-dark-theme]) .rvs-ck-punct,\nbody:not([data-ds-dark-theme]) .rvs-ck-op { color: #1f1f1f; }\nbody:not([data-ds-dark-theme]) .rvs-ck-heading { color: #0000ff; }\nbody:not([data-ds-dark-theme]) .rvs-ck-link { color: #001080; }\nbody:not([data-ds-dark-theme]) .rvs-ck-invalid { color: #cd3131; }\n\n/**\n * Opaque editor surface, also as plain CSS: the CodeMirror theme paints it too\n * (following the scheme through a Compartment), but a transparent editor is\n * the one failure a user notices immediately, so the container states it as\n * well.\n */\n.rvs-edge .cm-editor,\n.rvs-edge .cm-scroller,\n.rvs-edge .cm-gutters {\n background-color: var(--rvs-code-bg);\n}\n\n.rvs-edge .cm-editor {\n color: var(--rvs-code-fg);\n}\n\n.rvs-edge .cm-gutters {\n color: var(--rvs-code-gutter-fg, var(--rvs-fg3));\n border-right: 1px solid var(--rvs-border);\n}\n\n.rvs-edge .cm-activeLine {\n background-color: rgba(127, 127, 127, .08);\n}\n\n.rvs-edge .cm-activeLineGutter {\n background-color: rgba(127, 127, 127, .12);\n}\n\n/**\n * The code editor's own scrollbars.\n *\n * These had NO rule at all: CodeMirror's scroller is `.cm-scroller`, which is\n * none of `.rvs-scroll` / `.rvs-diff-grid` / `.rvs-hbar`, so the editor fell\n * back to the browser default — too thin to hit with a mouse.\n *\n * Both notations are needed and must stay in step: Chromium honours\n * `scrollbar-width` over `::-webkit-scrollbar` when both apply, so setting\n * only the webkit half would silently have no effect. Keep the numbers equal.\n */\n.rvs-edge .cm-scroller {\n scrollbar-width: auto;\n}\n\n.rvs-edge .cm-scroller::-webkit-scrollbar {\n width: 14px;\n height: 14px;\n}\n\n.rvs-edge .cm-scroller::-webkit-scrollbar-track {\n background: transparent;\n}\n\n.rvs-edge .cm-scroller::-webkit-scrollbar-thumb {\n /* Opaque, not a translucent token: a thin wash over code is exactly the\n low-contrast target that is hard to grab. */\n background: rgba(121, 121, 121, .8);\n border: 3px solid transparent;\n border-radius: 7px;\n background-clip: padding-box;\n min-height: 36px;\n}\n\n.rvs-edge .cm-scroller::-webkit-scrollbar-thumb:hover {\n background: rgba(150, 150, 150, .95);\n border: 3px solid transparent;\n background-clip: padding-box;\n}\n\n.rvs-edge .cm-scroller::-webkit-scrollbar-corner {\n background: transparent;\n}\n\n/* ------------------------------------------------------------ find widget */\n\n/**\n * \"You landed here\": the transient highlight on the line a jump landed on\n * (search hit, @-reference).\n *\n * Deliberately NOT a warm/amber wash: over the dark editor that muddies into a\n * brown-black band that reads as a shadow and swallows annotation colors. This\n * is the accent blue instead — a tinted band with a bright edge on the left and\n * hairlines top/bottom, so it reads as a marked region while every token keeps\n * its own colour and contrast.\n */\n.rvs-reveal-line {\n background-color: rgba(88, 166, 255, .22);\n box-shadow:\n inset 3px 0 0 0 #58a6ff,\n inset 0 1px 0 0 rgba(88, 166, 255, .38),\n inset 0 -1px 0 0 rgba(88, 166, 255, .38);\n}\n\nbody:not([data-ds-dark-theme]) .rvs-reveal-line {\n background-color: rgba(77, 107, 254, .14);\n box-shadow:\n inset 3px 0 0 0 #4d6bfe,\n inset 0 1px 0 0 rgba(77, 107, 254, .35),\n inset 0 -1px 0 0 rgba(77, 107, 254, .35);\n}\n\n/* --------------------------------------------------------- session changes */\n\n/**\n * 「本会话改了什么」:代码行背景 + 左侧改动栏(Cursor 风格)。\n *\n * 三种形态必须**一眼分得开**,光靠绿/红两个色相不够(色弱用户看新增行和删除行\n * 会糊成同一片),所以每种的左侧色条宽度/花样的语义也不同:\n * - 新增:实心绿条;\n * - 修改:实心蓝条(不是绿:修改块里既有留下的行也有新写的行,用\"中性的已变更\"\n * 色不会误导读者以为整块都是新内容);\n * - 删除:**斜纹**红底 + 更粗的红条 —— 这一行其实已经不存在了,只在中止位置上\n * 留个锚点,斜纹是这一层\"虚\"的信息里最不容易被当成真内容的花样。\n *\n * 选择器带上 `.cm-line`:`.cm-activeLine` 由 CodeMirror 的主题注入(运行时\n * `<style>`,通常排在本文档之后),只写 `.rvs-change-*` 两者同权重时会输给它,\n * 光标所在的那一行改动就会消失。\n */\n.rvs-edge .cm-line.rvs-change-add {\n background-color: rgba(46, 160, 67, .17);\n box-shadow:\n inset 3px 0 0 0 #2ea043,\n inset 0 -1px 0 0 rgba(46, 160, 67, .28);\n}\n\n.rvs-edge .cm-line.rvs-change-mod {\n background-color: rgba(56, 139, 253, .15);\n box-shadow:\n inset 3px 0 0 0 #388bfd,\n inset 0 -1px 0 0 rgba(56, 139, 253, .28);\n}\n\n.rvs-edge .cm-line.rvs-change-del {\n background-color: rgba(248, 81, 73, .09);\n background-image: repeating-linear-gradient(135deg, rgba(248, 81, 73, .17) 0 6px, transparent 6px 12px);\n box-shadow: inset 4px 0 0 0 #f85149;\n}\n\n/* 改动栏本身:符号 + 一列与行背景同色的窄条,读起来才是一条\"改动边\"。 */\n.rvs-edge .cm-gutterElement.rvs-change-gutter-add {\n box-shadow: inset 2px 0 0 0 #2ea043;\n}\n\n.rvs-edge .cm-gutterElement.rvs-change-gutter-mod {\n box-shadow: inset 2px 0 0 0 #388bfd;\n}\n\n.rvs-edge .cm-gutterElement.rvs-change-gutter-del {\n box-shadow: inset 2px 0 0 0 #f85149;\n}\n\n.rvs-edge .rvs-change-mark {\n display: inline-block;\n width: 12px;\n font-family: var(--rvs-mono);\n font-size: 12px;\n line-height: 1;\n text-align: center;\n user-select: none;\n}\n\n.rvs-edge .rvs-change-mark-add { color: #2ea043; }\n.rvs-edge .rvs-change-mark-mod { color: #388bfd; }\n.rvs-edge .rvs-change-mark-del { color: #f85149; }\n\n/**\n * 行内(字符级)差异:上面那三种底色回答\"哪几行变了\",这两条回答\"那一行的哪几个\n * 字变了\"。只改一个词的编辑,整行同色时完全看不出改了什么。\n *\n * `rvs-change-word-add` 刻意**只给背景、不给 `color`**:代码有自己的语法着色,\n * 覆盖前景色会把关键字/字符串/注释染成同一种颜色,比不标还难读。透明度压在行\n * 底色之上一点点,叠起来仍读得出\"整块改动\"的边界,又能看出块内哪几个字是新的。\n */\n.rvs-edge .rvs-change-word-add {\n background-color: rgba(46, 160, 67, .22);\n border-radius: 2px;\n}\n\n/**\n * 被删掉、在新文件里已经不存在的内容:零宽 widget,红底 + 删除线。\n *\n * 删除线画在 chip 自己身上(chip 是独立元素,不会波及周围代码);`white-space:\n * pre` 保留被删文本自己的缩进 —— 截断/去空白都会让 chip 谎报\"删了什么\"。\n * 字号略小 + 降一点不透明度,是让它读起来像\"残留物\"而不是一行真代码。\n */\n.rvs-edge .rvs-change-word-del {\n padding: 0 3px;\n margin: 0 1px;\n border-radius: 3px;\n background-color: rgba(248, 81, 73, .18);\n font-size: .92em;\n text-decoration: line-through;\n opacity: .85;\n white-space: pre;\n}\n\n/**\n * 撑宽度的隐形符号:只用它占位,绝不显示(见 `change-marks.ts` 的 SpacerMarker,\n * CodeMirror 会给这个 spacer 元素加 `visibility: hidden`,这里不重复声明)。\n */\n.rvs-edge .rvs-change-spacer {\n opacity: 0;\n}\n\n/* 浅色主题:同样的色相,压暗 + 提高不透明度,在近白底上才看得清。 */\nbody:not([data-ds-dark-theme]) .rvs-edge .cm-line.rvs-change-add {\n background-color: rgba(31, 136, 61, .13);\n box-shadow:\n inset 3px 0 0 0 #1f883d,\n inset 0 -1px 0 0 rgba(31, 136, 61, .22);\n}\n\nbody:not([data-ds-dark-theme]) .rvs-edge .cm-line.rvs-change-mod {\n background-color: rgba(9, 105, 218, .12);\n box-shadow:\n inset 3px 0 0 0 #0969da,\n inset 0 -1px 0 0 rgba(9, 105, 218, .22);\n}\n\nbody:not([data-ds-dark-theme]) .rvs-edge .cm-line.rvs-change-del {\n background-color: rgba(207, 34, 46, .07);\n background-image: repeating-linear-gradient(135deg, rgba(207, 34, 46, .14) 0 6px, transparent 6px 12px);\n box-shadow: inset 4px 0 0 0 #cf222e;\n}\n\nbody:not([data-ds-dark-theme]) .rvs-edge .cm-gutterElement.rvs-change-gutter-add {\n box-shadow: inset 2px 0 0 0 #1f883d;\n}\n\nbody:not([data-ds-dark-theme]) .rvs-edge .cm-gutterElement.rvs-change-gutter-mod {\n box-shadow: inset 2px 0 0 0 #0969da;\n}\n\nbody:not([data-ds-dark-theme]) .rvs-edge .cm-gutterElement.rvs-change-gutter-del {\n box-shadow: inset 2px 0 0 0 #cf222e;\n}\n\nbody:not([data-ds-dark-theme]) .rvs-edge .rvs-change-mark-add { color: #1f883d; }\nbody:not([data-ds-dark-theme]) .rvs-edge .rvs-change-mark-mod { color: #0969da; }\nbody:not([data-ds-dark-theme]) .rvs-edge .rvs-change-mark-del { color: #cf222e; }\n\n/* 浅色主题的行内差异:同样的色相压暗,在近白底上才看得清(暗色那套会糊成一片)。 */\nbody:not([data-ds-dark-theme]) .rvs-edge .rvs-change-word-add {\n background-color: rgba(26, 127, 55, .18);\n}\n\nbody:not([data-ds-dark-theme]) .rvs-edge .rvs-change-word-del {\n background-color: rgba(207, 34, 46, .14);\n}\n\n/**\n * 改动跳转按钮:浮在编辑器右上角。\n *\n * 放在编辑器上而不是面板里 —— 文件打开后光标就在这里,而面板可能被收起、被滚走。\n * 容器 `.rvs-body` 已经是 `position: relative`(它本来用于挂 find 浮层),所以这里\n * 只做绝对定位,不引入新的定位上下文。`pointer-events` 只开在按钮上:这块区域压在\n * 编辑器文字上方,整块可点会把右上角那几行变成死区。\n */\n.rvs-change-nav {\n position: absolute;\n top: 8px;\n right: 14px;\n z-index: 5;\n display: flex;\n align-items: center;\n gap: 2px;\n padding: 3px 4px;\n border: 1px solid var(--rvs-border2);\n border-radius: 8px;\n background: Canvas;\n box-shadow: 0 2px 8px rgba(0, 0, 0, .28);\n pointer-events: none;\n}\n\n/* The counter is what makes this read as \"3 of 7 changes\" rather than two\n anonymous arrows — and it keeps the buttons from looking like decoration. */\n.rvs-change-nav-count {\n padding: 0 6px 0 4px;\n color: var(--rvs-fg2);\n font-size: 11px;\n font-variant-numeric: tabular-nums;\n line-height: 22px;\n white-space: nowrap;\n}\n\n.rvs-change-nav-btn {\n width: 26px;\n height: 22px;\n padding: 0;\n border: 1px solid transparent;\n border-radius: 5px;\n background: transparent;\n color: var(--rvs-fg);\n font-size: 13px;\n font-weight: 600;\n line-height: 20px;\n text-align: center;\n cursor: pointer;\n pointer-events: auto;\n}\n\n.rvs-change-nav-btn:hover {\n border-color: var(--rvs-border2);\n background: var(--rvs-hover);\n color: var(--rvs-brand);\n}\n\n.rvs-change-nav-btn:active { background: var(--rvs-active); }\n\n/**\n * 悬浮在改动行上出现的「撤销这处改动」按钮。\n *\n * 它**不在编辑器的 DOM 里**:`undo-affordance.ts` 把它当 `position: fixed` 的元素挂到\n * `document.body`(和「添加到对话」那个浮层同一套做法),所以这里的选择器不带\n * `.rvs-edge` —— 带上了就一条都不生效。固定定位 + 高 z-index 是刻意的:宿主容器有\n * `overflow: hidden`,挂在里面会被 CodeMirror 的滚动层裁掉。\n *\n * 颜色按\"危险动作\"给:默认红边红字、悬停实心红。底色不透明(不用半透明的 overlay\n * token),否则后面的代码文字会透出来。\n */\n.rvs-undo-affordance {\n position: fixed;\n z-index: 2100;\n display: inline-flex;\n align-items: center;\n gap: 4px;\n max-width: 220px;\n padding: 3px 9px;\n border: 1px solid #f85149;\n border-radius: 6px;\n background-color: #1e2229;\n color: #ffb4ae;\n /* 按钮挂在 body 上,没有 .rvs-edge 的字体上下文:跟随宿主页面字体。 */\n font-family: inherit;\n font-size: 12px;\n line-height: 1.5;\n white-space: nowrap;\n cursor: pointer;\n box-shadow: 0 6px 18px rgba(0, 0, 0, .45);\n /* 纵向以悬停行的中线对齐(横向坐标由 JS 按行尾算好)。 */\n transform: translateY(-50%);\n}\n\n.rvs-undo-affordance:hover {\n background-color: #f85149;\n border-color: #f85149;\n color: #fff;\n}\n\n.rvs-undo-affordance:active {\n transform: translateY(-50%) scale(.97);\n}\n\n.rvs-undo-affordance:focus-visible {\n outline: 2px solid #58a6ff;\n outline-offset: 1px;\n}\n\n/* 删除锚点:这一行其实已经不在了,虚线边框把它和\"真实存在的行\"分开。 */\n.rvs-undo-affordance[data-kind=\"del\"] {\n border-style: dashed;\n}\n\nbody:not([data-ds-dark-theme]) .rvs-undo-affordance {\n border-color: #cf222e;\n background-color: #ffffff;\n color: #a40e26;\n box-shadow: 0 6px 18px rgba(31, 35, 40, .18);\n}\n\nbody:not([data-ds-dark-theme]) .rvs-undo-affordance:hover {\n background-color: #cf222e;\n border-color: #cf222e;\n color: #fff;\n}\n\nbody:not([data-ds-dark-theme]) .rvs-undo-affordance:focus-visible {\n outline-color: #4d6bfe;\n}\n\n/**\n * CodeMirror's own search decorations, also stated here: the editor's theme is\n * injected by the library, and these must stay readable even when only this\n * stylesheet is in play.\n */\n.rvs-edge .cm-searchMatch {\n background-color: rgba(226, 183, 20, .30);\n outline: 1px solid rgba(226, 183, 20, .55);\n}\n\n.rvs-edge .cm-searchMatch-selected {\n background-color: rgba(226, 183, 20, .48);\n}\n\n.rvs-edge .cm-selectionBackground,\n.rvs-edge .cm-content ::selection {\n background-color: rgba(58, 110, 165, .55) !important;\n}\n\n/** Build stamp in the editor info bar: identifies the loaded client bundle. */\n.rvs-build {\n flex: none;\n padding: 0 5px;\n border: 1px solid var(--rvs-border);\n border-radius: 4px;\n color: var(--rvs-fg3);\n font-size: 10px;\n font-family: var(--rvs-mono);\n opacity: .85;\n user-select: text;\n}\n\n.rvs-find {\n position: absolute;\n top: 6px;\n right: 18px;\n z-index: 30;\n display: flex;\n align-items: center;\n gap: 3px;\n padding: 3px 4px 3px 7px;\n border: 1px solid var(--rvs-border2);\n border-radius: 8px;\n background-color: Canvas;\n background-image: linear-gradient(var(--dsw-alias-bg-overlay, transparent), var(--dsw-alias-bg-overlay, transparent));\n color: var(--rvs-fg);\n box-shadow: 0 4px 14px rgba(0, 0, 0, .28);\n font-size: 12px;\n}\n\n.rvs-find-input {\n width: 170px;\n padding: 2px 6px;\n border: 1px solid var(--rvs-border2);\n border-radius: 5px;\n background: var(--rvs-input);\n color: var(--rvs-fg);\n font: inherit;\n font-size: 12px;\n outline: none;\n}\n\n.rvs-find-input:focus {\n border-color: var(--rvs-brand);\n}\n\n.rvs-find-count {\n flex: none;\n min-width: 54px;\n text-align: center;\n color: var(--rvs-fg3);\n font-size: 11px;\n white-space: nowrap;\n}\n\n.rvs-find-count.none {\n color: var(--rvs-git-d);\n}\n\n.rvs-code {\n flex: 1;\n min-height: 0;\n overflow: auto;\n margin: 0;\n padding: 6px 0 16px;\n font-family: var(--rvs-mono);\n font-size: 12.5px;\n line-height: 18px;\n white-space: pre;\n scrollbar-width: thin;\n background: var(--rvs-code-bg);\n color: var(--rvs-code-fg);\n}\n\n.rvs-code-line {\n display: flex;\n align-items: flex-start;\n min-width: max-content;\n}\n\n.rvs-code-line:hover {\n background: var(--rvs-hover);\n}\n\n.rvs-code-text {\n flex: 1;\n padding-right: 10px;\n white-space: pre-wrap;\n word-break: break-word;\n user-select: text;\n}\n\n/* ---------------------------------------------------------- history list */\n\n.rvs-commit-item {\n display: flex;\n align-items: center;\n gap: 6px;\n width: 100%;\n padding: 4px 8px 4px 4px;\n border: none;\n background: transparent;\n color: var(--rvs-fg);\n font: inherit;\n font-size: 12.5px;\n line-height: 18px;\n text-align: left;\n cursor: pointer;\n}\n\n.rvs-commit-item:hover {\n background: var(--rvs-hover);\n}\n\n.rvs-commit-item.selected {\n background: var(--rvs-active);\n}\n\n.rvs-graph-svg {\n flex: none;\n display: block;\n}\n\n.rvs-commit-hash {\n flex: none;\n font-family: var(--rvs-mono);\n font-size: 11px;\n color: var(--rvs-fg3);\n}\n\n.rvs-commit-msg {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.rvs-commit-date {\n flex: none;\n font-size: 11px;\n color: var(--rvs-fg3);\n}\n\n.rvs-ref-badge {\n flex: none;\n padding: 0 5px;\n border-radius: 8px;\n border: 1px solid var(--rvs-border2);\n font-size: 10px;\n line-height: 15px;\n color: var(--rvs-fg2);\n}\n\n.rvs-ref-badge.head {\n border-color: var(--rvs-brand);\n background: rgba(77, 107, 254, .2);\n color: var(--rvs-fg);\n}\n\n.rvs-ref-badge.tag {\n border-color: rgba(226, 183, 20, .55);\n background: rgba(226, 183, 20, .16);\n color: var(--rvs-git-m);\n}\n\n.rvs-detail {\n margin: 0 6px 8px;\n padding: 8px 10px;\n border: 1px solid var(--rvs-border2);\n border-left: 3px solid var(--rvs-accent);\n border-radius: 8px;\n background: var(--rvs-layer3);\n font-size: 12px;\n}\n\n.rvs-detail-meta {\n color: var(--rvs-fg3);\n font-size: 11px;\n margin-bottom: 6px;\n white-space: pre-wrap;\n}\n\n.rvs-file-row {\n display: flex;\n align-items: center;\n gap: 6px;\n padding: 2px 4px;\n border-radius: 5px;\n cursor: pointer;\n}\n\n.rvs-file-row:hover {\n background: var(--rvs-hover);\n}\n\n.rvs-file-path {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n /* Same tail-first truncation as the info bar; `plaintext` keeps the path's\n own LTR order (see .rvs-einfo-path). */\n direction: rtl;\n unicode-bidi: plaintext;\n text-align: left;\n}\n\n.rvs-file-status {\n flex: none;\n width: 16px;\n text-align: center;\n font-weight: 700;\n font-size: 11px;\n}\n\n/* ---------------------------------------------------- floating editor window */\n\n/**\n * The floating editor lives in the host's frame-wide overlay layer\n * (`position: absolute; inset: 0; z-index: 20`, click-through, children opt\n * back into pointer events). It is placed with viewport coordinates, so it\n * floats above every column without joining any of their stacking contexts.\n *\n * `.rvs-root` carries the whole design-system token set and is row-flex by\n * default; this frame overrides the direction to a column and takes its size\n * from the inline rect the component keeps.\n */\n.rvs-float-window {\n position: fixed;\n flex-direction: column;\n /* The rect maths (clamp/persist) is in border-box terms; without this the\n painted border would sit one pixel outside the computed rectangle. */\n box-sizing: border-box;\n min-width: 360px;\n min-height: 220px;\n overflow: hidden;\n border: 1px solid var(--rvs-border2);\n border-radius: 10px;\n background: var(--rvs-code-bg);\n color: var(--rvs-code-fg);\n box-shadow: 0 18px 60px rgba(0, 0, 0, .45), 0 2px 8px rgba(0, 0, 0, .28);\n z-index: 30;\n /* The overlay layer itself is click-through; the window opts back in even if\n the host wraps each overlay entry in its own element. */\n pointer-events: auto;\n}\n\n/* Minimized: the frame IS the ball (see `.rvs-float-mini`); the body stays\n mounted but hidden, and it must not impose a minimum height. */\n.rvs-float-window[data-rvs-float=\"collapsed\"] {\n min-height: 0;\n}\n\n/**\n * Minimized window = floating ball.\n *\n * A full-width 34px bar was too long: it read as a broken window and covered a\n * whole strip. The ball is a small circle at the window's own corner, draggable\n * anywhere, restored by a click (or Enter/Space). It keeps the dirty dot, so\n * \"the editor is out and has unsaved edits\" is still one glance away.\n */\n.rvs-float-window.rvs-float-mini {\n min-width: 0;\n min-height: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n border-radius: 50%;\n cursor: move;\n touch-action: none;\n}\n\n.rvs-float-ball {\n flex: 1;\n /* Stretch to the circle: the frame is a COLUMN flex box with centred items,\n so without this the hit area (and the absolutely positioned dot) would be\n sized by the 16px glyph instead of the 44px ball. */\n width: 100%;\n height: 100%;\n box-sizing: border-box;\n display: flex;\n align-items: center;\n justify-content: center;\n position: relative;\n border-radius: 50%;\n color: var(--rvs-brand);\n font-size: 16px;\n line-height: 1;\n cursor: move;\n outline: none;\n}\n\n.rvs-float-ball:hover,\n.rvs-float-ball:focus-visible {\n background: var(--rvs-hover);\n}\n\n.rvs-float-ball-glyph {\n pointer-events: none;\n}\n\n/* Unsaved edits stay visible while minimized. */\n.rvs-float-ball-dot {\n position: absolute;\n top: 8px;\n right: 8px;\n width: 8px;\n height: 8px;\n border-radius: 50%;\n background: var(--rvs-git-m);\n box-shadow: 0 0 0 2px var(--rvs-code-bg);\n pointer-events: none;\n}\n\n/* While a drag/resize gesture runs the pointer must not select editor text. */\n.rvs-float-window.dragging {\n user-select: none;\n}\n\n.rvs-float-titlebar {\n flex: none;\n display: flex;\n align-items: center;\n gap: 7px;\n height: 34px;\n padding: 0 6px 0 10px;\n border-bottom: 1px solid var(--rvs-border);\n background: var(--rvs-layer3);\n cursor: move;\n user-select: none;\n touch-action: none;\n}\n\n.rvs-float-glyph {\n color: var(--rvs-brand);\n font-size: 11px;\n}\n\n.rvs-float-title {\n flex: none;\n font-size: 12px;\n font-weight: 600;\n color: var(--rvs-fg2);\n}\n\n.rvs-float-file {\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n font-size: 11.5px;\n color: var(--rvs-fg3);\n direction: rtl;\n unicode-bidi: plaintext;\n text-align: left;\n}\n\n.rvs-float-spacer {\n flex: 1;\n}\n\n.rvs-float-btn {\n flex: none;\n width: 22px;\n height: 22px;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 0;\n border: none;\n border-radius: 5px;\n background: transparent;\n color: var(--rvs-fg3);\n font: inherit;\n font-size: 12px;\n line-height: 1;\n cursor: pointer;\n}\n\n.rvs-float-btn:hover {\n background: var(--rvs-hover);\n color: var(--rvs-fg);\n}\n\n.rvs-float-close:hover {\n background: rgba(241, 76, 76, .18);\n color: var(--rvs-danger);\n}\n\n.rvs-float-body {\n flex: 1 1 0;\n min-height: 0;\n min-width: 0;\n display: flex;\n overflow: hidden;\n background: var(--rvs-code-bg);\n}\n\n/* Keep the height chain pinned: the editor pane grows inside this box. */\n.rvs-float-body > .rvs-pane {\n flex: 1 1 0;\n min-height: 0;\n min-width: 0;\n}\n\n/* Eight grips, placed INSIDE the border box: the window clips overflow (rounded\n corners), so grips hanging outside would lose half their hit area. Corners\n are larger so they can actually be hit. */\n.rvs-float-resize {\n position: absolute;\n z-index: 3;\n}\n\n.rvs-float-resize-n { top: 0; left: 10px; right: 10px; height: 6px; cursor: ns-resize; }\n.rvs-float-resize-s { bottom: 0; left: 10px; right: 10px; height: 6px; cursor: ns-resize; }\n.rvs-float-resize-e { top: 10px; bottom: 10px; right: 0; width: 6px; cursor: ew-resize; }\n.rvs-float-resize-w { top: 10px; bottom: 10px; left: 0; width: 6px; cursor: ew-resize; }\n.rvs-float-resize-ne { top: 0; right: 0; width: 12px; height: 12px; cursor: nesw-resize; }\n.rvs-float-resize-nw { top: 0; left: 0; width: 12px; height: 12px; cursor: nwse-resize; }\n.rvs-float-resize-se { bottom: 0; right: 0; width: 12px; height: 12px; cursor: nwse-resize; }\n.rvs-float-resize-sw { bottom: 0; left: 0; width: 12px; height: 12px; cursor: nesw-resize; }\n\n/* Shown by whichever surface the single editor left behind (centre tab or the\n sidebar's editor panel). */\n.rvs-float-placeholder {\n flex: 1;\n min-height: 0;\n height: 100%;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 10px;\n padding: 24px;\n text-align: center;\n color: var(--rvs-fg3);\n font-size: 12.5px;\n}\n\n.rvs-float-placeholder-title {\n color: var(--rvs-fg2);\n font-size: 13px;\n font-weight: 600;\n}\n\n.rvs-float-placeholder-text {\n max-width: 360px;\n line-height: 1.6;\n}\n\n/* --------------------------------------------------------- context menu */\n\n.rvs-portal {\n font-family: inherit;\n}\n\n/**\n * Floating surfaces (menu / modal / toast) paint an OPAQUE base first and then\n * lay the skin's overlay token on top. The token is frequently translucent, so\n * using it alone let the panel's own text bleed through the menu and overlap\n * the entries; `Canvas` follows the resolved light/dark scheme, so the base is\n * correct in either theme.\n */\n.rvs-menu,\n.rvs-modal-box,\n.rvs-toast {\n background-color: Canvas;\n background-image: linear-gradient(var(--dsw-alias-bg-overlay, transparent), var(--dsw-alias-bg-overlay, transparent));\n color: var(--rvs-fg);\n}\n\n.rvs-menu {\n position: fixed;\n z-index: 1000;\n min-width: 172px;\n max-height: calc(100vh - 16px);\n overflow-y: auto;\n padding: 4px;\n border: 1px solid var(--rvs-border2);\n border-radius: 8px;\n box-shadow: 0 6px 20px rgba(0, 0, 0, .35);\n font-size: 12.5px;\n line-height: 18px;\n}\n\n.rvs-menu-sep {\n height: 1px;\n margin: 4px 6px;\n background: var(--rvs-border);\n}\n\n.rvs-menu-item {\n display: flex;\n align-items: center;\n gap: 8px;\n width: 100%;\n padding: 5px 10px;\n border: none;\n border-radius: 6px;\n background: transparent;\n color: inherit;\n font: inherit;\n text-align: left;\n cursor: pointer;\n}\n\n.rvs-menu-item:hover {\n background: var(--rvs-hover);\n}\n\n.rvs-menu-item.danger {\n color: var(--vsc-danger, #f14c4c);\n}\n\n.rvs-menu-item.danger:hover {\n background: rgba(241, 76, 76, .16);\n}\n\n.rvs-menu-item:disabled {\n opacity: .45;\n cursor: default;\n}\n\n.rvs-menu-item:disabled:hover {\n background: transparent;\n}\n\n.rvs-menu-hint {\n margin-left: auto;\n font-size: 11px;\n opacity: .6;\n}\n\n/* ------------------------------------------------------- modal + toast */\n\n.rvs-modal-mask {\n position: fixed;\n inset: 0;\n z-index: 100000;\n display: flex;\n align-items: center;\n justify-content: center;\n background: rgba(0, 0, 0, .45);\n backdrop-filter: blur(2px);\n animation: rvsFade .12s ease;\n}\n\n.rvs-modal-box {\n width: min(420px, calc(100vw - 40px));\n padding: 16px 18px;\n border: 1px solid var(--rvs-border2);\n border-radius: 10px;\n color: var(--rvs-fg);\n box-shadow: 0 12px 40px rgba(0, 0, 0, .38);\n font-size: 13px;\n animation: rvsPop .14s ease;\n}\n\n.rvs-modal-title {\n margin-bottom: 8px;\n font-size: 14px;\n font-weight: 600;\n}\n\n.rvs-modal-text {\n margin-bottom: 12px;\n color: var(--rvs-fg2);\n font-size: 12.5px;\n line-height: 1.6;\n word-break: break-word;\n white-space: pre-wrap;\n}\n\n.rvs-modal-row {\n display: flex;\n justify-content: flex-end;\n gap: 8px;\n margin-top: 14px;\n}\n\n.rvs-modal-btn {\n padding: 5px 14px;\n border: 1px solid var(--rvs-border2);\n border-radius: 7px;\n background: transparent;\n color: var(--rvs-fg);\n font: inherit;\n font-size: 12.5px;\n cursor: pointer;\n}\n\n.rvs-modal-btn:hover {\n background: var(--rvs-hover);\n}\n\n.rvs-modal-btn.primary {\n border-color: transparent;\n background: var(--rvs-accent);\n color: #fff;\n}\n\n.rvs-modal-btn.danger {\n border-color: transparent;\n background: var(--rvs-danger);\n color: #fff;\n}\n\n.rvs-toasts {\n position: fixed;\n right: 16px;\n bottom: 18px;\n z-index: 100001;\n display: flex;\n flex-direction: column;\n gap: 8px;\n align-items: flex-end;\n pointer-events: none;\n}\n\n.rvs-toast {\n max-width: 340px;\n padding: 7px 12px;\n border: 1px solid var(--rvs-border2);\n border-radius: 8px;\n color: var(--rvs-fg);\n box-shadow: 0 6px 20px rgba(0, 0, 0, .35);\n font-size: 12.5px;\n line-height: 1.5;\n word-break: break-word;\n white-space: pre-wrap;\n animation: rvsPop .14s ease;\n}\n\n.rvs-toast.error {\n border-color: rgba(241, 76, 76, .6);\n background-image: linear-gradient(rgba(58, 31, 34, .92), rgba(58, 31, 34, .92));\n}\n\n/* ------------------------------------------------------------- terminal */\n\n.rvs-term-tabs {\n flex: none;\n display: flex;\n align-items: stretch;\n gap: 2px;\n height: 32px;\n padding: 0 4px;\n overflow-x: auto;\n overflow-y: hidden;\n border-bottom: 1px solid var(--rvs-border);\n scrollbar-width: none;\n}\n\n.rvs-term-tabs::-webkit-scrollbar {\n height: 0;\n}\n\n.rvs-term-tab {\n flex: none;\n display: flex;\n align-items: center;\n gap: 6px;\n max-width: 180px;\n margin: 4px 0;\n padding: 0 6px 0 9px;\n border: none;\n border-radius: 6px;\n background: transparent;\n color: var(--rvs-fg3);\n font: inherit;\n font-size: 12px;\n cursor: pointer;\n}\n\n/* Wrapper that pairs a tab with its close button. The two are siblings so the\n ✕ can be a real focusable <button> (a nested one is invalid HTML and left\n keyboard users no way to close a tab). */\n.rvs-term-tabwrap {\n position: relative;\n flex: none;\n display: flex;\n align-items: center;\n}\n\n.rvs-term-tabwrap .rvs-term-tab {\n padding-right: 22px;\n}\n\n.rvs-term-tabwrap .rvs-etab-close {\n position: absolute;\n right: 3px;\n opacity: 0;\n}\n\n.rvs-term-tabwrap:hover .rvs-etab-close,\n.rvs-term-tabwrap:focus-within .rvs-etab-close {\n opacity: .8;\n}\n\n.rvs-term-tab:hover {\n background: var(--rvs-hover);\n color: var(--rvs-fg);\n}\n\n.rvs-term-tab.active {\n background: var(--rvs-layer3);\n color: var(--rvs-fg);\n}\n\n.rvs-term-tab.dead {\n color: var(--rvs-git-d);\n}\n\n.rvs-term-name {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.rvs-term-body {\n flex: 1;\n min-height: 0;\n position: relative;\n overflow: hidden;\n}\n\n.rvs-term-slot {\n position: absolute;\n inset: 0;\n}\n\n.rvs-term-dead {\n height: 100%;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 8px;\n background: var(--rvs-code-bg);\n color: var(--rvs-code-fg);\n}\n\n/* `.rvs-toast.error` is styled with the overlay form further up, so the popup\n opacity convention (an opaque Canvas base plus a token layer) still governs\n it. A flat `background: #3a1f22` shorthand sat here and overrode that chain —\n same opaque result, but outside the convention. */\n\n@keyframes rvsFade {\n from { opacity: 0; }\n to { opacity: 1; }\n}\n\n@keyframes rvsPop {\n from { opacity: 0; transform: scale(.97) translateY(3px); }\n to { opacity: 1; transform: none; }\n}\n\n.rvs-search-row {\n display: flex;\n gap: 6px;\n padding: 6px 8px;\n border-bottom: 1px solid var(--rvs-border);\n}\n\n/* --------------------------------------------------------- search options */\n\n.rvs-toggles {\n flex: none;\n display: inline-flex;\n border: 1px solid var(--rvs-border2);\n border-radius: 6px;\n overflow: hidden;\n}\n\n.rvs-toggle {\n min-width: 26px;\n height: 22px;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 0 5px;\n border: none;\n border-right: 1px solid var(--rvs-border);\n background: transparent;\n color: var(--rvs-fg3);\n font: inherit;\n cursor: pointer;\n}\n\n.rvs-toggle:last-child {\n border-right: none;\n}\n\n.rvs-toggle:hover {\n background: var(--rvs-hover);\n color: var(--rvs-fg);\n}\n\n.rvs-toggle.active {\n background: var(--rvs-active);\n color: var(--rvs-fg);\n box-shadow: inset 0 0 0 1px var(--rvs-brand);\n}\n\n.rvs-glyph {\n font-family: var(--rvs-mono);\n font-weight: 600;\n line-height: 1;\n letter-spacing: -0.02em;\n}\n\n.rvs-glyph-underline {\n text-decoration: underline;\n}\n\n.rvs-scope-label {\n flex: none;\n align-self: center;\n font-size: 11px;\n color: var(--rvs-fg3);\n white-space: nowrap;\n}\n\n/* --------------------------------------------------------- search results */\n\n.rvs-hit {\n display: flex;\n align-items: center;\n gap: 6px;\n width: 100%;\n padding: 3px 10px;\n border: none;\n background: transparent;\n color: var(--rvs-fg);\n font: inherit;\n font-size: 12.5px;\n text-align: left;\n cursor: pointer;\n}\n\n.rvs-hit:hover {\n background: var(--rvs-hover);\n}\n\n.rvs-hit-file {\n font-weight: 600;\n}\n\n.rvs-hit-line {\n padding-left: 30px;\n align-items: flex-start;\n font-family: var(--rvs-mono);\n font-size: 12px;\n color: var(--rvs-fg2);\n}\n\n.rvs-hit-count {\n flex: none;\n min-width: 18px;\n padding: 0 5px;\n border-radius: 9px;\n background: var(--rvs-hover);\n font-size: 10.5px;\n line-height: 15px;\n text-align: center;\n color: var(--rvs-fg3);\n}\n\n.rvs-hit-lineno {\n flex: none;\n width: 3.2em;\n text-align: right;\n color: var(--rvs-fg3);\n opacity: .8;\n font-size: 11px;\n}\n\n.rvs-hit-text {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: pre;\n direction: ltr;\n}\n\n.rvs-mark {\n padding: 0 1px;\n border-radius: 2px;\n background: rgba(226, 183, 20, .3);\n color: inherit;\n}\n\n.rvs-hit-dir {\n flex: none;\n color: var(--rvs-fg3);\n font-size: 11px;\n}\n\n.rvs-hit-name {\n flex: none;\n font-weight: 600;\n}\n\n.rvs-kbd {\n padding: 0 4px;\n border: 1px solid var(--rvs-border2);\n border-radius: 4px;\n font-size: 10px;\n color: var(--rvs-fg3);\n}\n\n/* --- SCM: repository directory row (git runs in THIS directory) ------------- */\n\n.rvs-repo-bar {\n display: flex;\n align-items: center;\n gap: 4px;\n padding: 4px 8px 4px 10px;\n border-bottom: 1px solid var(--rvs-border);\n background: var(--rvs-bg2, var(--dsw-alias-bg-layer-1, #1b1b1b));\n font-size: 11px;\n flex: none;\n}\n\n.rvs-repo-bar-label {\n color: var(--rvs-fg3);\n white-space: nowrap;\n}\n\n.rvs-repo-bar-path {\n flex: 1 1 auto;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n direction: rtl;\n unicode-bidi: plaintext;\n text-align: left;\n padding: 2px 6px;\n border: 1px solid var(--rvs-border2);\n border-radius: 4px;\n background: var(--rvs-bg3, rgba(255, 255, 255, .04));\n color: var(--rvs-fg);\n font-family: var(--rvs-mono, ui-monospace, Consolas, monospace);\n font-size: 11px;\n cursor: pointer;\n}\n\n.rvs-repo-bar-path:hover {\n border-color: var(--rvs-brand);\n color: var(--rvs-brand-fg, var(--rvs-fg));\n}\n\n.rvs-repo-bar-fix {\n margin-top: 10px;\n display: flex;\n justify-content: center;\n}\n\n/* --- SCM: repository directory picker -------------------------------------- */\n\n.rvs-repopick {\n width: min(560px, calc(100vw - 40px));\n display: flex;\n flex-direction: column;\n gap: 8px;\n}\n\n.rvs-repopick .rvs-modal-title {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 8px;\n}\n\n.rvs-repopick-bar {\n display: flex;\n align-items: center;\n flex-wrap: wrap;\n gap: 2px;\n min-height: 22px;\n font-family: var(--rvs-mono, ui-monospace, Consolas, monospace);\n font-size: 12px;\n}\n\n.rvs-crumb-wrap {\n display: inline-flex;\n align-items: center;\n gap: 2px;\n color: var(--rvs-fg3);\n}\n\n.rvs-crumb {\n border: none;\n background: none;\n padding: 1px 3px;\n border-radius: 3px;\n color: var(--rvs-brand-fg, var(--rvs-fg));\n font: inherit;\n cursor: pointer;\n}\n\n.rvs-crumb:hover {\n background: var(--rvs-row-hover, rgba(255, 255, 255, .07));\n}\n\n.rvs-repopick-probe {\n padding: 5px 8px;\n border-radius: 4px;\n font-size: 11.5px;\n border: 1px solid var(--rvs-border2);\n background: var(--rvs-bg3, rgba(255, 255, 255, .04));\n color: var(--rvs-fg2);\n}\n\n.rvs-repopick-probe.root {\n border-color: var(--rvs-ok, #2ea043);\n color: var(--rvs-ok, #2ea043);\n}\n\n.rvs-repopick-probe.inside {\n border-color: var(--rvs-brand);\n color: var(--rvs-brand-fg, var(--rvs-fg));\n}\n\n.rvs-repopick-found {\n display: flex;\n align-items: center;\n flex-wrap: wrap;\n gap: 4px;\n padding-bottom: 2px;\n}\n\n.rvs-repopick-found-label {\n color: var(--rvs-fg3);\n font-size: 11px;\n white-space: nowrap;\n}\n\n.rvs-repopick-list {\n flex: 1 1 auto;\n min-height: 150px;\n max-height: 42vh;\n overflow: auto;\n border: 1px solid var(--rvs-border);\n border-radius: 5px;\n background: var(--rvs-bg-layer, rgba(0, 0, 0, .18));\n padding: 2px;\n}\n\n.rvs-repopick-row {\n display: flex;\n align-items: center;\n gap: 6px;\n width: 100%;\n padding: 3px 6px;\n border: none;\n border-radius: 3px;\n background: none;\n color: var(--rvs-fg);\n font: inherit;\n font-size: 12px;\n text-align: left;\n cursor: pointer;\n}\n\n.rvs-repopick-row:hover {\n background: var(--rvs-row-hover, rgba(255, 255, 255, .07));\n}\n\n.rvs-repopick-row span {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.rvs-repopick-dim {\n color: var(--rvs-fg3);\n font-size: 11.5px;\n padding: 6px;\n}\n\n.rvs-repopick-type {\n display: flex;\n gap: 6px;\n align-items: center;\n}\n\n.rvs-repopick-type .rvs-input {\n flex: 1 1 auto;\n}\n\n.rvs-repopick-error {\n color: var(--rvs-error, #f85149);\n font-size: 11.5px;\n}\n";
|
|
15
15
|
const key$2 = "E:\\work_base\\dsh_remote_vs\\src\\client\\vsc\\vsc.css";
|
|
16
16
|
const injected$2 = typeof document === "undefined" ? void 0 : Array.prototype.find.call(document.querySelectorAll("style[data-rvs-css]"), (tag) => tag.dataset.rvsCss === key$2);
|
|
17
17
|
if (typeof document !== "undefined" && injected$2 === void 0) {
|
|
@@ -1374,6 +1374,19 @@ window.__ModuleLoader__.load({
|
|
|
1374
1374
|
"files.deleteConfirm": "确定删除 {target} 吗?此操作在远端不可撤销。",
|
|
1375
1375
|
"files.open": "打开",
|
|
1376
1376
|
"files.openAsText": "以文本打开",
|
|
1377
|
+
"files.openInFloat": "在悬浮编辑器中打开",
|
|
1378
|
+
"float.title": "悬浮编辑器",
|
|
1379
|
+
"float.dragHint": "拖动标题栏移动窗口;拖动边缘缩放;双击标题栏最大化或还原",
|
|
1380
|
+
"float.minimize": "最小化",
|
|
1381
|
+
"float.ballHint": "点击恢复悬浮编辑器,拖动可移动",
|
|
1382
|
+
"float.dockViewHint": "双击「远程编辑器」标签可浮出编辑器",
|
|
1383
|
+
"float.maximize": "最大化",
|
|
1384
|
+
"float.restore": "还原窗口",
|
|
1385
|
+
"float.dock": "停靠回主界面",
|
|
1386
|
+
"float.placeholder": "远程编辑器正在悬浮窗口中",
|
|
1387
|
+
"float.placeholderHint": "编辑器只有一份实例,已浮出到窗口中央;关闭悬浮窗或点「停靠」后回到这里。",
|
|
1388
|
+
"float.dockBack": "停靠回主界面",
|
|
1389
|
+
"float.unavailable": "当前宿主没有全帧悬浮层(shell.overlay),悬浮编辑器不可用。",
|
|
1377
1390
|
"files.rename": "重命名",
|
|
1378
1391
|
"files.copy": "复制",
|
|
1379
1392
|
"files.cut": "剪切",
|
|
@@ -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
|
|
37621
|
-
|
|
37622
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
/**
|
|
41734
|
-
|
|
41735
|
-
|
|
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
|
-
|
|
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
|
|
43049
|
-
*
|
|
43050
|
-
*
|
|
43051
|
-
*
|
|
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
|
-
*
|
|
43060
|
-
*
|
|
43061
|
-
*
|
|
43062
|
-
*
|
|
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
|
-
|
|
43754
|
+
rememberEditorViewState(tabId, state);
|
|
43081
43755
|
}, []);
|
|
43082
43756
|
/**
|
|
43083
|
-
* Closed tabs may not keep their scroll + caret.
|
|
43084
|
-
* and
|
|
43085
|
-
*
|
|
43086
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
43210
|
-
children: "
|
|
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
|
-
...
|
|
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")
|
|
60473
|
-
|
|
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
|
|
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":
|
|
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 (!
|
|
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;
|