@linxin666/dsh-remote-web-ui 0.1.1

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 (118) hide show
  1. package/LICENSE +28 -0
  2. package/README.md +346 -0
  3. package/cordis.patch.yml +10 -0
  4. package/lib/client.js +2817 -0
  5. package/lib/index.js +1455 -0
  6. package/lib/invariant.js +21 -0
  7. package/lib/types/client/PairFailedNotice.d.ts +12 -0
  8. package/lib/types/client/PairFailedNotice.d.ts.map +1 -0
  9. package/lib/types/client/PhoneIcon.d.ts +19 -0
  10. package/lib/types/client/PhoneIcon.d.ts.map +1 -0
  11. package/lib/types/client/PluginSettingsCard.d.ts +78 -0
  12. package/lib/types/client/PluginSettingsCard.d.ts.map +1 -0
  13. package/lib/types/client/RemoteEntry.d.ts +10 -0
  14. package/lib/types/client/RemoteEntry.d.ts.map +1 -0
  15. package/lib/types/client/RemotePanel.d.ts +50 -0
  16. package/lib/types/client/RemotePanel.d.ts.map +1 -0
  17. package/lib/types/client/RemoteSettingsCard.d.ts +75 -0
  18. package/lib/types/client/RemoteSettingsCard.d.ts.map +1 -0
  19. package/lib/types/client/deep-link.d.ts +40 -0
  20. package/lib/types/client/deep-link.d.ts.map +1 -0
  21. package/lib/types/client/index.d.ts +54 -0
  22. package/lib/types/client/index.d.ts.map +1 -0
  23. package/lib/types/client/locales.d.ts +144 -0
  24. package/lib/types/client/locales.d.ts.map +1 -0
  25. package/lib/types/client/pair-api.d.ts +100 -0
  26. package/lib/types/client/pair-api.d.ts.map +1 -0
  27. package/lib/types/client/settings-form.d.ts +119 -0
  28. package/lib/types/client/settings-form.d.ts.map +1 -0
  29. package/lib/types/gate.d.ts +58 -0
  30. package/lib/types/gate.d.ts.map +1 -0
  31. package/lib/types/index.d.ts +78 -0
  32. package/lib/types/index.d.ts.map +1 -0
  33. package/lib/types/invariant.d.ts +16 -0
  34. package/lib/types/invariant.d.ts.map +1 -0
  35. package/lib/types/lan.d.ts +15 -0
  36. package/lib/types/lan.d.ts.map +1 -0
  37. package/lib/types/mobile/api.d.ts +64 -0
  38. package/lib/types/mobile/api.d.ts.map +1 -0
  39. package/lib/types/mobile/index.d.ts +10 -0
  40. package/lib/types/mobile/index.d.ts.map +1 -0
  41. package/lib/types/mobile/messages.d.ts +103 -0
  42. package/lib/types/mobile/messages.d.ts.map +1 -0
  43. package/lib/types/mobile/messages.test.d.ts +2 -0
  44. package/lib/types/mobile/messages.test.d.ts.map +1 -0
  45. package/lib/types/mobile/mobile-styles.d.ts +6 -0
  46. package/lib/types/mobile/mobile-styles.d.ts.map +1 -0
  47. package/lib/types/mobile/mobile-theme.d.ts +22 -0
  48. package/lib/types/mobile/mobile-theme.d.ts.map +1 -0
  49. package/lib/types/mobile/mobile-theme.test.d.ts +2 -0
  50. package/lib/types/mobile/mobile-theme.test.d.ts.map +1 -0
  51. package/lib/types/mobile/mux.d.ts +52 -0
  52. package/lib/types/mobile/mux.d.ts.map +1 -0
  53. package/lib/types/mobile/rpc.d.ts +38 -0
  54. package/lib/types/mobile/rpc.d.ts.map +1 -0
  55. package/lib/types/mobile/theme-toggle.d.ts +12 -0
  56. package/lib/types/mobile/theme-toggle.d.ts.map +1 -0
  57. package/lib/types/mobile/views/App.d.ts +59 -0
  58. package/lib/types/mobile/views/App.d.ts.map +1 -0
  59. package/lib/types/mobile/views/ChatView.d.ts +39 -0
  60. package/lib/types/mobile/views/ChatView.d.ts.map +1 -0
  61. package/lib/types/mobile/views/ChatView.test.d.ts +2 -0
  62. package/lib/types/mobile/views/ChatView.test.d.ts.map +1 -0
  63. package/lib/types/mobile/views/SessionListView.d.ts +27 -0
  64. package/lib/types/mobile/views/SessionListView.d.ts.map +1 -0
  65. package/lib/types/mobile/views/SessionListView.test.d.ts +2 -0
  66. package/lib/types/mobile/views/SessionListView.test.d.ts.map +1 -0
  67. package/lib/types/mobile/views/WorkspaceView.d.ts +19 -0
  68. package/lib/types/mobile/views/WorkspaceView.d.ts.map +1 -0
  69. package/lib/types/mobile-api.d.ts +41 -0
  70. package/lib/types/mobile-api.d.ts.map +1 -0
  71. package/lib/types/mobile-routes.d.ts +16 -0
  72. package/lib/types/mobile-routes.d.ts.map +1 -0
  73. package/lib/types/pairing.d.ts +215 -0
  74. package/lib/types/pairing.d.ts.map +1 -0
  75. package/lib/types/routes.d.ts +58 -0
  76. package/lib/types/routes.d.ts.map +1 -0
  77. package/lib/types/tunnel.d.ts +99 -0
  78. package/lib/types/tunnel.d.ts.map +1 -0
  79. package/package.json +104 -0
  80. package/src/client/PairFailedNotice.tsx +33 -0
  81. package/src/client/PhoneIcon.tsx +31 -0
  82. package/src/client/PluginSettingsCard.tsx +207 -0
  83. package/src/client/RemoteEntry.tsx +202 -0
  84. package/src/client/RemotePanel.tsx +186 -0
  85. package/src/client/RemoteSettingsCard.tsx +220 -0
  86. package/src/client/deep-link.ts +137 -0
  87. package/src/client/index.ts +167 -0
  88. package/src/client/locales.ts +149 -0
  89. package/src/client/pair-api.ts +176 -0
  90. package/src/client/remote.module.css +422 -0
  91. package/src/client/settings-card.module.css +296 -0
  92. package/src/client/settings-form.ts +294 -0
  93. package/src/css-modules.d.ts +6 -0
  94. package/src/gate.ts +121 -0
  95. package/src/index.ts +299 -0
  96. package/src/invariant.ts +32 -0
  97. package/src/lan.ts +21 -0
  98. package/src/mobile/api.ts +106 -0
  99. package/src/mobile/index.tsx +27 -0
  100. package/src/mobile/messages.test.ts +222 -0
  101. package/src/mobile/messages.ts +555 -0
  102. package/src/mobile/mobile-styles.ts +853 -0
  103. package/src/mobile/mobile-theme.test.ts +84 -0
  104. package/src/mobile/mobile-theme.ts +76 -0
  105. package/src/mobile/mux.ts +126 -0
  106. package/src/mobile/rpc.ts +90 -0
  107. package/src/mobile/theme-toggle.tsx +47 -0
  108. package/src/mobile/views/App.tsx +152 -0
  109. package/src/mobile/views/ChatView.test.tsx +243 -0
  110. package/src/mobile/views/ChatView.tsx +650 -0
  111. package/src/mobile/views/SessionListView.test.tsx +139 -0
  112. package/src/mobile/views/SessionListView.tsx +186 -0
  113. package/src/mobile/views/WorkspaceView.tsx +108 -0
  114. package/src/mobile-api.ts +271 -0
  115. package/src/mobile-routes.ts +77 -0
  116. package/src/pairing.ts +390 -0
  117. package/src/routes.ts +346 -0
  118. package/src/tunnel.ts +264 -0
package/LICENSE ADDED
@@ -0,0 +1,28 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2026, zhu1090093659
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ 3. Neither the name of the copyright holder nor the names of its
16
+ contributors may be used to endorse or promote products derived from
17
+ this software without specific prior written permission.
18
+
19
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
package/README.md ADDED
@@ -0,0 +1,346 @@
1
+ # DSH Remote Web UI
2
+
3
+ > 移动端远程控制:扫码配对后,用手机远程使用当前 dsh web 工作区。
4
+
5
+ This repository is an external plugin package for DeepSeek Harness (DSH):
6
+ scan-to-pair mobile remote control for the dsh web GUI. It is a single
7
+ dual-face package — the host half owns pairing tokens, device sessions, and
8
+ the `/api/pair` route family; the browser half renders the sidebar-foot
9
+ entry (phone icon beside the settings button) and the pairing panel with a
10
+ QR code, live device status, and stop/refresh/copy actions.
11
+
12
+ ## What it does
13
+
14
+ - **Entry**: a phone icon in the sidebar foot, next to the settings button.
15
+ - **Panel**: "移动端远程控制" title, "扫码或在手机上打开链接,即可远程控制当前工作区"
16
+ subtitle, a "手机扫码连接" card with the status area ("等待手机连接" + status
17
+ badge), a large QR code, the "无法扫码?可以在手机上打开链接" hint, and three
18
+ buttons: 停止 / 刷新二维码 / 复制链接.
19
+ - **Phone side**: scanning the QR binds the phone with a one-time,
20
+ time-limited token and lands it on the **standalone mobile surface at
21
+ `/m`** — a thin client purpose-built for a small screen (see
22
+ [Screenshots](#screenshots)), not the desktop UI squeezed into a phone.
23
+ The link carries a `workspace` parameter so the phone lands in the same
24
+ workspace the desktop was looking at.
25
+ - **Security**: one active one-time token (a refresh invalidates the old
26
+ link; an accepted token cannot be reused; tokens expire). 停止 revokes
27
+ every paired device and the current token — paired devices are cut off on
28
+ their next request. When the plugin's `requirePairingForLan` gate is on
29
+ (default), every non-loopback `/api` request must carry a live paired
30
+ device cookie, so the QR is the only way into a LAN-exposed dsh web.
31
+ - **Live status**: the desktop panel mirrors the pairing state in real time
32
+ (waiting → connected → disconnected) over an SSE stream.
33
+
34
+ ## Screenshots
35
+
36
+ The phone surface on a 390pt viewport. Light is the default theme; a
37
+ sun/moon toggle in every header flips to the dark palette at any time.
38
+
39
+ - **Workspaces** — the roster, each row a workspace with its own sessions:
40
+ ![Workspaces](docs/screenshots/mobile-workspaces.png)
41
+ - **Sessions** — one workspace's sessions, headed by the 新建会话 button
42
+ (creates a blank session attached to the workspace and opens it
43
+ immediately):
44
+ ![Sessions](docs/screenshots/mobile-sessions.png)
45
+ - **Chat** — messages with the desktop fold discipline (collapsed
46
+ 深度思考 reasoning and 工具 tool-call rows), a pinned composer with
47
+ 模型 / 权限 chips, and a live stream while the agent works:
48
+ ![Chat](docs/screenshots/mobile-chat.png)
49
+ - **Model picker** — the bottom sheet with a provider-grouped catalog and a
50
+ 思考强度 section per model (the same `session.models` directory the
51
+ desktop uses):
52
+ ![Model sheet](docs/screenshots/mobile-model-sheet.png)
53
+
54
+ ## Requirements
55
+
56
+ - A DSH installation whose `dsh` CLI supports profiles (`dsh --profile`,
57
+ `dsh plugin`) — the profile/bundle mechanism this package rides on.
58
+ - For LAN use the server must be reachable from the phone: start with
59
+ `dsh web --host 0.0.0.0`. With the default `127.0.0.1` bind the panel
60
+ shows an explicit explanation instead of a dead QR code — unless a public
61
+ base URL is configured (see "Remote access over the internet" below),
62
+ which makes the QR reachable from anywhere without rebinding. The panel's
63
+ mint/stop endpoints are loopback-only by design: a desktop browser
64
+ opened at the LAN URL sees a "配对面板仅限本机使用" banner instead —
65
+ open the panel at `http://127.0.0.1` and let the phone use the paired
66
+ link.
67
+ - For the one-click public tunnel (`autoTunnel`), the `cloudflared`
68
+ platform binary ships with the package (its postinstall downloads it; a
69
+ runtime download covers installers that skip postinstall scripts). No
70
+ user-side tooling, account, or domain is needed — a Cloudflare quick
71
+ tunnel is free and anonymous.
72
+
73
+ ## Install
74
+
75
+ Install the family aggregate package `@linxin666/dsh-web-ui-all` (all plugins and skins in one) or this plugin alone:
76
+
77
+ ```sh
78
+ # Now (not yet published to npm): install from the dsh-web-ui monorepo
79
+ git clone https://github.com/zhu1090093659/dsh-web-ui.git
80
+ cd dsh-web-ui
81
+ pnpm install && pnpm -r build
82
+ dsh plugin --profile web add link:$(pwd)/packages/dsh-remote-web-ui
83
+
84
+ ```
85
+
86
+ Restart the profile (`dsh web`), then open the phone icon in the sidebar
87
+ foot. The plugin's `cordis.patch.yml` inserts the single plugin row that
88
+ mounts both halves.
89
+
90
+ > `github:<org>/<repo>` installs work for a standalone repo whose package
91
+ > sits at the root (the `prepare` script builds `lib/` during install;
92
+ > pnpm ≥10 blocks that until you copy the printed key into the profile's
93
+ > `pnpm-workspace.yaml` `allowBuilds` and re-run). Monorepo subpackages
94
+ > use the `link:` form above.
95
+
96
+ ## Use
97
+
98
+ 1. `dsh web --host 0.0.0.0` (the printed LAN URL confirms reachability).
99
+ 2. Click the phone icon → the panel mints a fresh one-time QR.
100
+ 3. Scan with the phone (or open the copied link): the phone binds and
101
+ lands on the **standalone mobile surface at `/m`** — no desktop UI on a
102
+ small screen. The surface is deliberately thin:
103
+ - workspaces straight away (a 新建会话 button lives on each workspace's
104
+ session list: it creates a blank session attached to that workspace via
105
+ the host's `session.create` and opens the new chat immediately),
106
+ - one workspace's sessions load **incrementally** (20 rows per page,
107
+ "加载更多会话" continues; never the whole list at once),
108
+ - opening a session fetches its chat content **on demand** (history
109
+ pages, "加载更早的消息" goes further back),
110
+ - a live stream shows new messages as they arrive, with a prompt box
111
+ for sending your own,
112
+ - a **light-first theme**: the surface ships a light palette by default;
113
+ a sun/moon toggle in every header flips to the dark palette and the
114
+ choice persists across visits (localStorage),
115
+ - messages render with the desktop fold discipline: reasoning hides
116
+ behind a collapsed 深度思考 disclosure, tool calls behind a collapsed
117
+ 工具 row (tap to see each call's arguments), very long answers behind
118
+ an explicit 展开全文 toggle, and each row carries its time — and
119
+ - a composer toolbar carries the **model** picker (provider-grouped
120
+ catalog with a 思考强度 effort section per model) and the **权限**
121
+ picker (permission presets; 完全权限 requires an explicit confirm
122
+ step). Both ride the host's own `session.models` /
123
+ `session.selectModel` RPCs and the `/permission` command — the phone
124
+ changes the same session settings the desktop would.
125
+ 4. The desktop badge flips to 已连接 in real time; it falls back to
126
+ offline/断开 when the phone leaves.
127
+ 5. 刷新二维码 invalidates the old link and issues a new one. 停止 revokes
128
+ mobile access: paired devices 403 on their next request, including their
129
+ live stream.
130
+
131
+ The mobile surface is fully self-contained in this plugin: the `/m` page
132
+ and its data channel (`/m/api`) are served by the plugin's own routes and
133
+ need **no harness source changes** — the phone's RPC calls ride the
134
+ plugin's `/m/api` proxy (which delegates to the host ApiProxy service and
135
+ pages `session.list` itself), so the tunneled Host never has to enter the
136
+ connection plugin's trust fence. The phone is gated by its paired-device
137
+ cookie and an explicit method allowlist (settings/credentials/host-action
138
+ domains are never reachable from the phone; model reads/writes are limited
139
+ to the advisory `session.models` / `session.selectModel` pair, creation to
140
+ `session.create` (workspace id only — the phone never names a working
141
+ directory of its own), and the permission picker only ever sends the
142
+ mode-agnostic `/permission` command
143
+ through the already-allowlisted `session.prompt`); the live stream arrives
144
+ over Server-Sent Events on `/m/api/events.mux`.
145
+
146
+ ### Behavior notes
147
+
148
+ - Installing this plugin gates non-loopback `/api` access behind pairing
149
+ (see `requirePairingForLan` in `src/index.ts`). A desktop browser opened
150
+ via the LAN URL must pair like any remote device; loopback (127.0.0.1)
151
+ is unaffected. Set `requirePairingForLan: false` in the profile patch to
152
+ restore the open-LAN behavior while keeping tokens/status/revocation.
153
+ - The QR link is built from the machine's non-internal IPv4 literals; a
154
+ multi-homed host (Wi-Fi + wired, or a proxy/VPN virtual adapter) shows a
155
+ radio picker so you can advertise the network the phone can actually
156
+ reach. The first literal is the default. When `publicBaseUrl` is set, the
157
+ picker adds a 公网地址 option on top — the default QR then uses the
158
+ public base, and picking a LAN literal re-mints an in-network link.
159
+ - A configured `publicBaseUrl` satisfies the reachable-bind requirement on
160
+ its own: `dsh web` bound to `127.0.0.1` (no `--host 0.0.0.0`) still mints
161
+ working public QR links through the tunnel.
162
+
163
+ ## Remote access over the internet (tunnels)
164
+
165
+ ### One-click public tunnel (recommended)
166
+
167
+ Turn on `autoTunnel` in the plugin settings card (or set
168
+ `autoTunnel: true` in the profile patch). The plugin then runs its own
169
+ Cloudflare quick tunnel — the `cloudflared` binary ships with the package,
170
+ no install, account, or domain needed — and wires everything itself:
171
+
172
+ - the minted `https://xxx.trycloudflare.com` URL becomes the QR base, so a
173
+ phone anywhere can pair. The panel shows the tunnel status (starting /
174
+ running / failed with the reason), and a crash is restarted
175
+ automatically with backoff.
176
+
177
+ The QR stays LAN-only until the tunnel reports its URL, and a tunnel
178
+ restart mints a NEW hostname — the plugin clears the old link and mints a
179
+ fresh one, so users never touch configuration. Note that a quick tunnel is
180
+ public: anyone with the URL can load the static page; the pairing gate is
181
+ the real fence, and the phone's data channel (`/m/api`) is protected by
182
+ its own paired-device gate plus a method allowlist — the tunneled Host
183
+ never needs to enter the connection plugin's trust fence, so **no profile
184
+ or harness customization is required for the auto tunnel to work**.
185
+
186
+ ### Manual tunnels (bring your own)
187
+
188
+ The QR link is normally a LAN URL, so a phone outside the house cannot use
189
+ it. Point a tunnel at the dsh web port and tell the plugin its public
190
+ address — the QR is then built from the tunnel URL and the phone-facing
191
+ pairing fence trusts the tunneled host. Two knobs are involved:
192
+
193
+ - **`publicBaseUrl`** (plugin config, in the profile patch or the settings
194
+ card): the public origin, e.g. `https://foo.trycloudflare.com`. The QR
195
+ link is built from it, and `accept`/`heartbeat`/`status` accept its host.
196
+ Malformed values are ignored with a warning (LAN-only behavior kept).
197
+ - **`--trusted-host <authority>`** (dsh web flag): the transport-level
198
+ `/api` fence of the connection plugin must accept the public host too —
199
+ without it every `/api` request through the tunnel 403s *before* the
200
+ pairing layer (the plugin's own fence only covers the `/api/pair`
201
+ routes). Pass the public host (or `host:port`) exactly as the tunnel
202
+ forwards it.
203
+
204
+ ### Cloudflare Tunnel (quick tunnel — no account, no domain)
205
+
206
+ Install the client once (macOS: `brew install cloudflared`; other systems:
207
+ grab the `cloudflared-darwin-{arm64,amd64}` binary from the official GitHub
208
+ releases). Then:
209
+
210
+ ```sh
211
+ # 1. Expose the local port (whatever dsh web is listening on):
212
+ cloudflared tunnel --url http://127.0.0.1:3080
213
+ # prints something like: https://xxxx-xxxx-xxxx.trycloudflare.com
214
+
215
+ # 2. Start dsh web with that host trusted (use --host 0.0.0.0 too when LAN
216
+ # access should stay available):
217
+ dsh web --trusted-host xxxx-xxxx-xxxx.trycloudflare.com
218
+ ```
219
+
220
+ Then set `publicBaseUrl: https://xxxx-xxxx-xxxx.trycloudflare.com` in the
221
+ profile patch (or the plugin settings card — it hot-reloads). Open the
222
+ phone icon at `http://127.0.0.1`, scan the QR from anywhere: the phone
223
+ binds, reloads into the mobile surface, and heartbeats keep it online.
224
+
225
+ Notes:
226
+
227
+ - Quick tunnels are free and need no login, but the hostname is random per
228
+ run: every `cloudflared` restart changes it, so update `--trusted-host`
229
+ and `publicBaseUrl` together. Cloudflare documents no uptime guarantees;
230
+ in-flight-request concurrency is capped (HTTP 429 past it), and Quick
231
+ Tunnels do not support Server-Sent Events — harmless here, because the
232
+ only SSE surface (the desktop panel status stream) is loopback-only and
233
+ the phone side uses plain requests plus heartbeats.
234
+ - A quick tunnel is public: anyone with the URL can load the static page.
235
+ The pairing gate is the real fence — unpaired devices get 403 on every
236
+ `/api` call — so keep `requirePairingForLan` on.
237
+ - For a stable hostname, create a named tunnel from the Cloudflare
238
+ dashboard (Networking → Tunnels; the domain must be hosted on
239
+ Cloudflare) and use its hostname in the same two places. Reachability
240
+ from mainland China is not guaranteed by Cloudflare; verify locally.
241
+ - Tailscale is an alternative for personal use that needs no plugin
242
+ changes at all: its virtual-interface address (`100.x.y.z`) shows up in
243
+ the QR's address picker automatically, and a phone on the same tailnet
244
+ reaches it like a LAN host.
245
+
246
+ ## Development
247
+
248
+ Work from this repository (no sibling checkout needed):
249
+
250
+ ```sh
251
+ cd ~/code/dsh-web-ui
252
+ export NPM_TOKEN='<token>' # only if private @deepseek-ai auth is still required
253
+ pnpm install
254
+ pnpm --filter @linxin666/dsh-remote-web-ui run build
255
+ pnpm --filter @linxin666/dsh-remote-web-ui test
256
+ pnpm --filter @linxin666/dsh-remote-web-ui run typecheck
257
+ ```
258
+
259
+ The peer APIs come from the official NPM SDK: every `@deepseek-ai/*` package
260
+ used here is declared in devDependencies (rc.6), and TypeScript/Vitest resolve
261
+ types straight from node_modules — no DSH source checkout is required. The
262
+ consumer-side `prepare` build (`tsdown.prepare.config.ts`) transpiles without
263
+ type checking, so git installs work without any harness checkout either.
264
+
265
+ ## Checks
266
+
267
+ ```sh
268
+ pnpm run typecheck
269
+ pnpm test
270
+ pnpm run build
271
+ ```
272
+
273
+ ## Harness contract dependencies
274
+
275
+ This plugin rides three harness seams that may not exist in older checkouts:
276
+
277
+ - **`api/gate` waterfall** (packages/client/connection): the /api route and
278
+ event WebSocket upgrades emit this event after the trust fence so plugins
279
+ can enforce application-level access control. Without it, revocation has
280
+ no server-side teeth.
281
+ - **`sidebar.remote` foot seat** (packages/client/ui-sidebar): the sidebar
282
+ declares and renders the seat the phone entry occupies.
283
+ - **LAN runtime connection fixes** (host-apiproxy `mintRpcId` fallback for
284
+ insecure-context origins; the 20260808-branch connection loop opening the
285
+ host stream after the mux stream): without them the browser runtime cannot
286
+ run on a plain-HTTP LAN page at all (the mobile side of this feature).
287
+
288
+ The fence helpers (`isTrustedApiRequest` / `isLoopbackHostname`) are
289
+ reimplemented locally in `src/gate.ts` / `src/routes.ts`: the 20260810
290
+ upstream moved the trust fence inside the connection plugin and stopped
291
+ exporting them, so the pairing routes carry their own copy scoped to the
292
+ literals the QR links advertise.
293
+ See the Agent Notes `api-gate-and-sidebar-remote-seat` and
294
+ `lan-runtime-connection-fixes` in the harness checkout.
295
+
296
+ ## Manual E2E: LAN pairing round trip
297
+
298
+ The unit/component specs cover the route family, the gate, and the panel,
299
+ but the pairing loop involves a real browser on a non-loopback origin.
300
+ Repeat this after any change to the wire contract or the connection loop:
301
+
302
+ 1. Start the server on all interfaces with a test workspace root:
303
+ `dsh web --host 0.0.0.0 --port 3190 --workspace-root /tmp/remote-e2e`.
304
+ 2. Open the **loopback** URL (`http://127.0.0.1:3190`) in a browser: the
305
+ phone icon sits in the sidebar foot; the panel mints a QR instantly.
306
+ 3. In a second tab (or a phone) open the **LAN** URL with the pair token
307
+ (e.g. `http://192.168.1.7:3190/?pair=<token>`): the page accepts, sets
308
+ the HttpOnly `dsh_pair` cookie, reloads, and boots the full UI — no
309
+ console errors, and a generation round trip completes.
310
+ 4. The desktop badge flips to 已连接 in real time; a LAN-origin desktop
311
+ page instead shows the 配对面板仅限本机使用 banner and opens no status
312
+ stream.
313
+ 5. 停止 on the desktop cuts the phone off: its next `/api` request 403s
314
+ (reconnect loops retry until a fresh QR re-pairs).
315
+
316
+ The public path is the same round trip through a tunnel (see "Remote access
317
+ over the internet"): loopback mint → phone opens the public QR URL →
318
+ accept → full UI. Both `publicBaseUrl` (plugin config) and
319
+ `--trusted-host` (dsh web flag) must name the tunneled host; the desktop
320
+ panel still opens at `http://127.0.0.1`.
321
+
322
+ ## Known Limitations and Deferred Work
323
+
324
+ - **Revocation is per-request**: a paired phone whose request is already in
325
+ flight when 停止 lands completes that request; the next one 403s.
326
+ - **Device sessions are in-memory**: pairing state (token + devices) resets
327
+ with the `dsh web` process.
328
+ - **No per-device management UI**: the panel shows aggregate status
329
+ (waiting / connected N / offline); individual device revocation is
330
+ deferred.
331
+ - **Quick-tunnel hostnames change per run**: a `trycloudflare.com` URL is
332
+ random on every `cloudflared` start, so `--trusted-host` and
333
+ `publicBaseUrl` must be updated together whenever the tunnel restarts.
334
+ A named tunnel (fixed hostname) avoids the churn.
335
+ - **Dev HMR**: `dsh web --dev` polls every roster bundle by path, so
336
+ rebuilding this package (its own `tsdown --watch`) hot-reloads the client
337
+ bundle; no harness-side watcher is involved.
338
+
339
+ ## Dependency rationale
340
+
341
+ `qrcode.react` (MIT, actively maintained, React 16–19 support) renders the
342
+ QR as a dependency-free SVG component — no canvas, no server-side image
343
+ generation. It is inlined into the client bundle at build time (like the
344
+ official skin/turtle-ui plugins inline their non-shared deps), so profile
345
+ installations need no extra runtime dependency beyond the dsh peer closure.
346
+ `schemastery` is the DSH-standard config schema validator.
@@ -0,0 +1,10 @@
1
+ # One package: the dual-face remote-control plugin. Install it into a dsh
2
+ # profile (`dsh plugin --profile <name> add link:...`) — the patch layer below
3
+ # inserts the single plugin row that mounts BOTH halves: the host side
4
+ # (pairing tokens, device sessions, /api/pair routes, the api/gate listener)
5
+ # and the browser side (the modules node half scans `dsh.client` packages into
6
+ # the web plugin roster, served from this package's lib/client.js).
7
+
8
+ - insert:
9
+ - id: remote-web-ui
10
+ name: '@linxin666/dsh-remote-web-ui'