dsh-remote-vs 0.0.0-stage → 0.8.3

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 (200) hide show
  1. package/LICENSE +21 -0
  2. package/README.en.md +385 -0
  3. package/README.md +347 -2
  4. package/cordis.patch.yml +259 -0
  5. package/icon.svg +7 -0
  6. package/lib/client.js +62602 -0
  7. package/lib/index.js +4287 -0
  8. package/lib/p2-bridge-refs.js +10 -0
  9. package/lib/p2-bridge.js +16 -0
  10. package/lib/p2-router-fs.js +252 -0
  11. package/lib/p2-router-refs.js +466 -0
  12. package/lib/p2-router-spill.js +62 -0
  13. package/lib/p2-router-subprocess.js +64 -0
  14. package/lib/p2-shell-bash.js +24 -0
  15. package/lib/ssh-proc-arI5lm4W.js +632 -0
  16. package/lib/types/changes/recorder.d.ts +240 -0
  17. package/lib/types/changes/status.d.ts +201 -0
  18. package/lib/types/changes/text-diff.d.ts +65 -0
  19. package/lib/types/client/CodeEditor.d.ts +125 -0
  20. package/lib/types/client/Dialog.d.ts +14 -0
  21. package/lib/types/client/DirectoryFlow.d.ts +30 -0
  22. package/lib/types/client/EditorPane.d.ts +30 -0
  23. package/lib/types/client/HostsView.d.ts +10 -0
  24. package/lib/types/client/NativeExplorer.d.ts +39 -0
  25. package/lib/types/client/RemoteDirectoryPicker.d.ts +16 -0
  26. package/lib/types/client/RemoteEditorView.d.ts +20 -0
  27. package/lib/types/client/RemoteExplorer.d.ts +18 -0
  28. package/lib/types/client/RemoteRoot.d.ts +16 -0
  29. package/lib/types/client/SearchView.d.ts +14 -0
  30. package/lib/types/client/SettingsHosts.d.ts +15 -0
  31. package/lib/types/client/TerminalView.d.ts +16 -0
  32. package/lib/types/client/WorkspaceWizard.d.ts +16 -0
  33. package/lib/types/client/WorkspacesView.d.ts +14 -0
  34. package/lib/types/client/XTerm.d.ts +15 -0
  35. package/lib/types/client/api.d.ts +162 -0
  36. package/lib/types/client/changes/ChangesDock.d.ts +35 -0
  37. package/lib/types/client/changes/model.d.ts +36 -0
  38. package/lib/types/client/changes/undo-affordance.d.ts +48 -0
  39. package/lib/types/client/changes/undo-context.d.ts +25 -0
  40. package/lib/types/client/composer.d.ts +165 -0
  41. package/lib/types/client/i18n.d.ts +17 -0
  42. package/lib/types/client/index.d.ts +14 -0
  43. package/lib/types/client/remote-file-open-target.d.ts +247 -0
  44. package/lib/types/client/remote-file-open.d.ts +35 -0
  45. package/lib/types/client/reveal.d.ts +15 -0
  46. package/lib/types/client/scm/DiffView.d.ts +24 -0
  47. package/lib/types/client/scm/HistoryPanel.d.ts +12 -0
  48. package/lib/types/client/scm/RepoPicker.d.ts +24 -0
  49. package/lib/types/client/scm/ScmPanel.d.ts +15 -0
  50. package/lib/types/client/scm/actions.d.ts +170 -0
  51. package/lib/types/client/scm/diff-model.d.ts +29 -0
  52. package/lib/types/client/scm/stage-queue.d.ts +99 -0
  53. package/lib/types/client/scm/status-map.d.ts +157 -0
  54. package/lib/types/client/scm/untracked-dirs.d.ts +72 -0
  55. package/lib/types/client/selection-popup.d.ts +61 -0
  56. package/lib/types/client/store.d.ts +437 -0
  57. package/lib/types/client/theme.d.ts +15 -0
  58. package/lib/types/client/ui.d.ts +46 -0
  59. package/lib/types/client/upload-plan.d.ts +78 -0
  60. package/lib/types/client/upload-source.d.ts +46 -0
  61. package/lib/types/client/use-remote-chat-target.d.ts +17 -0
  62. package/lib/types/client/vsc/FindWidget.d.ts +81 -0
  63. package/lib/types/client/vsc/change-marks.d.ts +150 -0
  64. package/lib/types/client/vsc/editor-langs.d.ts +5 -0
  65. package/lib/types/client/vsc/editor-palette.d.ts +37 -0
  66. package/lib/types/client/vsc/editor-theme.d.ts +32 -0
  67. package/lib/types/client/vsc/fallback-highlight.d.ts +26 -0
  68. package/lib/types/client/vsc/kit.d.ts +195 -0
  69. package/lib/types/client/vsc/markdown.d.ts +11 -0
  70. package/lib/types/client/vsc/reveal-line.d.ts +38 -0
  71. package/lib/types/client/vsc/tokenize.d.ts +104 -0
  72. package/lib/types/deploy.d.ts +94 -0
  73. package/lib/types/fs-ops.d.ts +8 -0
  74. package/lib/types/git-format.d.ts +34 -0
  75. package/lib/types/git-ops.d.ts +183 -0
  76. package/lib/types/http.d.ts +14 -0
  77. package/lib/types/importers.d.ts +20 -0
  78. package/lib/types/index.d.ts +87 -0
  79. package/lib/types/keygen.d.ts +19 -0
  80. package/lib/types/loopback.d.ts +7 -0
  81. package/lib/types/p2/bridge-refs.d.ts +21 -0
  82. package/lib/types/p2/bridge.d.ts +24 -0
  83. package/lib/types/p2/file-refs.d.ts +73 -0
  84. package/lib/types/p2/helper-rpc.d.ts +63 -0
  85. package/lib/types/p2/in-flight-gate.d.ts +25 -0
  86. package/lib/types/p2/router-fs.d.ts +92 -0
  87. package/lib/types/p2/router-refs.d.ts +45 -0
  88. package/lib/types/p2/router-spill.d.ts +11 -0
  89. package/lib/types/p2/router-subprocess.d.ts +23 -0
  90. package/lib/types/p2/shell-bash.d.ts +16 -0
  91. package/lib/types/p2/ssh-proc.d.ts +22 -0
  92. package/lib/types/p2/worlds.d.ts +179 -0
  93. package/lib/types/paths.d.ts +12 -0
  94. package/lib/types/pool.d.ts +105 -0
  95. package/lib/types/protocol.d.ts +452 -0
  96. package/lib/types/routes.d.ts +27 -0
  97. package/lib/types/search-ops.d.ts +68 -0
  98. package/lib/types/sftp.d.ts +90 -0
  99. package/lib/types/store.d.ts +66 -0
  100. package/lib/types/terminal.d.ts +51 -0
  101. package/lib/types/timeouts.d.ts +83 -0
  102. package/lib/types/tools.d.ts +41 -0
  103. package/lib/worlds-PUumRIh7.js +18765 -0
  104. package/locale/en.json +6 -0
  105. package/locale/zh.json +6 -0
  106. package/package.json +189 -4
  107. package/src/changes/recorder.ts +902 -0
  108. package/src/changes/status.ts +448 -0
  109. package/src/changes/text-diff.ts +377 -0
  110. package/src/client/CodeEditor.tsx +923 -0
  111. package/src/client/Dialog.tsx +76 -0
  112. package/src/client/DirectoryFlow.tsx +431 -0
  113. package/src/client/EditorPane.tsx +512 -0
  114. package/src/client/HostsView.tsx +312 -0
  115. package/src/client/NativeExplorer.tsx +121 -0
  116. package/src/client/RemoteDirectoryPicker.tsx +157 -0
  117. package/src/client/RemoteEditorView.tsx +54 -0
  118. package/src/client/RemoteExplorer.tsx +1398 -0
  119. package/src/client/RemoteRoot.tsx +389 -0
  120. package/src/client/SearchView.tsx +407 -0
  121. package/src/client/SettingsHosts.tsx +136 -0
  122. package/src/client/TerminalView.tsx +308 -0
  123. package/src/client/WorkspaceWizard.tsx +199 -0
  124. package/src/client/WorkspacesView.tsx +93 -0
  125. package/src/client/XTerm.tsx +308 -0
  126. package/src/client/api.ts +331 -0
  127. package/src/client/changes/ChangesDock.tsx +348 -0
  128. package/src/client/changes/changes.css +230 -0
  129. package/src/client/changes/model.ts +347 -0
  130. package/src/client/changes/undo-affordance.ts +389 -0
  131. package/src/client/changes/undo-context.ts +34 -0
  132. package/src/client/composer.ts +436 -0
  133. package/src/client/css.d.ts +7 -0
  134. package/src/client/i18n.ts +702 -0
  135. package/src/client/index.tsx +286 -0
  136. package/src/client/remote-file-open-target.ts +553 -0
  137. package/src/client/remote-file-open.ts +261 -0
  138. package/src/client/reveal.ts +33 -0
  139. package/src/client/scm/DiffView.tsx +570 -0
  140. package/src/client/scm/HistoryPanel.tsx +353 -0
  141. package/src/client/scm/RepoPicker.tsx +238 -0
  142. package/src/client/scm/ScmPanel.tsx +857 -0
  143. package/src/client/scm/actions.ts +460 -0
  144. package/src/client/scm/diff-model.ts +206 -0
  145. package/src/client/scm/stage-queue.ts +264 -0
  146. package/src/client/scm/status-map.ts +381 -0
  147. package/src/client/scm/untracked-dirs.ts +139 -0
  148. package/src/client/selection-popup.ts +157 -0
  149. package/src/client/store.tsx +1670 -0
  150. package/src/client/theme.ts +54 -0
  151. package/src/client/ui.tsx +136 -0
  152. package/src/client/upload-plan.ts +153 -0
  153. package/src/client/upload-source.ts +145 -0
  154. package/src/client/use-remote-chat-target.ts +153 -0
  155. package/src/client/vsc/FindWidget.tsx +446 -0
  156. package/src/client/vsc/change-marks.ts +586 -0
  157. package/src/client/vsc/editor-langs.ts +137 -0
  158. package/src/client/vsc/editor-palette.ts +107 -0
  159. package/src/client/vsc/editor-theme.ts +93 -0
  160. package/src/client/vsc/fallback-highlight.ts +145 -0
  161. package/src/client/vsc/kit.tsx +777 -0
  162. package/src/client/vsc/markdown.tsx +172 -0
  163. package/src/client/vsc/reveal-line.ts +142 -0
  164. package/src/client/vsc/tokenize.ts +569 -0
  165. package/src/client/vsc/vsc.css +2669 -0
  166. package/src/deploy.ts +481 -0
  167. package/src/fs-ops.ts +57 -0
  168. package/src/git-format.ts +205 -0
  169. package/src/git-ops.ts +693 -0
  170. package/src/http.ts +55 -0
  171. package/src/importers.ts +141 -0
  172. package/src/index.ts +442 -0
  173. package/src/keygen.ts +77 -0
  174. package/src/loopback.ts +47 -0
  175. package/src/p2/bridge-refs.ts +27 -0
  176. package/src/p2/bridge.ts +32 -0
  177. package/src/p2/file-refs.ts +392 -0
  178. package/src/p2/helper-rpc.ts +394 -0
  179. package/src/p2/in-flight-gate.ts +63 -0
  180. package/src/p2/router-fs.ts +327 -0
  181. package/src/p2/router-refs.ts +166 -0
  182. package/src/p2/router-spill.ts +75 -0
  183. package/src/p2/router-subprocess.ts +92 -0
  184. package/src/p2/shell-bash.ts +30 -0
  185. package/src/p2/ssh-proc.ts +802 -0
  186. package/src/p2/worlds.ts +735 -0
  187. package/src/paths.ts +50 -0
  188. package/src/pool.ts +811 -0
  189. package/src/protocol.ts +460 -0
  190. package/src/routes.ts +802 -0
  191. package/src/search-ops.ts +220 -0
  192. package/src/sftp.ts +677 -0
  193. package/src/store.ts +364 -0
  194. package/src/terminal.ts +291 -0
  195. package/src/timeouts.ts +122 -0
  196. package/src/tools.ts +320 -0
  197. package/vendor/dsh-remote-helpers/dsh-ssh-helper-0.2.1-alpha.2-linux-x64.helper.json +16 -0
  198. package/vendor/dsh-remote-helpers/dsh-ssh-helper-0.2.1-alpha.2-linux-x64.tar.gz +4 -0
  199. package/vendor/dsh-remote-helpers/dsh-ssh-helper-0.2.1-alpha.2-linux-x64.tar.gz.sha256 +1 -0
  200. package/vendor/dsh-remote-helpers/glibc-ceiling.patch +31 -0
@@ -0,0 +1,436 @@
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.
10
+ *
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),
19
+ * - `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).
24
+ *
25
+ * Remote adaptations (ours):
26
+ * - `remoteRelativePath`: POSIX-only prefix strip against `remoteRoot`
27
+ * (case-sensitive; outside/traversal refuses with `undefined`, never an
28
+ * 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).
34
+ * - `sameLocalDir`: Windows-tolerant alias-dir comparison for that gate.
35
+ *
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).
40
+ */
41
+ import type { Context } from '@deepseek-ai/cordis'
42
+
43
+ /** Structural slice of the web-core conversation service we touch. */
44
+ interface SessionInputLike {
45
+ state: { getSnapshot(): { draft: string; draftRev?: number } }
46
+ setDraft(text: string): void
47
+ }
48
+
49
+ interface ConversationLike {
50
+ input: { for(actx: unknown): SessionInputLike }
51
+ }
52
+
53
+ function conversationOf(ctx: Context): ConversationLike | undefined {
54
+ try {
55
+ const raw = (ctx as unknown as { get(name: string): unknown }).get('conversation')
56
+ if (typeof raw !== 'object' || raw === null) return undefined
57
+ const input = (raw as { input?: unknown }).input as { for?: unknown } | undefined
58
+ if (typeof input?.for !== 'function') return undefined
59
+ return raw as ConversationLike
60
+ } catch {
61
+ return undefined
62
+ }
63
+ }
64
+
65
+ function sessionScope(ctx: Context, sessionId: string): unknown {
66
+ // Soft read ONLY: direct `ctx.sessions` property access goes through the
67
+ // cordis proxy trap and throws `cannot get property "sessions" without
68
+ // inject` (our client inject is ['slots', 'uiWorkspace'] by design).
69
+ // `ctx.get` is explicitly "without the inject requirement".
70
+ try {
71
+ const sessions = (ctx as unknown as { get(name: string): unknown }).get('sessions') as
72
+ | { scope?(id: string): unknown }
73
+ | undefined
74
+ return typeof sessions?.scope === 'function' ? sessions.scope(sessionId) : undefined
75
+ } catch {
76
+ return undefined
77
+ }
78
+ }
79
+
80
+ /** A resolved composer caret/selection in draft coordinates. */
81
+ export interface DraftCaret {
82
+ start: number
83
+ end: number
84
+ }
85
+
86
+ /**
87
+ * The spliced draft plus the caret index (in that draft) right after the
88
+ * inserted text — the left-side separating space shifts the caret by one,
89
+ * which naive `start + text.length` misses (it would land before the tail).
90
+ */
91
+ interface SpliceResult {
92
+ draft: string
93
+ caretAfter: number
94
+ }
95
+
96
+ /**
97
+ * Splice `text` into `draft` at `caret` (replacing any live selection) with
98
+ * whitespace-aware joins and report the caret position right after the
99
+ * inserted text. `caret === null` (position unknown) appends at the end,
100
+ * exactly like the original behavior.
101
+ */
102
+ function spliceInsert(draft: string, text: string, caret: DraftCaret | null): SpliceResult {
103
+ if (caret === null || draft === '') {
104
+ const next = draft.trim() === '' ? text : `${draft}${/\s$/.test(draft) ? '' : ' '}${text}`
105
+ return { draft: next, caretAfter: next.length }
106
+ }
107
+ const prefix = draft.slice(0, caret.start)
108
+ const suffix = draft.slice(caret.end)
109
+ if (prefix === '' && suffix === '') return { draft: text, caretAfter: text.length }
110
+ // One separating space, but never doubled against adjacent whitespace
111
+ // (or the string edges) — mirrors how typing in the middle of a sentence
112
+ // behaves.
113
+ const left = prefix === '' || /\s$/.test(prefix) ? '' : ' '
114
+ const right = suffix === '' || /^\s/.test(suffix) || /\s$/.test(text) ? '' : ' '
115
+ return {
116
+ draft: `${prefix}${left}${text}${right}${suffix}`,
117
+ caretAfter: prefix.length + left.length + text.length,
118
+ }
119
+ }
120
+
121
+ /**
122
+ * The spliced draft string (see {@link spliceInsert}); pure string math —
123
+ * unit-tested directly.
124
+ */
125
+ export function insertAtCaret(draft: string, text: string, caret: DraftCaret | null): string {
126
+ return spliceInsert(draft, text, caret).draft
127
+ }
128
+
129
+ /**
130
+ * Locate the composer `<textarea>` in the conversation column: prefer the
131
+ * `data-phase`-tagged textarea (the composer's marker), falling back to any
132
+ * textarea in the column, then to a bare data-phase textarea (older host
133
+ * layouts without the column attribute). Null in jsdom-less hosts — and on
134
+ * DSH 0.2.1, whose composer is a Lexical contenteditable (the probe then
135
+ * misses and callers append at the end, matching the local explorer).
136
+ */
137
+ function findComposerTextarea(): HTMLTextAreaElement | null {
138
+ if (typeof document === 'undefined') return null
139
+ const column = document.querySelector('#root [data-slot="conversation"]')
140
+ const find = (scope: ParentNode): HTMLTextAreaElement | null =>
141
+ scope.querySelector('textarea[data-phase]') ?? scope.querySelector('textarea')
142
+ return column !== null
143
+ ? find(column)
144
+ : document.querySelector<HTMLTextAreaElement>('textarea[data-phase]')
145
+ }
146
+
147
+ /**
148
+ * Resolve the composer's live caret from its DOM `<textarea>`. The draft
149
+ * store has no caret API, so the value-sync check (`el.value === draft`)
150
+ * discards stale or wrong-composer reads — a caret must never be applied
151
+ * against a draft it was not measured on.
152
+ *
153
+ * Returns null when the composer is missing, disabled/read-only, out of
154
+ * sync with the store draft, or has no measurable selection (jsdom/odd
155
+ * hosts report null selectionStart/End).
156
+ */
157
+ export function probeComposerCaret(draft: string): DraftCaret | null {
158
+ const el = findComposerTextarea()
159
+ if (el === null || el.disabled || el.readOnly) return null
160
+ if (el.value !== draft) return null
161
+ let start = el.selectionStart
162
+ let end = el.selectionEnd
163
+ if (typeof start !== 'number' || typeof end !== 'number') return null
164
+ if (!Number.isFinite(start) || !Number.isFinite(end)) return null
165
+ start = Math.max(0, Math.min(start, draft.length))
166
+ end = Math.max(start, Math.min(end, draft.length))
167
+ return { start, end }
168
+ }
169
+
170
+ /**
171
+ * Restore the composer caret to `caretIndex` after a programmatic
172
+ * `setDraft` commit. A controlled textarea update resets the caret (React
173
+ * commits the value asynchronously and the browser moves the caret to the
174
+ * start/end), so the placement is scheduled and retried across at most two
175
+ * animation frames (setTimeout fallback for jsdom), and only applied when
176
+ * the textarea still matches `expectedDraft` — a newer edit or a different
177
+ * composer wins the race untouched. The caret is clamped into the value
178
+ * bounds, mirroring how browsers clamp type-in positions.
179
+ */
180
+ export function placeComposerCaretAfterInsert(expectedDraft: string, caretIndex: number): void {
181
+ let remaining = 2
182
+ let scheduled = false
183
+ const schedule = (fn: () => void): void => {
184
+ if (scheduled) return
185
+ scheduled = true
186
+ if (typeof requestAnimationFrame === 'function') requestAnimationFrame(fn)
187
+ else setTimeout(fn, 0)
188
+ }
189
+ const place = (): void => {
190
+ scheduled = false
191
+ if (remaining <= 0) return
192
+ remaining -= 1
193
+ const el = findComposerTextarea()
194
+ if (el === null || el.disabled || el.readOnly) return
195
+ if (el.value !== expectedDraft) {
196
+ // The commit has not landed yet (or a competing edit won) — one more
197
+ // frame before giving up.
198
+ schedule(place)
199
+ return
200
+ }
201
+ const clamped = Math.max(0, Math.min(caretIndex, el.value.length))
202
+ el.setSelectionRange(clamped, clamped)
203
+ }
204
+ schedule(place)
205
+ }
206
+
207
+ /**
208
+ * Insert `text` into the session's composer draft at the composer's live
209
+ * caret (see {@link probeComposerCaret}), falling back to appending at the
210
+ * end when the caret cannot be resolved. Returns false — and logs — when the
211
+ * conversation service or the session scope is unavailable.
212
+ */
213
+ export function appendToDraft(ctx: Context, sessionId: string, text: string): boolean {
214
+ try {
215
+ const actx = sessionScope(ctx, sessionId)
216
+ if (actx === undefined) {
217
+ console.warn('[dsh-remote-vs] draft insert skipped: no session scope', sessionId)
218
+ return false
219
+ }
220
+ const conversation = conversationOf(ctx)
221
+ if (conversation === undefined) {
222
+ console.warn('[dsh-remote-vs] draft insert skipped: conversation service unavailable')
223
+ return false
224
+ }
225
+ const input = conversation.input.for(actx)
226
+ const draft = input.state.getSnapshot().draft
227
+ const caret = probeComposerCaret(draft)
228
+ const { draft: next, caretAfter } = spliceInsert(draft, text, caret)
229
+ input.setDraft(next)
230
+ // Put the caret right after the inserted text once the value commit
231
+ // lands — see the module doc for why this keeps stacked inserts at the
232
+ // running position.
233
+ placeComposerCaretAfterInsert(next, caretAfter)
234
+ return true
235
+ } catch (error) {
236
+ console.warn('[dsh-remote-vs] draft insert failed:', error)
237
+ return false
238
+ }
239
+ }
240
+
241
+ /**
242
+ * The DSH `@` spelling for one relative path, mirroring the host grammar
243
+ * (`formatFileMention` in `@deepseek-ai/dsh-file-reference`): plain when
244
+ * there is no whitespace, quoted when there is, and `undefined` when the
245
+ * path contains a control character or an embedded quote the editor grammar
246
+ * cannot represent.
247
+ *
248
+ * Files and folders share this one constructor so the quoting rule cannot
249
+ * drift between them. A folder spells the trailing slash *inside* the quotes
250
+ * (`@"my dir/"`, `@docs/`): the host's folder token matcher
251
+ * (`FOLDER_REF_RE` in `dsh-client-ui-conversation`) only accepts a quoted
252
+ * directory once the quote closes, and the message bubble's tokenizer
253
+ * (`@"[^"\n]+"` in `dsh-client-ui-primitives`) only classifies a closed
254
+ * quoted token as a folder — unquoted `@my dir/` decorates as a file chip
255
+ * covering just `my`. The host's own picker emits the *open* form
256
+ * (`@"my dir/`) while the user is still descending through completion; a
257
+ * programmatic reference is a finished mention, so it closes the quote.
258
+ */
259
+ export function mentionFor(
260
+ relativePath: string,
261
+ kind: 'file' | 'folder',
262
+ ): { mention: string; label: string } | undefined {
263
+ const path = relativePath.replace(/[\\/]+$/, '')
264
+ // eslint-disable-next-line no-control-regex -- rejecting control characters is the point of this guard
265
+ if (/[\u0000-\u001f\u007f-\u009f"]/u.test(path)) return undefined
266
+ const target = kind === 'folder' ? `${path}/` : path
267
+ const mention = /\s/u.test(target) ? `@"${target}"` : `@${target}`
268
+ const at = Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\'))
269
+ const label = at === -1 ? path : path.slice(at + 1)
270
+ return { mention, label }
271
+ }
272
+
273
+ /**
274
+ * A complete directory token (`@docs/`, `@"my dir/"`): the explorer inserts a
275
+ * finished mention, so the trailing slash sits inside the closing quote.
276
+ * Separators normalize to '/' first (the fs-tree spelling); the spelling
277
+ * itself is {@link mentionFor}'s single job, folder kind — this helper adds no
278
+ * quoting rule of its own.
279
+ */
280
+ export function directoryMention(relativePath: string): string | undefined {
281
+ return mentionFor(relativePath.replace(/\\/g, '/'), 'folder')?.mention
282
+ }
283
+
284
+ /**
285
+ * Project one absolute POSIX remote path against the binding's remote root.
286
+ * The remote twin of better-sidebar's `workspaceRelativePath`: unknown roots,
287
+ * traversal components and paths outside the root are refused with
288
+ * `undefined` — a mention must never fall back to an absolute token.
289
+ * Case-sensitive (remote hosts are POSIX); both inputs must be absolute.
290
+ */
291
+ export function remoteRelativePath(root: string | undefined, path: string): string | undefined {
292
+ if (root === undefined || !root.startsWith('/') || !path.startsWith('/')) return undefined
293
+ const strip = (value: string): string => (value === '/' ? value : value.replace(/\/+$/, ''))
294
+ const base = strip(root)
295
+ const target = strip(path)
296
+ if ([base, target].some(value => value.split('/').some(part => part === '.' || part === '..'))) return undefined
297
+ if (target === base) return '.'
298
+ if (!target.startsWith(`${base === '/' ? '' : base}/`)) return undefined
299
+ const relative = base === '/' ? target.slice(1) : target.slice(base.length + 1)
300
+ return relative === '' || relative.startsWith('/') ? undefined : relative
301
+ }
302
+
303
+ /**
304
+ * Whether two Host-local directory paths name the same directory. The
305
+ * session↔binding gate compares the workspaces snapshot path (canonical
306
+ * host dir) against our `aliasPaths` entry; both spellings may differ in
307
+ * separators, trailing slashes and — on Windows/macOS — letter case, so all
308
+ * three normalize before comparison. Alias ids are hex, so lowercasing is
309
+ * lossless on every platform DSH runs on.
310
+ */
311
+ export function sameLocalDir(left: string, right: string): boolean {
312
+ const norm = (value: string): string => value.replace(/\\/g, '/').replace(/\/+$/, '').toLowerCase()
313
+ return norm(left) === norm(right)
314
+ }
315
+
316
+ /**
317
+ * Insert one remote explorer entry into the session's composer draft. Files
318
+ * mint a structured chip like the native `@` picker
319
+ * (`insertFileReference`); directories append the folder mention as plain
320
+ * text so DSH's folder decoration and completion keep working. The caller
321
+ * owns the binding gate (the session must belong to the viewed binding —
322
+ * see `use-remote-chat-target.ts`); an unrepresentable path is a logged
323
+ * no-op, never an absolute mention.
324
+ */
325
+ export function insertRemoteReference(
326
+ ctx: Context | undefined,
327
+ sessionId: string,
328
+ remoteRoot: string,
329
+ absPath: string,
330
+ isDir: boolean,
331
+ ): boolean {
332
+ // The shared explorer store boots asynchronously: before its context lands
333
+ // the insertion is a no-op rather than a crash.
334
+ if (ctx === undefined) return false
335
+ try {
336
+ const relative = remoteRelativePath(remoteRoot, absPath)
337
+ if (relative === undefined) {
338
+ console.warn('[dsh-remote-vs] reference insert skipped: path outside the remote workspace root')
339
+ return false
340
+ }
341
+ const mention = isDir ? directoryMention(relative) : mentionFor(relative, 'file')?.mention
342
+ if (mention === undefined) {
343
+ console.warn('[dsh-remote-vs] reference insert skipped: path cannot be represented as a mention')
344
+ return false
345
+ }
346
+ if (!isDir && insertFileReference(ctx, sessionId, relative)) return true
347
+ // The host adds space after a file chip, but not before it. Complete
348
+ // directory inserts need their own separator for the next @ click.
349
+ return appendToDraft(ctx, sessionId, isDir ? `${mention} ` : mention)
350
+ } catch (error) {
351
+ console.warn('[dsh-remote-vs] remote-reference insert failed:', error)
352
+ return false
353
+ }
354
+ }
355
+
356
+ /**
357
+ * Insert one FILE reference as a structured chip (like DSH's own `@` picker).
358
+ * The chip displays `@<basename>` but serializes to `@<relative path>` on
359
+ * send, so the reference stays a single link from trigger to basename.
360
+ *
361
+ * Directories keep the plain-text completion behavior (`@dir/`) through
362
+ * `insertRemoteReference`. The rc.1 picker also supports folder chips on
363
+ * explicit selection; its drill action still inserts plain text.
364
+ */
365
+ export function insertFileReference(ctx: Context, sessionId: string, relativePath: string): boolean {
366
+ const reference = mentionFor(relativePath, 'file')
367
+ if (reference === undefined) return false
368
+ try {
369
+ const actx = sessionScope(ctx, sessionId)
370
+ if (actx === undefined) return false
371
+ const conversation = conversationOf(ctx)
372
+ if (conversation === undefined) return false
373
+ const input = conversation.input.for(actx)
374
+ const before = input.state.getSnapshot()
375
+ if (before.draftRev === undefined) return false
376
+ // The session-scope Context's typed `emit` is keyed to DSH's closed event
377
+ // map; this internal composer event is deliberately string-loose at runtime.
378
+ // Verified present in DSH 0.2.1-alpha.2 (dsh-client-ui-conversation
379
+ // `input.d.ts`: 'slash/input-insert-reference').
380
+ ;(actx as unknown as { emit(name: string, payload: unknown): void }).emit('slash/input-insert-reference', {
381
+ reference: {
382
+ source: 'reference',
383
+ ref: reference.mention,
384
+ label: reference.label,
385
+ appearance: 'file',
386
+ clipboardText: reference.mention,
387
+ },
388
+ span: {
389
+ draftRev: before.draftRev,
390
+ start: before.draft.length,
391
+ end: before.draft.length,
392
+ },
393
+ })
394
+ const after = input.state.getSnapshot()
395
+ return after.draftRev !== before.draftRev
396
+ } catch (error) {
397
+ console.warn('[dsh-remote-vs] file-reference insert failed:', error)
398
+ return false
399
+ }
400
+ }
401
+
402
+ /** Max inserted selection length (UTF-16 code units, i.e. JS `.length`). */
403
+ export const SELECTION_LIMIT = 500
404
+
405
+ /** The source line span a selection maps to (1-based, inclusive). */
406
+ export interface SelectionLines {
407
+ start: number
408
+ end: number
409
+ }
410
+
411
+ /**
412
+ * The fence info line: `rel[:start[-end]]` — lines are omitted entirely
413
+ * when unknown.
414
+ */
415
+ export function remoteHeaderOf(remoteRoot: string, absPath: string, lines?: SelectionLines): string {
416
+ const rel = remoteRelativePath(remoteRoot, absPath) ?? absPath
417
+ if (lines === undefined) return rel
418
+ if (lines.end > lines.start) return `${rel}:${lines.start}-${lines.end}`
419
+ return `${rel}:${lines.start}`
420
+ }
421
+
422
+ /**
423
+ * The full text appended to the composer draft for one selection.
424
+ * Over the limit the content is dropped: the plain path line is the whole
425
+ * payload (an empty fenced block would just occupy the draft).
426
+ */
427
+ export function buildRemoteSelectionInsert(
428
+ absPath: string,
429
+ remoteRoot: string,
430
+ lines: SelectionLines | undefined,
431
+ selected: string,
432
+ ): string {
433
+ const header = remoteHeaderOf(remoteRoot, absPath, lines)
434
+ if (selected.length > SELECTION_LIMIT) return header
435
+ return `\`\`\`${header}\n${selected}\n\`\`\``
436
+ }
@@ -0,0 +1,7 @@
1
+ declare module '*.css' {
2
+ const content: string
3
+ export default content
4
+ }
5
+
6
+ /** Injected by tsdown (`define.__RVS_BUILD__`): a MMDD-HHmm build stamp. */
7
+ declare const __RVS_BUILD__: string