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.
- package/LICENSE +21 -0
- package/README.en.md +385 -0
- package/README.md +347 -2
- package/cordis.patch.yml +259 -0
- package/icon.svg +7 -0
- package/lib/client.js +62602 -0
- package/lib/index.js +4287 -0
- package/lib/p2-bridge-refs.js +10 -0
- package/lib/p2-bridge.js +16 -0
- package/lib/p2-router-fs.js +252 -0
- package/lib/p2-router-refs.js +466 -0
- package/lib/p2-router-spill.js +62 -0
- package/lib/p2-router-subprocess.js +64 -0
- package/lib/p2-shell-bash.js +24 -0
- package/lib/ssh-proc-arI5lm4W.js +632 -0
- package/lib/types/changes/recorder.d.ts +240 -0
- package/lib/types/changes/status.d.ts +201 -0
- package/lib/types/changes/text-diff.d.ts +65 -0
- package/lib/types/client/CodeEditor.d.ts +125 -0
- package/lib/types/client/Dialog.d.ts +14 -0
- package/lib/types/client/DirectoryFlow.d.ts +30 -0
- package/lib/types/client/EditorPane.d.ts +30 -0
- package/lib/types/client/HostsView.d.ts +10 -0
- package/lib/types/client/NativeExplorer.d.ts +39 -0
- package/lib/types/client/RemoteDirectoryPicker.d.ts +16 -0
- package/lib/types/client/RemoteEditorView.d.ts +20 -0
- package/lib/types/client/RemoteExplorer.d.ts +18 -0
- package/lib/types/client/RemoteRoot.d.ts +16 -0
- package/lib/types/client/SearchView.d.ts +14 -0
- package/lib/types/client/SettingsHosts.d.ts +15 -0
- package/lib/types/client/TerminalView.d.ts +16 -0
- package/lib/types/client/WorkspaceWizard.d.ts +16 -0
- package/lib/types/client/WorkspacesView.d.ts +14 -0
- package/lib/types/client/XTerm.d.ts +15 -0
- package/lib/types/client/api.d.ts +162 -0
- package/lib/types/client/changes/ChangesDock.d.ts +35 -0
- package/lib/types/client/changes/model.d.ts +36 -0
- package/lib/types/client/changes/undo-affordance.d.ts +48 -0
- package/lib/types/client/changes/undo-context.d.ts +25 -0
- package/lib/types/client/composer.d.ts +165 -0
- package/lib/types/client/i18n.d.ts +17 -0
- package/lib/types/client/index.d.ts +14 -0
- package/lib/types/client/remote-file-open-target.d.ts +247 -0
- package/lib/types/client/remote-file-open.d.ts +35 -0
- package/lib/types/client/reveal.d.ts +15 -0
- package/lib/types/client/scm/DiffView.d.ts +24 -0
- package/lib/types/client/scm/HistoryPanel.d.ts +12 -0
- package/lib/types/client/scm/RepoPicker.d.ts +24 -0
- package/lib/types/client/scm/ScmPanel.d.ts +15 -0
- package/lib/types/client/scm/actions.d.ts +170 -0
- package/lib/types/client/scm/diff-model.d.ts +29 -0
- package/lib/types/client/scm/stage-queue.d.ts +99 -0
- package/lib/types/client/scm/status-map.d.ts +157 -0
- package/lib/types/client/scm/untracked-dirs.d.ts +72 -0
- package/lib/types/client/selection-popup.d.ts +61 -0
- package/lib/types/client/store.d.ts +437 -0
- package/lib/types/client/theme.d.ts +15 -0
- package/lib/types/client/ui.d.ts +46 -0
- package/lib/types/client/upload-plan.d.ts +78 -0
- package/lib/types/client/upload-source.d.ts +46 -0
- package/lib/types/client/use-remote-chat-target.d.ts +17 -0
- package/lib/types/client/vsc/FindWidget.d.ts +81 -0
- package/lib/types/client/vsc/change-marks.d.ts +150 -0
- package/lib/types/client/vsc/editor-langs.d.ts +5 -0
- package/lib/types/client/vsc/editor-palette.d.ts +37 -0
- package/lib/types/client/vsc/editor-theme.d.ts +32 -0
- package/lib/types/client/vsc/fallback-highlight.d.ts +26 -0
- package/lib/types/client/vsc/kit.d.ts +195 -0
- package/lib/types/client/vsc/markdown.d.ts +11 -0
- package/lib/types/client/vsc/reveal-line.d.ts +38 -0
- package/lib/types/client/vsc/tokenize.d.ts +104 -0
- package/lib/types/deploy.d.ts +94 -0
- package/lib/types/fs-ops.d.ts +8 -0
- package/lib/types/git-format.d.ts +34 -0
- package/lib/types/git-ops.d.ts +183 -0
- package/lib/types/http.d.ts +14 -0
- package/lib/types/importers.d.ts +20 -0
- package/lib/types/index.d.ts +87 -0
- package/lib/types/keygen.d.ts +19 -0
- package/lib/types/loopback.d.ts +7 -0
- package/lib/types/p2/bridge-refs.d.ts +21 -0
- package/lib/types/p2/bridge.d.ts +24 -0
- package/lib/types/p2/file-refs.d.ts +73 -0
- package/lib/types/p2/helper-rpc.d.ts +63 -0
- package/lib/types/p2/in-flight-gate.d.ts +25 -0
- package/lib/types/p2/router-fs.d.ts +92 -0
- package/lib/types/p2/router-refs.d.ts +45 -0
- package/lib/types/p2/router-spill.d.ts +11 -0
- package/lib/types/p2/router-subprocess.d.ts +23 -0
- package/lib/types/p2/shell-bash.d.ts +16 -0
- package/lib/types/p2/ssh-proc.d.ts +22 -0
- package/lib/types/p2/worlds.d.ts +179 -0
- package/lib/types/paths.d.ts +12 -0
- package/lib/types/pool.d.ts +105 -0
- package/lib/types/protocol.d.ts +452 -0
- package/lib/types/routes.d.ts +27 -0
- package/lib/types/search-ops.d.ts +68 -0
- package/lib/types/sftp.d.ts +90 -0
- package/lib/types/store.d.ts +66 -0
- package/lib/types/terminal.d.ts +51 -0
- package/lib/types/timeouts.d.ts +83 -0
- package/lib/types/tools.d.ts +41 -0
- package/lib/worlds-PUumRIh7.js +18765 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +189 -4
- package/src/changes/recorder.ts +902 -0
- package/src/changes/status.ts +448 -0
- package/src/changes/text-diff.ts +377 -0
- package/src/client/CodeEditor.tsx +923 -0
- package/src/client/Dialog.tsx +76 -0
- package/src/client/DirectoryFlow.tsx +431 -0
- package/src/client/EditorPane.tsx +512 -0
- package/src/client/HostsView.tsx +312 -0
- package/src/client/NativeExplorer.tsx +121 -0
- package/src/client/RemoteDirectoryPicker.tsx +157 -0
- package/src/client/RemoteEditorView.tsx +54 -0
- package/src/client/RemoteExplorer.tsx +1398 -0
- package/src/client/RemoteRoot.tsx +389 -0
- package/src/client/SearchView.tsx +407 -0
- package/src/client/SettingsHosts.tsx +136 -0
- package/src/client/TerminalView.tsx +308 -0
- package/src/client/WorkspaceWizard.tsx +199 -0
- package/src/client/WorkspacesView.tsx +93 -0
- package/src/client/XTerm.tsx +308 -0
- package/src/client/api.ts +331 -0
- package/src/client/changes/ChangesDock.tsx +348 -0
- package/src/client/changes/changes.css +230 -0
- package/src/client/changes/model.ts +347 -0
- package/src/client/changes/undo-affordance.ts +389 -0
- package/src/client/changes/undo-context.ts +34 -0
- package/src/client/composer.ts +436 -0
- package/src/client/css.d.ts +7 -0
- package/src/client/i18n.ts +702 -0
- package/src/client/index.tsx +286 -0
- package/src/client/remote-file-open-target.ts +553 -0
- package/src/client/remote-file-open.ts +261 -0
- package/src/client/reveal.ts +33 -0
- package/src/client/scm/DiffView.tsx +570 -0
- package/src/client/scm/HistoryPanel.tsx +353 -0
- package/src/client/scm/RepoPicker.tsx +238 -0
- package/src/client/scm/ScmPanel.tsx +857 -0
- package/src/client/scm/actions.ts +460 -0
- package/src/client/scm/diff-model.ts +206 -0
- package/src/client/scm/stage-queue.ts +264 -0
- package/src/client/scm/status-map.ts +381 -0
- package/src/client/scm/untracked-dirs.ts +139 -0
- package/src/client/selection-popup.ts +157 -0
- package/src/client/store.tsx +1670 -0
- package/src/client/theme.ts +54 -0
- package/src/client/ui.tsx +136 -0
- package/src/client/upload-plan.ts +153 -0
- package/src/client/upload-source.ts +145 -0
- package/src/client/use-remote-chat-target.ts +153 -0
- package/src/client/vsc/FindWidget.tsx +446 -0
- package/src/client/vsc/change-marks.ts +586 -0
- package/src/client/vsc/editor-langs.ts +137 -0
- package/src/client/vsc/editor-palette.ts +107 -0
- package/src/client/vsc/editor-theme.ts +93 -0
- package/src/client/vsc/fallback-highlight.ts +145 -0
- package/src/client/vsc/kit.tsx +777 -0
- package/src/client/vsc/markdown.tsx +172 -0
- package/src/client/vsc/reveal-line.ts +142 -0
- package/src/client/vsc/tokenize.ts +569 -0
- package/src/client/vsc/vsc.css +2669 -0
- package/src/deploy.ts +481 -0
- package/src/fs-ops.ts +57 -0
- package/src/git-format.ts +205 -0
- package/src/git-ops.ts +693 -0
- package/src/http.ts +55 -0
- package/src/importers.ts +141 -0
- package/src/index.ts +442 -0
- package/src/keygen.ts +77 -0
- package/src/loopback.ts +47 -0
- package/src/p2/bridge-refs.ts +27 -0
- package/src/p2/bridge.ts +32 -0
- package/src/p2/file-refs.ts +392 -0
- package/src/p2/helper-rpc.ts +394 -0
- package/src/p2/in-flight-gate.ts +63 -0
- package/src/p2/router-fs.ts +327 -0
- package/src/p2/router-refs.ts +166 -0
- package/src/p2/router-spill.ts +75 -0
- package/src/p2/router-subprocess.ts +92 -0
- package/src/p2/shell-bash.ts +30 -0
- package/src/p2/ssh-proc.ts +802 -0
- package/src/p2/worlds.ts +735 -0
- package/src/paths.ts +50 -0
- package/src/pool.ts +811 -0
- package/src/protocol.ts +460 -0
- package/src/routes.ts +802 -0
- package/src/search-ops.ts +220 -0
- package/src/sftp.ts +677 -0
- package/src/store.ts +364 -0
- package/src/terminal.ts +291 -0
- package/src/timeouts.ts +122 -0
- package/src/tools.ts +320 -0
- package/vendor/dsh-remote-helpers/dsh-ssh-helper-0.2.1-alpha.2-linux-x64.helper.json +16 -0
- package/vendor/dsh-remote-helpers/dsh-ssh-helper-0.2.1-alpha.2-linux-x64.tar.gz +4 -0
- package/vendor/dsh-remote-helpers/dsh-ssh-helper-0.2.1-alpha.2-linux-x64.tar.gz.sha256 +1 -0
- 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
|
+
}
|