dsh-remote-control-plugin 0.3.2 → 0.3.4
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/INSTALL.md +20 -27
- package/client.js +36 -6
- package/host/relay-link.js +11 -5
- package/package.json +6 -14
package/INSTALL.md
CHANGED
|
@@ -1,39 +1,32 @@
|
|
|
1
1
|
# Install on another desktop
|
|
2
2
|
|
|
3
|
-
Published as `dsh-remote-control-plugin`.
|
|
4
|
-
build step, no postinstall script.
|
|
3
|
+
Published as `dsh-remote-control-plugin`. Self-contained: no dependencies, no build step, no postinstall.
|
|
5
4
|
|
|
6
|
-
##
|
|
5
|
+
## Install
|
|
7
6
|
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
Web GUI: **Plugins → Add plugin**, enter `dsh-remote-control-plugin@0.3.4`, keep the official npm
|
|
8
|
+
registry selected, then **Install** and **Enable now**. Restart DSH.
|
|
10
9
|
|
|
11
|
-
|
|
10
|
+
From a chat (Standard or Creator preset — PTC disables `tool-plugin-manager`):
|
|
12
11
|
|
|
13
|
-
|
|
14
|
-
preset — PTC disables `tool-plugin-manager`):
|
|
12
|
+
> Install the DSH plugin bundle `dsh-remote-control-plugin@0.3.4` with plugin_manager.
|
|
15
13
|
|
|
16
|
-
|
|
14
|
+
## Connect two machines
|
|
17
15
|
|
|
18
|
-
|
|
16
|
+
1. On the machine with a reachable address: **Devices → Relay → Create relay here**, **Copy**.
|
|
17
|
+
2. On the other: **Devices → Relay**, paste the invite, **Connect**.
|
|
18
|
+
3. Either: **Request**, then **Allow** on the target.
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
2. On the other machine: **Devices → Relay**, paste the invite into the URL field, **Connect**.
|
|
24
|
-
3. Either machine: **Request** next to the peer, then **Allow** on the target.
|
|
25
|
-
|
|
26
|
-
Two machines on the same LAN need no relay at all — LAN discovery lists them directly.
|
|
27
|
-
|
|
28
|
-
No reachable machine? Run a relay anywhere both can reach (`PORT=8787 node relay/server.js`) and
|
|
29
|
-
paste `http://<host>:8787 <workspace key>` into both panels.
|
|
20
|
+
Same-LAN machines need no relay. No reachable machine? Run
|
|
21
|
+
`PORT=8787 node relay/server.js` anywhere both can reach and paste
|
|
22
|
+
`http://<host>:8787 <workspace key>` into both panels.
|
|
30
23
|
|
|
31
24
|
## Notes
|
|
32
25
|
|
|
33
|
-
-
|
|
34
|
-
- Approvals
|
|
35
|
-
-
|
|
36
|
-
- Peer traffic is signed but not encrypted
|
|
37
|
-
- 0.3.
|
|
38
|
-
|
|
39
|
-
|
|
26
|
+
- Trust comes from the one-time **Allow**; the account label is cosmetic.
|
|
27
|
+
- Approvals live in `$DSH_HOME/remote-control-peers.json`; delete it to revoke.
|
|
28
|
+
- Relay and control API share one port (default `57330`).
|
|
29
|
+
- Peer traffic is signed but not encrypted; use a private network or TLS in front.
|
|
30
|
+
- 0.3.4 fixes: ordered transcript chunks, hosted relay persistence, stale
|
|
31
|
+
relay peer pruning, relay fallback when a direct LAN route is blocked, and a
|
|
32
|
+
client module id that matches the package name.
|
package/client.js
CHANGED
|
@@ -117,12 +117,14 @@ window.__ModuleLoader__.load({
|
|
|
117
117
|
|
|
118
118
|
/* ---------------- chat ---------------- */
|
|
119
119
|
|
|
120
|
-
function Chat({ peerId, sessionId, title }) {
|
|
120
|
+
function Chat({ peerId, sessionId, title, blank }) {
|
|
121
121
|
const [entries, setEntries] = useState([])
|
|
122
122
|
const [live, setLive] = useState('')
|
|
123
123
|
const [running, setRunning] = useState(false)
|
|
124
124
|
const [draft, setDraft] = useState('')
|
|
125
125
|
const [error, setError] = useState(null)
|
|
126
|
+
const [streamState, setStreamState] = useState('connecting')
|
|
127
|
+
const [attempt, setAttempt] = useState(0)
|
|
126
128
|
const seen = useRef(new Set())
|
|
127
129
|
const logRef = useRef(null)
|
|
128
130
|
const pinned = useRef(true)
|
|
@@ -133,11 +135,14 @@ window.__ModuleLoader__.load({
|
|
|
133
135
|
setLive('')
|
|
134
136
|
setRunning(false)
|
|
135
137
|
setError(null)
|
|
138
|
+
setStreamState('connecting')
|
|
136
139
|
if (!peerId || !sessionId) return undefined
|
|
137
140
|
const source = new EventSource(
|
|
138
141
|
`/remote-control/api/peer/${encodeURIComponent(peerId)}/stream?sessionId=${encodeURIComponent(sessionId)}`,
|
|
139
142
|
)
|
|
143
|
+
source.onopen = () => setStreamState('live')
|
|
140
144
|
source.onmessage = (event) => {
|
|
145
|
+
setStreamState('live')
|
|
141
146
|
let frame
|
|
142
147
|
try { frame = JSON.parse(event.data) } catch { return }
|
|
143
148
|
if (frame.type === 'snapshot') {
|
|
@@ -172,9 +177,13 @@ window.__ModuleLoader__.load({
|
|
|
172
177
|
}
|
|
173
178
|
if (frame.type === 'remote-error') setError(`${frame.code || 'error'}: ${frame.message || 'stream failed'}`)
|
|
174
179
|
}
|
|
175
|
-
source.onerror = () => {
|
|
180
|
+
source.onerror = () => {
|
|
181
|
+
// EventSource retries by itself, but a dead stream must not look like
|
|
182
|
+
// an empty conversation.
|
|
183
|
+
setStreamState('error')
|
|
184
|
+
}
|
|
176
185
|
return () => source.close()
|
|
177
|
-
}, [peerId, sessionId])
|
|
186
|
+
}, [peerId, sessionId, attempt])
|
|
178
187
|
|
|
179
188
|
useEffect(() => {
|
|
180
189
|
if (!pinned.current) return
|
|
@@ -217,8 +226,25 @@ window.__ModuleLoader__.load({
|
|
|
217
226
|
},
|
|
218
227
|
},
|
|
219
228
|
entries.length === 0 && !live
|
|
220
|
-
? h('div', { style: S.empty },
|
|
229
|
+
? h('div', { style: S.empty },
|
|
230
|
+
streamState === 'error'
|
|
231
|
+
? h('div', null,
|
|
232
|
+
h('div', { style: { marginBottom: '8px' } },
|
|
233
|
+
'Could not open the transcript. The device may be unreachable, or it refused the stream.'),
|
|
234
|
+
h('button', {
|
|
235
|
+
type: 'button',
|
|
236
|
+
style: S.button,
|
|
237
|
+
onClick: () => setAttempt((value) => value + 1),
|
|
238
|
+
}, 'Retry'))
|
|
239
|
+
: streamState === 'connecting'
|
|
240
|
+
? 'Connecting to the device…'
|
|
241
|
+
: (blank
|
|
242
|
+
? 'This session is empty. Send a message to start it.'
|
|
243
|
+
: 'No messages yet. Send one below.'))
|
|
221
244
|
: entries.map((entry) => renderEntry(entry)),
|
|
245
|
+
streamState === 'error' && entries.length > 0
|
|
246
|
+
? h('div', { style: S.note }, 'Stream lost — retrying…')
|
|
247
|
+
: null,
|
|
222
248
|
live ? h('div', { style: { ...S.bubble, ...S.assistantBubble, opacity: 0.85 } }, live) : null,
|
|
223
249
|
),
|
|
224
250
|
h('div', { style: S.composer },
|
|
@@ -337,7 +363,10 @@ window.__ModuleLoader__.load({
|
|
|
337
363
|
setSessionId(null)
|
|
338
364
|
try {
|
|
339
365
|
const value = await api(`/remote-control/api/peer/${encodeURIComponent(peer.deviceId)}/sessions`)
|
|
340
|
-
|
|
366
|
+
const items = (value.items || []).slice().sort(
|
|
367
|
+
(a, b) => Number(Boolean(a.blank)) - Number(Boolean(b.blank)) || (b.updatedAt || 0) - (a.updatedAt || 0),
|
|
368
|
+
)
|
|
369
|
+
setSessions(items)
|
|
341
370
|
} catch (failure) {
|
|
342
371
|
setError(failure.message)
|
|
343
372
|
setSessions([])
|
|
@@ -530,7 +559,7 @@ window.__ModuleLoader__.load({
|
|
|
530
559
|
h('div', { style: { minWidth: 0 } },
|
|
531
560
|
h('div', { style: S.name }, session.title || session.cwd || session.id),
|
|
532
561
|
h('div', { style: S.meta },
|
|
533
|
-
[session.running ? 'running' : '', relativeTime(session.updatedAt)].filter(Boolean).join(' · ')),
|
|
562
|
+
[session.running ? 'running' : '', session.blank ? 'empty' : '', relativeTime(session.updatedAt)].filter(Boolean).join(' · ')),
|
|
534
563
|
),
|
|
535
564
|
))),
|
|
536
565
|
),
|
|
@@ -539,6 +568,7 @@ window.__ModuleLoader__.load({
|
|
|
539
568
|
peerId: openPeer.deviceId,
|
|
540
569
|
sessionId,
|
|
541
570
|
title: sessions.find((session) => session.id === sessionId)?.title,
|
|
571
|
+
blank: sessions.find((session) => session.id === sessionId)?.blank === true,
|
|
542
572
|
})
|
|
543
573
|
: h('div', { style: S.empty }, 'Pick a session to control it.'),
|
|
544
574
|
),
|
package/host/relay-link.js
CHANGED
|
@@ -194,23 +194,29 @@ export function createRelayLink({ url, key, deviceId, deviceName, userId, localP
|
|
|
194
194
|
method: 'GET',
|
|
195
195
|
headers: { ...message.headers, host: `127.0.0.1:${localPort}` },
|
|
196
196
|
}, (response) => {
|
|
197
|
-
|
|
197
|
+
// Chunks must reach the peer in order: one POST per chunk has no ordering
|
|
198
|
+
// guarantee, and a reordered SSE stream never forms a complete frame.
|
|
199
|
+
let chain = Promise.resolve()
|
|
200
|
+
const push = (message) => {
|
|
201
|
+
chain = chain.then(() => send(from, message)).catch(() => {})
|
|
202
|
+
}
|
|
203
|
+
push({ kind: 'stream-status', id, status: response.statusCode ?? 502 })
|
|
198
204
|
if ((response.statusCode ?? 500) >= 400) {
|
|
199
205
|
response.resume()
|
|
200
206
|
locals.delete(id)
|
|
201
|
-
|
|
207
|
+
push({ kind: 'stream-end', id })
|
|
202
208
|
return
|
|
203
209
|
}
|
|
204
210
|
response.on('data', (chunk) => {
|
|
205
|
-
|
|
211
|
+
push({ kind: 'stream-chunk', id, chunk: chunk.toString('utf8') })
|
|
206
212
|
})
|
|
207
213
|
response.on('end', () => {
|
|
208
214
|
locals.delete(id)
|
|
209
|
-
|
|
215
|
+
push({ kind: 'stream-end', id })
|
|
210
216
|
})
|
|
211
217
|
response.on('error', () => {
|
|
212
218
|
locals.delete(id)
|
|
213
|
-
|
|
219
|
+
push({ kind: 'stream-end', id })
|
|
214
220
|
})
|
|
215
221
|
})
|
|
216
222
|
request.on('error', () => {
|
package/package.json
CHANGED
|
@@ -1,17 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-remote-control-plugin",
|
|
3
|
-
"version": "0.3.
|
|
4
|
-
"description": "Remote control for DSH: a Devices panel that discovers other DSH desktops, pairs with them once, and drives their sessions across LAN or a relay.",
|
|
5
|
-
"license": "MIT",
|
|
3
|
+
"version": "0.3.4",
|
|
6
4
|
"type": "module",
|
|
7
|
-
"
|
|
8
|
-
"dsh",
|
|
9
|
-
"deepseek-harness",
|
|
10
|
-
"cordis",
|
|
11
|
-
"cordis-plugin",
|
|
12
|
-
"remote-control",
|
|
13
|
-
"devices"
|
|
14
|
-
],
|
|
5
|
+
"description": "Remote control for DSH: a Devices panel that discovers other DSH desktops signed into the same account, pairs with them once, and drives their sessions.",
|
|
15
6
|
"exports": {
|
|
16
7
|
".": "./index.js",
|
|
17
8
|
"./client": "./client.js",
|
|
@@ -33,9 +24,6 @@
|
|
|
33
24
|
"web/app.css",
|
|
34
25
|
"web/icon.svg"
|
|
35
26
|
],
|
|
36
|
-
"engines": {
|
|
37
|
-
"node": ">=20"
|
|
38
|
-
},
|
|
39
27
|
"dsh": {
|
|
40
28
|
"bundle": {
|
|
41
29
|
"patch": "./cordis.patch.yml"
|
|
@@ -44,5 +32,9 @@
|
|
|
44
32
|
"platform": "web",
|
|
45
33
|
"immediately": true
|
|
46
34
|
}
|
|
35
|
+
},
|
|
36
|
+
"license": "MIT",
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=20"
|
|
47
39
|
}
|
|
48
40
|
}
|