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.
@@ -1,83 +1,170 @@
1
1
  /**
2
- * Remote `@file` insertion into the current session's composer draft: the
3
- * far-side twin of dsh-better-sidebar's `conversation-draft.ts` /
4
- * `selection-payload.ts` (logic vendored, see below). The stock local flow
5
- * projects explorer rows against the host-confirmed workspace root; remote
6
- * rows are POSIX absolutes on another machine, so they project against the
7
- * binding's `remotePath` instead — the same workspace-relative spelling the
8
- * P3 `TransparentFileReferences` router completes, and the `read` tool
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 pieces (behavior-identical to better-sidebar 0.25.0):
12
- * - `spliceInsert` / `insertAtCaret` (whitespace-aware caret splice),
13
- * - `findComposerTextarea` / `probeComposerCaret` /
14
- * `placeComposerCaretAfterInsert` (DOM caret probe; on DSH 0.2.1 the
15
- * composer is a Lexical editor, so the probe misses and inserts append at
16
- * the end — exactly what the local explorer does there),
17
- * - `appendToDraft` (draft read + `setDraft` write through
18
- * `ctx.get('conversation')`, missing service degrades to a logged no-op),
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
- * - `insertFileReference` (`slash/input-insert-reference`, verified present
21
- * in DSH 0.2.1-alpha.2's dsh-client-ui-conversation contract),
22
- * - `SELECTION_LIMIT` / `buildRemoteSelectionInsert` (the fenced
23
- * `rel:lines` payload shape).
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
- * is no workspaces-membership lookup here because the binding gate lives
31
- * in `use-remote-chat-target.ts` (current session must belong to the
32
- * viewed binding, else the button is disabled — a remote-relative mention
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
- * function degrades to a logged no-op when a web-core service is missing,
38
- * and the pure pieces stay unit-testable. DOM-only: no Node builtins (the
39
- * client purity gate forbids them).
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
- /** A resolved composer caret/selection in draft coordinates. */
43
- export interface DraftCaret {
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
- * The spliced draft string (see {@link spliceInsert}); pure string math —
49
- * unit-tested directly.
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 insertAtCaret(draft: string, text: string, caret: DraftCaret | null): string;
114
+ export declare function foldClipboardOffset(draft: string, occurrences: readonly OccurrenceLike[], offset: number): number;
52
115
  /**
53
- * Resolve the composer's live caret from its DOM `<textarea>`. The draft
54
- * store has no caret API, so the value-sync check (`el.value === draft`)
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
- * Returns null when the composer is missing, disabled/read-only, out of
59
- * sync with the store draft, or has no measurable selection (jsdom/odd
60
- * hosts report null selectionStart/End).
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 probeComposerCaret(draft: string): DraftCaret | null;
133
+ export declare function appendOffset(snapshot: InputSnapshot): number | undefined;
63
134
  /**
64
- * Restore the composer caret to `caretIndex` after a programmatic
65
- * `setDraft` commit. A controlled textarea update resets the caret (React
66
- * commits the value asynchronously and the browser moves the caret to the
67
- * start/end), so the placement is scheduled and retried across at most two
68
- * animation frames (setTimeout fallback for jsdom), and only applied when
69
- * the textarea still matches `expectedDraft` — a newer edit or a different
70
- * composer wins the race untouched. The caret is clamped into the value
71
- * bounds, mirroring how browsers clamp type-in positions.
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 placeComposerCaretAfterInsert(expectedDraft: string, caretIndex: number): void;
148
+ export declare function insertDraftReference(ctx: Context, sessionId: string, reference: PendingReference): boolean;
74
149
  /**
75
- * Insert `text` into the session's composer draft at the composer's live
76
- * caret (see {@link probeComposerCaret}), falling back to appending at the
77
- * end when the caret cannot be resolved. Returns false — and logs — when the
78
- * conversation service or the session scope is unavailable.
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
- * (`insertFileReference`); directories append the folder mention as plain
132
- * text so DSH's folder decoration and completion keep working. The caller
133
- * owns the binding gate (the session must belong to the viewed binding —
134
- * see `use-remote-chat-target.ts`); an unrepresentable path is a logged
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 full text appended to the composer draft for one selection.
162
- * Over the limit the content is dropped: the plain path line is the whole
163
- * payload (an empty fenced block would just occupy the draft).
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): 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: string;
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: string): void;
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: string, left: number, top: number): void;
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.1",
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 { appendToDraft, buildRemoteSelectionInsert } from './composer.ts'
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
- if (ctx !== undefined && sessionId !== undefined) appendToDraft(ctx, sessionId, insert)
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
  })