dsh-remote-control-plugin 0.3.2 → 0.3.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/INSTALL.md CHANGED
@@ -1,29 +1,29 @@
1
1
  # Install on another desktop
2
2
 
3
- Published as `dsh-remote-control-plugin`. The package is self-contained — no dependencies, no
3
+ Published as `dsh-remote-control-plugin`. The package is self-contained — no dependencies, no
4
4
  build step, no postinstall script.
5
5
 
6
6
  ## 1. Install it
7
7
 
8
- In the Web GUI: **Plugins → Add plugin**, enter `dsh-remote-control-plugin@0.3.2`, keep the
8
+ In the Web GUI: **Plugins → Add plugin**, enter `dsh-remote-control-plugin@0.3.3`, keep the
9
9
  official npm registry selected, then **Install** and **Enable now**. Restart DSH.
10
10
 
11
11
  A **Devices** icon appears in the sidebar.
12
12
 
13
13
  From a chat, the same install works where the plugin tool is enabled (Standard or Creator
14
- preset — PTC disables `tool-plugin-manager`):
14
+ preset — PTC disables `tool-plugin-manager`):
15
15
 
16
- > Install the DSH plugin bundle `dsh-remote-control-plugin@0.3.2` with plugin_manager, then tell me when it is applied.
16
+ > Install the DSH plugin bundle `dsh-remote-control-plugin@0.3.3` with plugin_manager, then tell me when it is applied.
17
17
 
18
18
  ## 2. Connect two machines
19
19
 
20
20
  Pick the machine with a reachable address (same LAN, VPN, or a forwarded port):
21
21
 
22
- 1. On that machine: **Devices → Relay → Create relay here**, then **Copy** the invite.
23
- 2. On the other machine: **Devices → Relay**, paste the invite into the URL field, **Connect**.
22
+ 1. On that machine: **Devices → Relay → Create relay here**, then **Copy** the invite.
23
+ 2. On the other machine: **Devices → Relay**, paste the invite into the URL field, **Connect**.
24
24
  3. Either machine: **Request** next to the peer, then **Allow** on the target.
25
25
 
26
- Two machines on the same LAN need no relay at all — LAN discovery lists them directly.
26
+ Two machines on the same LAN need no relay at all — LAN discovery lists them directly.
27
27
 
28
28
  No reachable machine? Run a relay anywhere both can reach (`PORT=8787 node relay/server.js`) and
29
29
  paste `http://<host>:8787 <workspace key>` into both panels.
@@ -34,6 +34,7 @@ paste `http://<host>:8787 <workspace key>` into both panels.
34
34
  - Approvals persist in `$DSH_HOME/remote-control-peers.json`; deleting it revokes every peer.
35
35
  - The relay and the control API share one port (default `57330`), so one forwarded port is enough.
36
36
  - Peer traffic is signed but not encrypted. Use a private network or TLS in front of the port.
37
- - 0.3.2 fixes three relay bugs: a hosted relay now survives a restart, peers no
38
- longer linger as `503 relay-offline` after a relay goes away, and a blocked
39
- direct LAN route falls back to the outbound relay instead of failing.
37
+ - 0.3.3 fixes four relay bugs: a hosted relay now survives a restart, peers no
38
+ longer linger as `503 relay-offline` after a relay goes away, a blocked direct
39
+ LAN route falls back to the outbound relay, and transcript chunks are sent in
40
+ order so a large snapshot always assembles into a complete frame.
package/client.js CHANGED
@@ -1,5 +1,5 @@
1
1
  window.__ModuleLoader__.load({
2
- id: 'dsh-remote-control-plugin',
2
+ id: '@local/dsh-remote-control',
3
3
  factory(require) {
4
4
  const React = require('react')
5
5
  const h = React.createElement
@@ -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 = () => { /* EventSource retries on its own */ }
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 }, 'No messages yet. Send one below.')
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
- setSessions(value.items || [])
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
  ),
@@ -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
- send(from, { kind: 'stream-status', id, status: response.statusCode ?? 502 }).catch(() => {})
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
- send(from, { kind: 'stream-end', id }).catch(() => {})
207
+ push({ kind: 'stream-end', id })
202
208
  return
203
209
  }
204
210
  response.on('data', (chunk) => {
205
- send(from, { kind: 'stream-chunk', id, chunk: chunk.toString('utf8') }).catch(() => {})
211
+ push({ kind: 'stream-chunk', id, chunk: chunk.toString('utf8') })
206
212
  })
207
213
  response.on('end', () => {
208
214
  locals.delete(id)
209
- send(from, { kind: 'stream-end', id }).catch(() => {})
215
+ push({ kind: 'stream-end', id })
210
216
  })
211
217
  response.on('error', () => {
212
218
  locals.delete(id)
213
- send(from, { kind: 'stream-end', id }).catch(() => {})
219
+ push({ kind: 'stream-end', id })
214
220
  })
215
221
  })
216
222
  request.on('error', () => {
package/package.json CHANGED
@@ -1,48 +1,48 @@
1
- {
2
- "name": "dsh-remote-control-plugin",
3
- "version": "0.3.2",
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",
6
- "type": "module",
7
- "keywords": [
8
- "dsh",
9
- "deepseek-harness",
10
- "cordis",
11
- "cordis-plugin",
12
- "remote-control",
13
- "devices"
14
- ],
15
- "exports": {
16
- ".": "./index.js",
17
- "./client": "./client.js",
18
- "./locale/*.json": "./locale/*.json",
19
- "./icon": "./icon.svg"
20
- },
21
- "files": [
22
- "index.js",
23
- "client.js",
24
- "host/*.js",
25
- "cordis.patch.yml",
26
- "locale/*.json",
27
- "icon.svg",
28
- "README.md",
29
- "INSTALL.md",
30
- "LICENSE",
31
- "web/index.html",
32
- "web/app.js",
33
- "web/app.css",
34
- "web/icon.svg"
35
- ],
36
- "engines": {
37
- "node": ">=20"
38
- },
39
- "dsh": {
40
- "bundle": {
41
- "patch": "./cordis.patch.yml"
42
- },
43
- "client": {
44
- "platform": "web",
45
- "immediately": true
46
- }
47
- }
48
- }
1
+ {
2
+ "name": "dsh-remote-control-plugin",
3
+ "version": "0.3.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",
6
+ "type": "module",
7
+ "keywords": [
8
+ "dsh",
9
+ "deepseek-harness",
10
+ "cordis",
11
+ "cordis-plugin",
12
+ "remote-control",
13
+ "devices"
14
+ ],
15
+ "exports": {
16
+ ".": "./index.js",
17
+ "./client": "./client.js",
18
+ "./locale/*.json": "./locale/*.json",
19
+ "./icon": "./icon.svg"
20
+ },
21
+ "files": [
22
+ "index.js",
23
+ "client.js",
24
+ "host/*.js",
25
+ "cordis.patch.yml",
26
+ "locale/*.json",
27
+ "icon.svg",
28
+ "README.md",
29
+ "INSTALL.md",
30
+ "LICENSE",
31
+ "web/index.html",
32
+ "web/app.js",
33
+ "web/app.css",
34
+ "web/icon.svg"
35
+ ],
36
+ "engines": {
37
+ "node": "\u003e=20"
38
+ },
39
+ "dsh": {
40
+ "bundle": {
41
+ "patch": "./cordis.patch.yml"
42
+ },
43
+ "client": {
44
+ "platform": "web",
45
+ "immediately": true
46
+ }
47
+ }
48
+ }