dsh-remote-vs 0.9.1 → 0.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/client.js +231 -171
- package/lib/types/client/composer.d.ts +158 -75
- package/lib/types/client/selection-popup.d.ts +8 -8
- package/package.json +1 -1
- package/src/client/CodeEditor.tsx +6 -3
- package/src/client/composer.ts +329 -220
- package/src/client/selection-popup.ts +12 -12
|
@@ -1,83 +1,170 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Remote `@file` insertion into the current session's composer
|
|
3
|
-
* far-side twin of dsh-better-sidebar's `conversation-draft.ts`
|
|
4
|
-
* `selection-payload.ts
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* consumes through the fs router.
|
|
2
|
+
* Remote `@file` / selection insertion into the current session's composer
|
|
3
|
+
* draft: the far-side twin of dsh-better-sidebar's `conversation-draft.ts` +
|
|
4
|
+
* `selection-payload.ts`. The stock local flow projects explorer rows against
|
|
5
|
+
* the host-confirmed workspace root; remote rows are POSIX absolutes on
|
|
6
|
+
* another machine, so they project against the binding's `remotePath` instead —
|
|
7
|
+
* the same workspace-relative spelling the P3 `TransparentFileReferences`
|
|
8
|
+
* router completes, and the `read` tool consumes through the fs router.
|
|
10
9
|
*
|
|
11
|
-
* Vendored
|
|
12
|
-
* - `
|
|
13
|
-
* - `
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* the
|
|
17
|
-
*
|
|
18
|
-
*
|
|
10
|
+
* Vendored behavior (dsh-better-sidebar **0.28.1**, same semantics):
|
|
11
|
+
* - `leadFor` / `appendText` (the one separating-space rule),
|
|
12
|
+
* - `foldClipboardOffset` / `appendOffset` (the host keeps TWO coordinate
|
|
13
|
+
* systems over the draft: the clipboard projection — `draft` — expands every
|
|
14
|
+
* chip to its whole `clipboardText`, while every insert event's `span` speaks
|
|
15
|
+
* the editor's DETECT projection, one placeholder character per chip; a span
|
|
16
|
+
* derived without folding is rejected by the host on the second chip),
|
|
17
|
+
* - `insertDraftReference` — mint one structured chip through the host's public
|
|
18
|
+
* `insertReference` facade (the method the native `@` picker calls), with a
|
|
19
|
+
* leading space pushed through `slash/input-insert-text` when the draft needs
|
|
20
|
+
* one,
|
|
21
|
+
* - `appendToDraft` — plain text: `setDraft` only for a chipless draft;
|
|
22
|
+
* anything carrying chips (or a claimed `/command`) is written through the
|
|
23
|
+
* span channel, because `setDraft` rebuilds the document from a string and
|
|
24
|
+
* FLATTENS every chip,
|
|
25
|
+
* - `insertReferenceOrText` — the single fallback policy both surfaces use,
|
|
19
26
|
* - `mentionFor` / `directoryMention` (the `@` quoting grammar),
|
|
20
|
-
* - `
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
27
|
+
* - `SELECTION_LIMIT` / `buildRemoteSelectionInsert` (fenced `rel:lines`
|
|
28
|
+
* payload; over the limit the content is dropped and the chip carries the
|
|
29
|
+
* plain path line, so an empty fenced block never occupies the draft).
|
|
30
|
+
*
|
|
31
|
+
* The 0.25.0-era version of this file inserted at the composer's DOM caret and
|
|
32
|
+
* always wrote plain text through `setDraft`; both are gone on purpose — the
|
|
33
|
+
* host publishes no caret for the Lexical composer, and `setDraft` silently
|
|
34
|
+
* destroyed existing chips.
|
|
24
35
|
*
|
|
25
36
|
* Remote adaptations (ours):
|
|
26
37
|
* - `remoteRelativePath`: POSIX-only prefix strip against `remoteRoot`
|
|
27
38
|
* (case-sensitive; outside/traversal refuses with `undefined`, never an
|
|
28
39
|
* absolute mention — the local `workspaceRelativePath` policy).
|
|
29
|
-
* - `insertRemoteReference`: the caller passes `remoteRoot` directly; there
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
* in a local session would name nothing).
|
|
40
|
+
* - `insertRemoteReference`: the caller passes `remoteRoot` directly; there is
|
|
41
|
+
* no workspaces-membership lookup here because the binding gate lives in
|
|
42
|
+
* `use-remote-chat-target.ts` (current session must belong to the viewed
|
|
43
|
+
* binding, else the button is disabled).
|
|
34
44
|
* - `sameLocalDir`: Windows-tolerant alias-dir comparison for that gate.
|
|
35
45
|
*
|
|
36
|
-
* No ctx reads at module scope (explicit `ctx` params only) so every
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
46
|
+
* No ctx reads at module scope (explicit `ctx` params only) so every function
|
|
47
|
+
* degrades to a logged no-op when a web-core service is missing, and the pure
|
|
48
|
+
* pieces stay unit-testable. DOM-only: no Node builtins (the client purity gate
|
|
49
|
+
* forbids them).
|
|
40
50
|
*/
|
|
41
51
|
import type { Context } from '@deepseek-ai/cordis';
|
|
42
|
-
/**
|
|
43
|
-
export interface
|
|
52
|
+
/** One reference chip's footprint in clipboard-projection coordinates. */
|
|
53
|
+
export interface OccurrenceLike {
|
|
54
|
+
/** Start offset in the clipboard projection (same plane as `draft`). */
|
|
55
|
+
offset: number;
|
|
56
|
+
/** Length in the clipboard projection (the chip's whole `clipboardText`). */
|
|
57
|
+
length: number;
|
|
58
|
+
}
|
|
59
|
+
/** Pick-time draft span guarded by the input revision (detect coordinates). */
|
|
60
|
+
export interface TokenSpan {
|
|
44
61
|
start: number;
|
|
45
62
|
end: number;
|
|
63
|
+
draftRev: number;
|
|
64
|
+
}
|
|
65
|
+
/** The host's structured-reference payload (mirror of `ReferenceInsert`). */
|
|
66
|
+
export interface ReferenceInsert {
|
|
67
|
+
/** Trigger source id; `'reference'` for a programmatic insert. */
|
|
68
|
+
source: string;
|
|
69
|
+
/** What the model receives on send — the chip's full serialized form. */
|
|
70
|
+
ref: string;
|
|
71
|
+
/** What the composer shows in place of `ref`. */
|
|
72
|
+
label: string;
|
|
73
|
+
/** Chip icon family; the host picks its own default when omitted. */
|
|
74
|
+
appearance?: 'session' | 'file' | 'folder';
|
|
75
|
+
/** The text this chip occupies in the draft projection. */
|
|
76
|
+
clipboardText: string;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* One reference to insert: the compact `label` the composer shows, and the
|
|
80
|
+
* `text` the model receives on send. The two are deliberately different — a
|
|
81
|
+
* selection reads as `docs/a.md:12-15` on screen while the model still gets the
|
|
82
|
+
* whole fenced block. A plain-text fallback inserts `text` (never the label),
|
|
83
|
+
* so a chip failure still delivers the payload the user meant.
|
|
84
|
+
*/
|
|
85
|
+
export interface PendingReference {
|
|
86
|
+
label: string;
|
|
87
|
+
text: string;
|
|
88
|
+
/** What the draft string carries for this chip; defaults to `label`. */
|
|
89
|
+
clipboardText?: string;
|
|
90
|
+
/** Chip icon family. Omit when `text` is not a path (a selection). */
|
|
91
|
+
appearance?: 'file' | 'folder' | 'session';
|
|
46
92
|
}
|
|
93
|
+
/** Snapshot the insertion logic reads (structural slice of the host's InputState). */
|
|
94
|
+
interface InputSnapshot {
|
|
95
|
+
draft: string;
|
|
96
|
+
draftRev?: number;
|
|
97
|
+
occurrences?: readonly OccurrenceLike[];
|
|
98
|
+
phase?: 'plain' | 'adjudicating' | 'claimed' | 'submitting';
|
|
99
|
+
claim?: {
|
|
100
|
+
readonly token: string;
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
/** `draft` with `text` appended behind one separating space when needed. */
|
|
104
|
+
export declare function appendText(draft: string, text: string): string;
|
|
47
105
|
/**
|
|
48
|
-
*
|
|
49
|
-
*
|
|
106
|
+
* Fold one clipboard-projection offset to its detect-projection twin.
|
|
107
|
+
*
|
|
108
|
+
* An offset inside a chip's expansion snaps to that chip's trailing edge (the
|
|
109
|
+
* host only ever replaces whole chips, never splits one). An offset exactly at
|
|
110
|
+
* a chip's START resolves to the position BEFORE the chip — the host's own
|
|
111
|
+
* `detectOffsetOfClipboardOffset` reads that case as "inside" and would move a
|
|
112
|
+
* caret one chip too far right.
|
|
50
113
|
*/
|
|
51
|
-
export declare function
|
|
114
|
+
export declare function foldClipboardOffset(draft: string, occurrences: readonly OccurrenceLike[], offset: number): number;
|
|
52
115
|
/**
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
* discards stale or wrong-composer reads — a caret must never be applied
|
|
56
|
-
* against a draft it was not measured on.
|
|
116
|
+
* The detect-projection offset that appends at the document end, or
|
|
117
|
+
* `undefined` when it cannot be derived.
|
|
57
118
|
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
119
|
+
* `foldClipboardOffset` alone is not enough: a command chip (`/goal `, present
|
|
120
|
+
* while `phase === 'claimed'`) collapses to one detect character but never
|
|
121
|
+
* appears in `occurrences` — the host only collects REFERENCE chips.
|
|
122
|
+
*
|
|
123
|
+
* CORRECTION over the upstream 0.28.1 formula: the chip's clipboard text is
|
|
124
|
+
* `claim.token.trimEnd()` and the separator is a SEPARATE text node, so the
|
|
125
|
+
* detect projection is `1 (chip) + separator.length + rest`. Upstream folds the
|
|
126
|
+
* whole `claim.token` into that one placeholder, which lands one character
|
|
127
|
+
* short — a selection or `@file` appended while a slash command is claimed then
|
|
128
|
+
* splices INSIDE the command's last argument word (`/goal do it` became
|
|
129
|
+
* `/goal do i <payload>t`). Verified against the installed host:
|
|
130
|
+
* `CommandChipNode.getTextContent()` returns the token without its separator,
|
|
131
|
+
* and `insertCommand` inserts the separator as its own text node.
|
|
61
132
|
*/
|
|
62
|
-
export declare function
|
|
133
|
+
export declare function appendOffset(snapshot: InputSnapshot): number | undefined;
|
|
63
134
|
/**
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
135
|
+
* Mint one reference chip through the host's public `insertReference` facade —
|
|
136
|
+
* the same method the native `@` picker's event listener calls, so the
|
|
137
|
+
* composer shows a compact chip while the model receives `reference.text`.
|
|
138
|
+
* The host runs its own admission-phase and revision guards and reports whether
|
|
139
|
+
* the edit applied.
|
|
140
|
+
*
|
|
141
|
+
* The chip's `clipboardText` is the compact `label` by default, not the full
|
|
142
|
+
* payload: other draft consumers (copy, plain projections) stay short, and the
|
|
143
|
+
* full payload rides in `text` for send.
|
|
144
|
+
*
|
|
145
|
+
* @returns false when the host refused the insert (stale revision, frozen
|
|
146
|
+
* submission phase, or no chip channel) so the caller can fall back.
|
|
72
147
|
*/
|
|
73
|
-
export declare function
|
|
148
|
+
export declare function insertDraftReference(ctx: Context, sessionId: string, reference: PendingReference): boolean;
|
|
74
149
|
/**
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
150
|
+
* Append `text` to the session's composer draft. Returns false — and logs —
|
|
151
|
+
* when the conversation service or the session scope is unavailable.
|
|
152
|
+
*
|
|
153
|
+
* A chipless draft takes the plain `setDraft` path. A draft that CARRIES chips
|
|
154
|
+
* is written through `slash/input-insert-text` at the folded end offset
|
|
155
|
+
* instead: `setDraft` rebuilds the document from a string and flattens every
|
|
156
|
+
* chip, which quietly destroys references the user placed by hand. The same
|
|
157
|
+
* span path covers a claimed `/command`, whose prefix chip is not in
|
|
158
|
+
* `occurrences` (see {@link appendOffset}).
|
|
79
159
|
*/
|
|
80
160
|
export declare function appendToDraft(ctx: Context, sessionId: string, text: string): boolean;
|
|
161
|
+
/**
|
|
162
|
+
* Insert one reference as a chip, degrading to its plain-text form when the
|
|
163
|
+
* host has no chip channel or refuses the insert. This is the ONE place the
|
|
164
|
+
* fallback policy lives: the explorer's `@` button and the viewer selection
|
|
165
|
+
* popup both come through here.
|
|
166
|
+
*/
|
|
167
|
+
export declare function insertReferenceOrText(ctx: Context, sessionId: string, reference: PendingReference): boolean;
|
|
81
168
|
/**
|
|
82
169
|
* The DSH `@` spelling for one relative path, mirroring the host grammar
|
|
83
170
|
* (`formatFileMention` in `@deepseek-ai/dsh-file-reference`): plain when
|
|
@@ -127,24 +214,14 @@ export declare function remoteRelativePath(root: string | undefined, path: strin
|
|
|
127
214
|
export declare function sameLocalDir(left: string, right: string): boolean;
|
|
128
215
|
/**
|
|
129
216
|
* Insert one remote explorer entry into the session's composer draft. Files
|
|
130
|
-
* mint a structured chip like the native `@` picker
|
|
131
|
-
*
|
|
132
|
-
* text so DSH's folder decoration and completion
|
|
133
|
-
* owns the binding gate (the session must belong to
|
|
134
|
-
* see `use-remote-chat-target.ts`); an unrepresentable
|
|
135
|
-
* no-op, never an absolute mention.
|
|
217
|
+
* mint a structured chip exactly like the native `@` picker (same facade the
|
|
218
|
+
* viewer selection popup uses, with a plain-text fallback); directories append
|
|
219
|
+
* the folder mention as plain text so DSH's folder decoration and completion
|
|
220
|
+
* keep working. The caller owns the binding gate (the session must belong to
|
|
221
|
+
* the viewed binding — see `use-remote-chat-target.ts`); an unrepresentable
|
|
222
|
+
* path is a logged no-op, never an absolute mention.
|
|
136
223
|
*/
|
|
137
224
|
export declare function insertRemoteReference(ctx: Context | undefined, sessionId: string, remoteRoot: string, absPath: string, isDir: boolean): boolean;
|
|
138
|
-
/**
|
|
139
|
-
* Insert one FILE reference as a structured chip (like DSH's own `@` picker).
|
|
140
|
-
* The chip displays `@<basename>` but serializes to `@<relative path>` on
|
|
141
|
-
* send, so the reference stays a single link from trigger to basename.
|
|
142
|
-
*
|
|
143
|
-
* Directories keep the plain-text completion behavior (`@dir/`) through
|
|
144
|
-
* `insertRemoteReference`. The rc.1 picker also supports folder chips on
|
|
145
|
-
* explicit selection; its drill action still inserts plain text.
|
|
146
|
-
*/
|
|
147
|
-
export declare function insertFileReference(ctx: Context, sessionId: string, relativePath: string): boolean;
|
|
148
225
|
/** Max inserted selection length (UTF-16 code units, i.e. JS `.length`). */
|
|
149
226
|
export declare const SELECTION_LIMIT = 500;
|
|
150
227
|
/** The source line span a selection maps to (1-based, inclusive). */
|
|
@@ -158,8 +235,14 @@ export interface SelectionLines {
|
|
|
158
235
|
*/
|
|
159
236
|
export declare function remoteHeaderOf(remoteRoot: string, absPath: string, lines?: SelectionLines): string;
|
|
160
237
|
/**
|
|
161
|
-
* The
|
|
162
|
-
*
|
|
163
|
-
*
|
|
238
|
+
* The composer payload for one selection: the chip label and the model text.
|
|
239
|
+
*
|
|
240
|
+
* The chip reads as `rel:start-end` (compact, and it is what the draft
|
|
241
|
+
* projection carries); the model receives the whole fenced block. Over the
|
|
242
|
+
* limit the content is dropped and BOTH halves are the plain path line — an
|
|
243
|
+
* empty fenced block would just occupy the draft. No `appearance` is set: the
|
|
244
|
+
* host can make `'file'` chips openable, and a selection's `text` is not a
|
|
245
|
+
* path, so a click must not hand a fenced block to the file opener.
|
|
164
246
|
*/
|
|
165
|
-
export declare function buildRemoteSelectionInsert(absPath: string, remoteRoot: string, lines: SelectionLines | undefined, selected: string):
|
|
247
|
+
export declare function buildRemoteSelectionInsert(absPath: string, remoteRoot: string, lines: SelectionLines | undefined, selected: string): PendingReference;
|
|
248
|
+
export {};
|
|
@@ -32,30 +32,30 @@
|
|
|
32
32
|
*/
|
|
33
33
|
import { type RefObject } from 'react';
|
|
34
34
|
/** The floating "add to conversation" action: payload + viewport anchor. */
|
|
35
|
-
export interface SelectionPopup {
|
|
36
|
-
insert:
|
|
35
|
+
export interface SelectionPopup<T> {
|
|
36
|
+
insert: T;
|
|
37
37
|
left: number;
|
|
38
38
|
top: number;
|
|
39
39
|
}
|
|
40
|
-
export interface SelectionPopupOptions {
|
|
40
|
+
export interface SelectionPopupOptions<T> {
|
|
41
41
|
/** Commit the payload into the composer draft (button click). */
|
|
42
|
-
onCommit(insert:
|
|
42
|
+
onCommit(insert: T): void;
|
|
43
43
|
/**
|
|
44
44
|
* The DOM surface that must stay on screen for the popup to live: the
|
|
45
45
|
* CodeMirror host. Called lazily (refs are null until the content loads).
|
|
46
46
|
*/
|
|
47
47
|
getSurface(): HTMLElement | null;
|
|
48
48
|
}
|
|
49
|
-
export interface SelectionPopupControls {
|
|
49
|
+
export interface SelectionPopupControls<T> {
|
|
50
50
|
/** The current popup (null = hidden). */
|
|
51
|
-
popup: SelectionPopup | null;
|
|
51
|
+
popup: SelectionPopup<T> | null;
|
|
52
52
|
/** Attach to the portaled button element. */
|
|
53
53
|
buttonRef: RefObject<HTMLButtonElement>;
|
|
54
54
|
/** Anchor the popup above a selection (viewport-clamped). */
|
|
55
|
-
show(insert:
|
|
55
|
+
show(insert: T, left: number, top: number): void;
|
|
56
56
|
/** Hide the popup (idempotent). */
|
|
57
57
|
hide(): void;
|
|
58
58
|
/** The button's click: commit the stored payload, then hide. */
|
|
59
59
|
commit(): void;
|
|
60
60
|
}
|
|
61
|
-
export declare function useSelectionPopup(options: SelectionPopupOptions): SelectionPopupControls
|
|
61
|
+
export declare function useSelectionPopup<T>(options: SelectionPopupOptions<T>): SelectionPopupControls<T>;
|
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.9.
|
|
4
|
+
"version": "0.9.2",
|
|
5
5
|
"icon": "icon.svg",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"dsh",
|
|
@@ -34,7 +34,7 @@ import { shell } from '@codemirror/legacy-modes/mode/shell'
|
|
|
34
34
|
import { properties } from '@codemirror/legacy-modes/mode/properties'
|
|
35
35
|
import { dockerFile } from '@codemirror/legacy-modes/mode/dockerfile'
|
|
36
36
|
import { diff } from '@codemirror/legacy-modes/mode/diff'
|
|
37
|
-
import {
|
|
37
|
+
import { buildRemoteSelectionInsert, insertReferenceOrText, type PendingReference } from './composer.ts'
|
|
38
38
|
import { useSelectionPopup } from './selection-popup.ts'
|
|
39
39
|
import { initialEditorTheme, watchEditorScheme } from './vsc/editor-theme.ts'
|
|
40
40
|
|
|
@@ -243,11 +243,14 @@ export const CodeEditor = forwardRef<CodeEditorHandle, {
|
|
|
243
243
|
* listeners (outside mousedown, Escape, hidden tab/window, surface
|
|
244
244
|
* leaving the viewport) — see selection-popup.ts.
|
|
245
245
|
*/
|
|
246
|
-
const selectionPopup = useSelectionPopup({
|
|
246
|
+
const selectionPopup = useSelectionPopup<PendingReference>({
|
|
247
247
|
onCommit: (insert) => {
|
|
248
248
|
const ctx = ctxRef.current
|
|
249
249
|
const sessionId = targetRef.current.sessionId
|
|
250
|
-
|
|
250
|
+
// The same entry the explorer's @ button uses: mint one structured chip
|
|
251
|
+
// (compact `rel:start-end` label, fenced block to the model), falling back
|
|
252
|
+
// to plain text only when the host refuses the chip channel.
|
|
253
|
+
if (ctx !== undefined && sessionId !== undefined) insertReferenceOrText(ctx, sessionId, insert)
|
|
251
254
|
},
|
|
252
255
|
getSurface: () => hostRef.current,
|
|
253
256
|
})
|