dsh-remote-vs 0.8.8 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/README.en.md +15 -2
  2. package/README.md +12 -1
  3. package/lib/client.js +1507 -59
  4. package/lib/types/client/CodeEditor.d.ts +27 -1
  5. package/lib/types/client/FloatingEditorWindow.d.ts +20 -0
  6. package/lib/types/client/FloatingPlaceholder.d.ts +9 -0
  7. package/lib/types/client/RemoteEditorView.d.ts +4 -0
  8. package/lib/types/client/TerminalView.d.ts +3 -0
  9. package/lib/types/client/XTerm.d.ts +6 -0
  10. package/lib/types/client/float-editor.d.ts +123 -0
  11. package/lib/types/client/float-tab-trigger.d.ts +24 -0
  12. package/lib/types/client/live-editor.d.ts +94 -0
  13. package/lib/types/client/remote-file-open-target.d.ts +0 -22
  14. package/lib/types/client/shell.d.ts +29 -0
  15. package/lib/types/client/store.d.ts +160 -6
  16. package/package.json +1 -1
  17. package/src/client/CodeEditor.tsx +95 -8
  18. package/src/client/EditorPane.tsx +45 -24
  19. package/src/client/FloatingEditorWindow.tsx +494 -0
  20. package/src/client/FloatingPlaceholder.tsx +22 -0
  21. package/src/client/RemoteEditorView.tsx +7 -2
  22. package/src/client/RemoteExplorer.tsx +61 -0
  23. package/src/client/RemoteRoot.tsx +16 -7
  24. package/src/client/SearchView.tsx +49 -2
  25. package/src/client/TerminalView.tsx +53 -2
  26. package/src/client/XTerm.tsx +15 -0
  27. package/src/client/float-editor.ts +271 -0
  28. package/src/client/float-tab-trigger.ts +51 -0
  29. package/src/client/i18n.ts +32 -0
  30. package/src/client/index.tsx +48 -1
  31. package/src/client/live-editor.ts +155 -0
  32. package/src/client/remote-file-open-target.ts +1 -5
  33. package/src/client/remote-file-open.ts +2 -2
  34. package/src/client/scm/ScmPanel.tsx +42 -1
  35. package/src/client/shell.ts +35 -0
  36. package/src/client/store.tsx +367 -16
  37. package/src/client/vsc/editor-theme.ts +6 -1
  38. package/src/client/vsc/vsc.css +235 -0
@@ -23,7 +23,18 @@ export interface CodeEditorViewState {
23
23
  export interface CodeEditorHandle {
24
24
  getContent: () => string;
25
25
  isDirty: () => boolean;
26
- markSaved: () => void;
26
+ /**
27
+ * Advance the dirty baseline to the bytes a save just wrote to the remote
28
+ * file, and re-derive the dirty flag from the LIVE buffer against them.
29
+ *
30
+ * Takes the saved text explicitly because the user may have typed while the
31
+ * remote write was in flight: the buffer is then NEWER than the file, and
32
+ * declaring it saved would be the same silent-loss bug in the other
33
+ * direction. `tabId` guards against the pane having switched to another tab
34
+ * while the save awaited — marking that editor saved would corrupt ITS
35
+ * baseline.
36
+ */
37
+ markSaved: (savedText: string, tabId: string) => void;
27
38
  /** Snapshot scroll + selection, for restoring after a remount. */
28
39
  getViewState: () => CodeEditorViewState | undefined;
29
40
  /** Scroll back and put the caret where it was. */
@@ -120,6 +131,21 @@ export declare const CodeEditor: import("react").ForwardRefExoticComponent<{
120
131
  * where a file was when its tab is re-selected.
121
132
  */
122
133
  viewState?: CodeEditorViewState;
134
+ /**
135
+ * The text as it was last LOADED from / SAVED to the remote file — the dirty
136
+ * baseline.
137
+ *
138
+ * Separate from `initial` because a tab switch or a surface flip remounts the
139
+ * editor with the LIVE buffer as `initial` (that is how unsaved edits survive
140
+ * the remount), while the file on disk is still the older `savedText`. Using
141
+ * `initial` as the baseline would declare the unsaved text clean: the dirty
142
+ * dot and status would lie, the close guard would stop prompting and Save
143
+ * would disable — a silent way to lose the very edits the handoff preserved.
144
+ *
145
+ * Absent (diff panes, older callers): the mount's own document is the
146
+ * baseline, which is the historical behaviour.
147
+ */
148
+ savedText?: string;
123
149
  onSave: () => void;
124
150
  onDirty: (dirty: boolean) => void;
125
151
  } & import("react").RefAttributes<CodeEditorHandle>>;
@@ -0,0 +1,20 @@
1
+ /**
2
+ * The floating editor window: the plugin's ONE editor pane hosted in a
3
+ * draggable, resizable frame above the app shell.
4
+ *
5
+ * It is mounted by the client entry into the host's frame-wide overlay slot
6
+ * (`shell.overlay`) and reads the same module-level store as every other
7
+ * surface, so tabs / previews / diffs / find / change marks / undo / @file are
8
+ * not a re-implementation: this file only owns the FRAME (position, size,
9
+ * z-order, drag + resize + zoom) and the handoff around the mount swap.
10
+ *
11
+ * The mount is exclusive: `EditorPane` must exist exactly once, so the centre
12
+ * conversation view and the sidebar fallback render a placeholder while
13
+ * `editorSurface === 'float'` (see `FloatingEditorPlaceholder`).
14
+ */
15
+ import { type ReactNode } from 'react';
16
+ import type { Context } from '@deepseek-ai/cordis';
17
+ /** The window frame plus the editor. `ctx` is only needed for the boot hook. */
18
+ export declare function FloatingEditorWindow(props: {
19
+ ctx: Context;
20
+ }): ReactNode;
@@ -0,0 +1,9 @@
1
+ /**
2
+ * What the centre conversation view and the sidebar fallback show while the
3
+ * editor is popped out: the editor itself is a single instance and lives in the
4
+ * floating window, so these surfaces must not mount a second one (that would
5
+ * be two live CodeMirror buffers for one tab — the exact defect `EditorPane`'s
6
+ * header warns about). A dock button keeps the surface from looking broken.
7
+ */
8
+ import type { ReactNode } from 'react';
9
+ export declare function FloatingEditorPlaceholder(): ReactNode;
@@ -6,6 +6,10 @@
6
6
  * sidebar then keeps the whole column for the explorer. Both surfaces read
7
7
  * the same module-level store, so a diff opened from source control appears
8
8
  * here, and a save here repaints the sidebar's change list.
9
+ *
10
+ * While the editor floats (`editorSurface === 'float'`) this tab is NOT the
11
+ * editor's mount any more, so it renders a placeholder instead: mounting a
12
+ * second `EditorPane` would give the same tab two live buffers.
9
13
  */
10
14
  import type { Context } from '@deepseek-ai/cordis';
11
15
  import type { ReactNode } from 'react';
@@ -9,8 +9,11 @@
9
9
  */
10
10
  import { type ReactNode } from 'react';
11
11
  import type { RemoteHostSummary } from '../protocol.ts';
12
+ import type { TerminalRequest } from './store.tsx';
12
13
  export declare function TerminalView(props: {
13
14
  t: (key: string) => string;
14
15
  hosts: RemoteHostSummary[];
15
16
  visible: boolean;
17
+ /** A pending "open a terminal here" request from the search results' menu. */
18
+ request?: TerminalRequest;
16
19
  }): ReactNode;
@@ -12,4 +12,10 @@ export declare function XTerm(props: {
12
12
  fontFamily?: string;
13
13
  /** When the parent sub-tab hides/shows this view without unmounting. */
14
14
  visible?: boolean;
15
+ /**
16
+ * Line typed into the fresh shell once its PTY reports ready (a menu entry's
17
+ * `cd`). Sent ONCE per mount: a reconnect is a new mount and thus a new shell,
18
+ * which correctly re-runs the line.
19
+ */
20
+ initialInput?: string;
15
21
  }): ReactNode;
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Pure geometry and surface rules for the floating editor.
3
+ *
4
+ * Kept free of React and of the DOM on purpose: the drag / resize / zoom maths
5
+ * is the part that is easy to get subtly wrong (negative sizes, a window that
6
+ * escapes the viewport, a stale stored rect from a larger screen), and this
7
+ * module is what `scripts/check-float-editor.mjs` drives directly. The window
8
+ * component is then only wiring.
9
+ *
10
+ * The SURFACE rule lives here too: the plugin has exactly one live editor, and
11
+ * which mount owns it is a three-way decision (centre conversation view,
12
+ * sidebar fallback, floating window). Encoding it as a pure function keeps the
13
+ * invariant testable and out of three separate `=== 'center'` comparisons.
14
+ */
15
+ /** Where the editor pane is mounted right now. */
16
+ export type EditorSurface = 'center' | 'sidebar' | 'float';
17
+ /** The persisted layout of the floating window, in viewport pixels. */
18
+ export interface FloatRect {
19
+ x: number;
20
+ y: number;
21
+ width: number;
22
+ height: number;
23
+ }
24
+ /** Viewport size the geometry is clamped to. */
25
+ export interface FloatViewport {
26
+ width: number;
27
+ height: number;
28
+ }
29
+ /** A resize grip: the compass directions of the eight handles. */
30
+ export type FloatResizeHandle = 'n' | 's' | 'e' | 'w' | 'ne' | 'nw' | 'se' | 'sw';
31
+ /** Smallest window the editor can be squeezed into and stay usable. */
32
+ export declare const FLOAT_MIN_WIDTH = 360;
33
+ export declare const FLOAT_MIN_HEIGHT = 220;
34
+ /** Height of the title bar; a minimized window keeps exactly this much. */
35
+ export declare const FLOAT_TITLEBAR_HEIGHT = 34;
36
+ /** Gap a maximized window keeps from the viewport edges. */
37
+ export declare const FLOAT_VIEWPORT_EDGE = 8;
38
+ /** Editor font-size range for Ctrl+wheel zoom (px). */
39
+ export declare const FLOAT_FONT_MIN = 9;
40
+ export declare const FLOAT_FONT_MAX = 28;
41
+ export declare const FLOAT_FONT_DEFAULT = 13;
42
+ /** Diameter of the minimized ball, in pixels. */
43
+ export declare const FLOAT_BALL_SIZE = 44;
44
+ /**
45
+ * How far a pointer may travel and still count as a CLICK: the minimized window
46
+ * is a single 44px ball, so "click restores / drag moves" has to be decided
47
+ * from one pointer gesture.
48
+ */
49
+ export declare const FLOAT_BALL_CLICK_SLOP = 4;
50
+ /** A point in viewport coordinates (the minimized ball's origin). */
51
+ export interface FloatPoint {
52
+ x: number;
53
+ y: number;
54
+ }
55
+ /** The effective editor surface: floating wins, otherwise the base mount. */
56
+ export declare function effectiveEditorSurface(base: 'center' | 'sidebar', floatOpen: boolean): EditorSurface;
57
+ /**
58
+ * The rect a fresh floating window opens at: about two thirds of the frame,
59
+ * centred, with a hard size cap so a 4K monitor does not produce a 2600px
60
+ * editor for a 40-column file.
61
+ */
62
+ export declare function defaultFloatRect(viewport: FloatViewport): FloatRect;
63
+ /**
64
+ * Bring a rect back inside the viewport and to a legal size.
65
+ *
66
+ * Used on every drag step, on restore, and whenever the browser window is
67
+ * resized: a rect persisted on a 2560px screen must not open a window whose
68
+ * title bar is off-screen on a 1366px laptop.
69
+ */
70
+ export declare function clampFloatRect(rect: FloatRect, viewport: FloatViewport): FloatRect;
71
+ /** Move the window by a pointer delta, staying inside the viewport. */
72
+ export declare function dragFloatRect(start: FloatRect, dx: number, dy: number, viewport: FloatViewport): FloatRect;
73
+ /**
74
+ * Resize from one grip.
75
+ *
76
+ * Each axis is adjusted from the edge that MOVED (west/north resize changes the
77
+ * origin too), then clamped: the minimum size is anchored at the opposite edge
78
+ * (so dragging the north grip past the bottom stops instead of flipping the
79
+ * rect) and no edge may leave the viewport.
80
+ */
81
+ export declare function resizeFloatRect(start: FloatRect, handle: FloatResizeHandle, dx: number, dy: number, viewport: FloatViewport): FloatRect;
82
+ /** The rect a maximized window occupies: the frame minus a small margin. */
83
+ export declare function maximizeFloatRect(viewport: FloatViewport): FloatRect;
84
+ /**
85
+ * Parse a persisted rect.
86
+ *
87
+ * Anything malformed is discarded rather than repaired into a guess: the
88
+ * caller then opens the default rect. Values that are merely STALE (a rect from
89
+ * another screen) are legal input and get clamped.
90
+ */
91
+ export declare function parseFloatRect(raw: string | null | undefined, viewport: FloatViewport): FloatRect | undefined;
92
+ /** Serialize a rect for localStorage. */
93
+ export declare function serializeFloatRect(rect: FloatRect): string;
94
+ /**
95
+ * One Ctrl+wheel step: up (negative deltaY) grows the text, down shrinks it.
96
+ * Values are whole pixels so the stored size stays stable across sessions.
97
+ */
98
+ export declare function stepFloatZoom(current: number, deltaY: number): number;
99
+ /** Parse the persisted editor font size (fallback = the default 13px). */
100
+ export declare function parseFloatFontSize(raw: string | null | undefined): number;
101
+ /**
102
+ * Where the minimized ball may sit: fully on screen, whatever position was
103
+ * asked for (a ball restored on a smaller monitor must stay clickable).
104
+ */
105
+ export declare function clampBallPosition(pos: FloatPoint, viewport: FloatViewport): FloatPoint;
106
+ /**
107
+ * The ball's default spot when a window is minimized: its own top-left corner,
108
+ * so the ball appears where the window was instead of jumping to a corner.
109
+ */
110
+ export declare function ballFromRect(rect: FloatRect, viewport: FloatViewport): FloatPoint;
111
+ /**
112
+ * Whether a finished gesture was a CLICK rather than a drag.
113
+ *
114
+ * The minimized window is one small target with two actions on the same
115
+ * pointer: a press that never really moved restores the window, a real drag
116
+ * moves the ball. Without this distinction every move-by-3px would also
117
+ * restore, and every click would be a zero-distance drag.
118
+ */
119
+ export declare function isBallClick(dx: number, dy: number): boolean;
120
+ /** Parse a persisted ball position (stale geometry is clamped, garbage dropped). */
121
+ export declare function parseBallPosition(raw: string | null | undefined, viewport: FloatViewport): FloatPoint | undefined;
122
+ /** Serialize a ball position for localStorage. */
123
+ export declare function serializeBallPosition(pos: FloatPoint): string;
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Double-click on the shell's 「远程编辑器」 tab floats the editor.
3
+ *
4
+ * That tab belongs to the host's conversation-view header, not to us: the plugin
5
+ * registers a VIEW into the shell's tab strip and never sees the tab row's DOM.
6
+ * There is no public service for "the user double-clicked my tab" either, so the
7
+ * only way to honour the gesture is a document-level `dblclick` listener that
8
+ * recognises the tab the same way `reveal.ts` does for clicks — by its label,
9
+ * skipping our own surfaces.
10
+ *
11
+ * Fail-safe by construction: a miss (another host build, a renamed tab, a
12
+ * double-click somewhere else) simply does nothing. It never prevents the
13
+ * default action, never throws, and does not steal double-clicks from the
14
+ * plugin's own UI (`.rvs-root`) or from dialogs.
15
+ */
16
+ /**
17
+ * Install the double-click trigger.
18
+ *
19
+ * @param label - the tab's visible label (the same localized string the view
20
+ * registered with); matched against the tab's trimmed text.
21
+ * @param onTrigger - called once per recognised double-click.
22
+ * @returns disposer removing the listener.
23
+ */
24
+ export declare function installFloatTabTrigger(label: string, onTrigger: () => void): () => void;
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Handoff for the ONE live editor buffer when its mount is replaced.
3
+ *
4
+ * The plugin mounts exactly one `EditorPane` at a time (centre conversation
5
+ * view, sidebar fallback, or the floating window), and `CodeEditor` reads its
6
+ * `initial` text once on mount. So moving the editor between surfaces is
7
+ * effectively a remount: without a handoff the buffer typed since the last
8
+ * load is gone and the tab's stored text is the pre-edit one.
9
+ *
10
+ * This module is the handoff, and it is module-level for the same reason the
11
+ * store is: `EditorPane` is unmounted before the replacement mounts, so no
12
+ * React state survives the swap.
13
+ *
14
+ * Two things travel, and they are captured SEPARATELY because their costs
15
+ * differ by orders of magnitude:
16
+ * - the buffer text (the store copies it onto the tab before the surface
17
+ * flips, so the replacement mounts with it as `initial`) — materializes the
18
+ * whole document, so it is taken only when the tab may differ from disk;
19
+ * - scroll + caret (kept here and read back by the replacement, which is how
20
+ * a tab switch already restored its position — same storage, now shared by
21
+ * every surface instead of one component's `useRef`) — cheap, taken on
22
+ * every switch.
23
+ */
24
+ import type { CodeEditorHandle, CodeEditorViewState } from './CodeEditor.tsx';
25
+ /**
26
+ * Register the editor that is on screen right now.
27
+ *
28
+ * The caller passes a getter, not a handle: `editorRef.current` is `null`
29
+ * during the first render and is cleared before the parent's cleanup runs, so
30
+ * a snapshot taken later must read through the getter.
31
+ * @returns disposer; only the registration that is still current is cleared.
32
+ */
33
+ export declare function registerLiveEditor(tabId: string, getHandle: () => CodeEditorHandle | null): () => void;
34
+ /**
35
+ * Snapshot the live position (scroll + caret) without touching the document.
36
+ *
37
+ * Cheap, so the store can call it on EVERY switch: a surface flip remounts the
38
+ * very same tab, and the replacement's render happens before the old view's
39
+ * unmount report — the position has to be in the map before that render.
40
+ */
41
+ export declare function captureLiveEditorPosition(): void;
42
+ /**
43
+ * Whether the live buffer differs from its disk baseline.
44
+ *
45
+ * The store uses this to skip the O(document) text copy on clean tabs (see
46
+ * `captureLiveBuffer`): the boolean is cheap, `getContent()` materializes the
47
+ * whole file as a string.
48
+ */
49
+ export declare function isLiveEditorDirty(): boolean;
50
+ /**
51
+ * Copy the live buffer's text, for a switch that would otherwise lose it.
52
+ *
53
+ * Called from the store BEFORE the tab switch / surface flip: at that moment
54
+ * the editor is still mounted and its CodeMirror view still owns the edits.
55
+ * @returns undefined when no file editor is live (a diff tab, or a view that
56
+ * was never the selected one) — there is nothing to carry over then.
57
+ */
58
+ export declare function captureLiveEditorText(): {
59
+ tabId: string;
60
+ text: string;
61
+ } | undefined;
62
+ /**
63
+ * Whether one tab's file editor is the one registered right now.
64
+ *
65
+ * The save path asks this before it clears a tab's dirty flag: if an editor IS
66
+ * mounted for that tab, the flag belongs to the live buffer and only the
67
+ * editor may clear it (see `markLiveEditorSaved`).
68
+ */
69
+ export declare function isLiveEditorFor(tabId: string): boolean;
70
+ /**
71
+ * Advance the registered editor's dirty baseline to the bytes a save wrote.
72
+ *
73
+ * Routed through the MODULE-LEVEL registration rather than a React ref on
74
+ * purpose: the pane can be remounted while the remote write is in flight (a
75
+ * surface flip, a dock, a tab switch), and the ref the save started with is
76
+ * then detached — that is exactly how a tab ended up reported clean while its
77
+ * live buffer was newer than the file. The registration always names the editor
78
+ * mounted NOW, so the correction lands on whichever instance is on screen, and
79
+ * it is skipped (never misapplied) when that editor shows another tab: that tab
80
+ * heals on its next mount from `savedText` (see `CodeEditor`).
81
+ * @returns true when an editor for this tab took the new baseline.
82
+ */
83
+ export declare function markLiveEditorSaved(tabId: string, savedText: string): boolean;
84
+ /** Remember one tab's scroll + caret (reported by the editor, see EditorPane). */
85
+ export declare function rememberEditorViewState(tabId: string, state: CodeEditorViewState): void;
86
+ /** The stored position for one tab, or undefined when it was never seen. */
87
+ export declare function editorViewStateOf(tabId: string): CodeEditorViewState | undefined;
88
+ /**
89
+ * Drop positions of tabs that no longer exist.
90
+ *
91
+ * The map used to live in a component with the same cleanup: without it a
92
+ * long session accumulated one state object per file ever opened.
93
+ */
94
+ export declare function pruneEditorViewStates(liveIds: ReadonlySet<string>): void;
@@ -1,25 +1,3 @@
1
- /**
2
- * Where a clicked file reference should open when the owning session lives in
3
- * a remote workspace (pure — no React, no fetch, so the gate can drive it).
4
- *
5
- * DSH renders every file reference as a `dsh-resource://file/…` address and
6
- * opens it through its own right-sidebar preview. That preview resolves the
7
- * address path against the SESSION WORKSPACE ROOT. A model that quotes a path
8
- * relative to its CURRENT directory (e.g. `_xfer/reports/x.md` while working
9
- * in `<workspace>/<inner-repo>`) therefore misses by one segment, and a
10
- * remote session's workspace root only exists as an EMPTY local alias dir —
11
- * the click dies with the host-side ENOENT the user sees.
12
- *
13
- * This module answers "which binding, which remote paths to try" for the
14
- * client interceptor:
15
- * - relative paths are tried against the workspace root first and the
16
- * session's current working directory second (the model's usual writing
17
- * base), which is what makes the one-segment miss heal;
18
- * - absolute spellings (local alias, POSIX remote, drive-qualified alias)
19
- * are folded back onto the binding;
20
- * - anything that does not name a known binding answers `undefined`, so the
21
- * native preview keeps handling local sessions and unknown addresses.
22
- */
23
1
  /** One remote binding as the client store exposes it. */
24
2
  export interface RemoteOpenBinding {
25
3
  id: string;
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Shell quoting for the lines this plugin types into a remote terminal.
3
+ *
4
+ * One rule, one place: the client builds POSIX shell text in three spots (the
5
+ * `cd` typed into a fresh terminal, the bounded suffix search for an unresolved
6
+ * click target, and the terminal command a menu entry sends), and a second copy
7
+ * of the quoting rule is how an injection bug reappears after it was fixed once.
8
+ *
9
+ * Single quotes are the only form that is safe for arbitrary paths — double
10
+ * quotes still expand `$`, backticks and `\` — and the closing quote is the one
11
+ * character they cannot carry, so it is spliced in as `'\''`.
12
+ */
13
+ /**
14
+ * Quote one value for a POSIX shell, as a single word.
15
+ *
16
+ * @param value - the raw path or word.
17
+ * @returns `'value'` with every embedded `'` spliced as `'\''`.
18
+ */
19
+ export declare function shellQuote(value: string): string;
20
+ /**
21
+ * The command typed into a fresh remote terminal to land in one directory.
22
+ *
23
+ * A trailing newline is NOT included: the caller writes the line and the
24
+ * terminal's Enter, which keeps the same string reusable for display.
25
+ *
26
+ * @param dir - absolute remote directory.
27
+ * @returns `cd '<dir>'`.
28
+ */
29
+ export declare function cdCommand(dir: string): string;
@@ -5,6 +5,7 @@ import { type StatusBuckets, type TreeMarks } from './scm/status-map.ts';
5
5
  import { type DiffPayload } from './scm/actions.ts';
6
6
  import { type DirExpansions } from './scm/untracked-dirs.ts';
7
7
  import { type Lang } from './i18n.ts';
8
+ import { type EditorSurface } from './float-editor.ts';
8
9
  /** One editor tab (code file, preview or diff). */
9
10
  export interface EditorTab {
10
11
  id: string;
@@ -17,6 +18,16 @@ export interface EditorTab {
17
18
  error: string | undefined;
18
19
  /** File payload. */
19
20
  text: string;
21
+ /**
22
+ * The text as last LOADED from / SAVED to the remote file.
23
+ *
24
+ * `text` is the live-buffer handoff copy (see `captureLiveBuffer`) and may
25
+ * therefore hold unsaved edits; this is the disk-side reference the editor
26
+ * uses as its dirty baseline. Without the split, a remount with the handoff
27
+ * text would label unsaved edits "clean" (dirty dot off, Save disabled, close
28
+ * guard silent) — the exact silent-loss path the handoff exists to avoid.
29
+ */
30
+ baseline?: string;
20
31
  dirty: boolean;
21
32
  saving: boolean;
22
33
  binary: boolean;
@@ -82,6 +93,30 @@ export interface SearchRequest {
82
93
  /** Absolute remote directory the search is scoped to. */
83
94
  scope: string;
84
95
  }
96
+ /** One "show this folder in the explorer tree" request. */
97
+ export interface RevealRequest {
98
+ /** Monotonic id so asking for the same folder twice still re-triggers. */
99
+ id: number;
100
+ /** Absolute remote DIRECTORY to expand and scroll to. */
101
+ path: string;
102
+ }
103
+ /** One "open a terminal here" request. */
104
+ export interface TerminalRequest {
105
+ /** Monotonic id so the same folder can be opened twice. */
106
+ id: number;
107
+ /**
108
+ * Host alias the directory lives on.
109
+ *
110
+ * Carried explicitly, and NOT taken from the terminal panel's own last-used
111
+ * host: the panel remembers whatever host the user ran a shell on last, so a
112
+ * menu action on a workspace bound elsewhere opened the shell on the WRONG
113
+ * machine — the `cd` then failed with "No such file or directory" for a path
114
+ * that exists perfectly well on its own host.
115
+ */
116
+ alias: string;
117
+ /** Absolute remote directory the fresh shell should `cd` into. */
118
+ dir: string;
119
+ }
85
120
  /** Stored repository sub-directory for one binding ('' = the workspace root). */
86
121
  export declare function readRepoSubdir(workspaceId: string): string;
87
122
  /** Everything a view can read, plus the actions. */
@@ -120,9 +155,16 @@ export interface ExplorerValue {
120
155
  activeTab: EditorTab | undefined;
121
156
  activeTabId: string | undefined;
122
157
  revision: number;
123
- editorSurface: 'center' | 'sidebar';
158
+ /** Effective editor mount: the floating window wins over the base surface. */
159
+ editorSurface: EditorSurface;
160
+ floatOpen: boolean;
161
+ floatAvailable: boolean;
124
162
  view: RailView;
125
163
  searchRequest: SearchRequest | undefined;
164
+ /** Pending "show this folder in the tree" request (consumed by the explorer). */
165
+ revealRequest: RevealRequest | undefined;
166
+ /** Pending "open a terminal here" request (consumed by the terminal panel). */
167
+ terminalRequest: TerminalRequest | undefined;
126
168
  setView: (view: RailView) => void;
127
169
  /** Switch to the search panel (optionally scoped to a folder) — the tree's
128
170
  "find in folder" entry point. */
@@ -131,6 +173,17 @@ export interface ExplorerValue {
131
173
  query?: string;
132
174
  scope: string;
133
175
  }) => void;
176
+ /**
177
+ * Show one remote directory in the explorer: switch to it, expand the folder
178
+ * and its ancestors, then scroll its row into view. Raised by the search
179
+ * results' "open containing folder" entry.
180
+ */
181
+ revealInExplorer: (dir: string) => void;
182
+ /**
183
+ * Open a fresh terminal tab whose shell `cd`s into `dir`. Raised by the
184
+ * search results' "open in terminal" entry.
185
+ */
186
+ openTerminalAt: (dir: string) => void;
134
187
  /**
135
188
  * The USER's pick — the workspace dropdowns and the wizard's "open files".
136
189
  *
@@ -160,6 +213,21 @@ export interface ExplorerValue {
160
213
  openFile: (fullPath: string, options?: {
161
214
  line?: number;
162
215
  }) => void;
216
+ /**
217
+ * 「在悬浮编辑器中打开」:把编辑器整体浮出,再打开这个文件。
218
+ *
219
+ * 先捕获当前编辑器缓冲区再切表面(见 `captureLiveBuffer`):主界面里没保存
220
+ * 的编辑不会因为这次切换丢掉。
221
+ */
222
+ openInFloatEditor: (fullPath: string, options?: {
223
+ line?: number;
224
+ }) => void;
225
+ /** 同上,只是打开的是工作区 diff(SCM 更改列表的悬浮入口)。 */
226
+ openDiffInFloatEditor: (rel: string, staged: boolean, untracked?: boolean) => void;
227
+ /** 把当前编辑器整体浮出(双击会话视图的「远程编辑器」标签)。 */
228
+ openFloatingEditor: () => void;
229
+ /** 停靠回主界面(底层的中心视图或侧栏表面)。 */
230
+ dockFloatingEditor: () => void;
163
231
  openDiff: (rel: string, staged: boolean, untracked?: boolean) => void;
164
232
  openCommitFile: (hash: string, rel: string) => void;
165
233
  activateTab: (id: string) => void;
@@ -176,7 +244,12 @@ export interface ExplorerValue {
176
244
  * `changeSet.marks`,`hunk` 是 `patches` 的下标)。
177
245
  */
178
246
  undoChange: (tabId: string, mark: ChangeMark) => Promise<void>;
179
- saveTab: (id: string, content: string) => Promise<void>;
247
+ /**
248
+ * Write one file tab back to the remote host.
249
+ * @returns true only when the write really succeeded — the caller uses it to
250
+ * advance the editor's dirty baseline (see `CodeEditorHandle.markSaved`).
251
+ */
252
+ saveTab: (id: string, content: string) => Promise<boolean>;
180
253
  reloadTab: (id: string) => Promise<void>;
181
254
  info: (text: string) => void;
182
255
  fail: (text: string) => void;
@@ -197,6 +270,32 @@ export declare function requestSearch(request: {
197
270
  query?: string;
198
271
  scope: string;
199
272
  }): void;
273
+ /**
274
+ * Show one remote directory in the explorer.
275
+ *
276
+ * Two stages, because the tree is lazily loaded: this raises the request and
277
+ * switches the panel, and the explorer itself expands the ancestors (loading
278
+ * each level) and scrolls the row into view — it is the only place that knows
279
+ * which directories are currently expanded.
280
+ *
281
+ * @param dir - absolute remote DIRECTORY (a file's parent for a file hit).
282
+ */
283
+ export declare function revealInExplorer(dir: string): void;
284
+ /**
285
+ * Open a fresh terminal tab whose shell lands in `dir` — on the CURRENT
286
+ * workspace's host.
287
+ *
288
+ * The terminal panel consumes the request (it owns the tabs) and the shell gets
289
+ * a quoted `cd` once its PTY reports ready — the host's terminal route has no
290
+ * "start in this directory" parameter, so a typed command is the honest way.
291
+ *
292
+ * The alias travels IN the request. The panel otherwise opens new tabs on the
293
+ * host it last used, which put the shell on a different machine than the path
294
+ * belongs to and made the `cd` fail with "No such file or directory".
295
+ *
296
+ * @param dir - absolute remote directory.
297
+ */
298
+ export declare function openTerminalAt(dir: string): void;
200
299
  /**
201
300
  * Select the binding every view works against — the USER's choice.
202
301
  *
@@ -373,8 +472,16 @@ export declare function remoteOpenBindings(): {
373
472
  export declare function openRemoteFileInBinding(bindingId: string, remotePath: string, options?: {
374
473
  line?: number;
375
474
  }): boolean;
376
- /** Whether the editor tab is mounted in the centre conversation view. */
377
- export declare function isEditorCentered(): boolean;
475
+ /**
476
+ * Whether the plugin's own rich editor is the place a remote file click should
477
+ * land (the click interceptor's gate, see `remote-file-open.ts`).
478
+ *
479
+ * The centre conversation view is the normal case; while the editor floats it
480
+ * is the floating window instead — the file still belongs in OUR editor, not in
481
+ * the host's native preview. On the sidebar-fallback layout (`sidebar`) the
482
+ * interceptor stays off, exactly as before this feature.
483
+ */
484
+ export declare function isEditorTakeoverActive(): boolean;
378
485
  /** Open (or focus) a working-tree diff of one repository path. */
379
486
  export declare function openDiff(rel: string, staged: boolean, untracked?: boolean): void;
380
487
  /** Open (or focus) one file of one commit (`ref^` against `ref`). */
@@ -382,8 +489,8 @@ export declare function openCommitFile(hash: string, rel: string): void;
382
489
  export declare function activateTab(id: string): void;
383
490
  export declare function closeTab(id: string): void;
384
491
  export declare function updateTab(id: string, next: Partial<EditorTab>): void;
385
- /** Write one tab back to the remote host. */
386
- export declare function saveTab(id: string, content: string): Promise<void>;
492
+ /** Write one tab back to the remote host. @returns true when the write landed. */
493
+ export declare function saveTab(id: string, content: string): Promise<boolean>;
387
494
  /**
388
495
  * Re-read one tab (file content or diff payload).
389
496
  *
@@ -405,6 +512,53 @@ export declare function saveTab(id: string, content: string): Promise<void>;
405
512
  export declare function reloadTab(id: string): Promise<void>;
406
513
  /** Note where the editor pane is mounted (set by the client entry). */
407
514
  export declare function setEditorSurface(surface: 'center' | 'sidebar'): void;
515
+ /**
516
+ * Record that the host's frame-wide overlay slot actually rendered the
517
+ * floating window host (see `FloatingEditorWindow`).
518
+ *
519
+ * This is a runtime handshake, not a configuration flag: on a host whose
520
+ * `shell.overlay` slot is missing, the entry never mounts and the menu entries
521
+ * stay disabled instead of opening a surface that cannot paint.
522
+ */
523
+ export declare function setFloatEditorAvailable(available: boolean): void;
524
+ /**
525
+ * Float the editor in its current state, without opening anything.
526
+ *
527
+ * The entry is a double-click on the shell's 「远程编辑器」 tab (see
528
+ * `float-tab-trigger.ts`): the user is already looking at the editor and wants
529
+ * it out of the layout — no file, no diff, same tabs and buffers.
530
+ *
531
+ * Idempotent: while the editor already floats this does nothing (the window is
532
+ * on screen; a second trigger must not restart it).
533
+ */
534
+ export declare function openFloatingEditor(): void;
535
+ /**
536
+ * Open one file in the floating editor (file-tree / search / SCM menu action).
537
+ *
538
+ * Order matters twice:
539
+ * 1. the live buffer is captured BEFORE the surface flips (the flip unmounts
540
+ * the editor that owns it);
541
+ * 2. a second capture runs even when already floating, because opening
542
+ * another file is a tab switch — the very action that remounts the editor
543
+ * and would otherwise drop the edits typed in the tab being left.
544
+ * A host whose overlay slot is unavailable gets the file in the normal surface
545
+ * instead (with the reason toasted), so the click is never a dead end.
546
+ */
547
+ export declare function openInFloatEditor(fullPath: string, options?: {
548
+ line?: number;
549
+ }): void;
550
+ /**
551
+ * The floating variant of `openDiff` — the SCM change list's entry.
552
+ *
553
+ * Same handoff order as `openInFloatEditor`; a diff tab has no text buffer of
554
+ * its own, but the file editor that WAS on screen still does.
555
+ */
556
+ export declare function openDiffInFloatEditor(rel: string, staged: boolean, untracked?: boolean): void;
557
+ /**
558
+ * Dock the editor back onto its base surface (centre conversation view or the
559
+ * sidebar fallback), carrying the live buffer with it.
560
+ */
561
+ export declare function dockFloatingEditor(): void;
408
562
  /**
409
563
  * 换一份「本会话改了什么」的标记。
410
564
  *
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-remote-vs",
3
3
  "description": "VSCode-style remote development for DSH: SSH host manager (key/password), helper deploy pipeline with version check, remote file explorer + editor + terminal in dsh-better-sidebar.",
4
- "version": "0.8.8",
4
+ "version": "0.9.0",
5
5
  "icon": "icon.svg",
6
6
  "keywords": [
7
7
  "dsh",