dsh-zen-remote 1.1.16 → 2.0.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 (261) hide show
  1. package/AGENTS.md +54 -9
  2. package/README.md +249 -190
  3. package/README.zh-CN.md +247 -201
  4. package/assets/home.png +0 -0
  5. package/assets/info.png +0 -0
  6. package/assets/pairing.png +0 -0
  7. package/assets/session.png +0 -0
  8. package/assets/sheet.png +0 -0
  9. package/cordis.patch.yml +12 -11
  10. package/cordis.patch.yml.example +24 -9
  11. package/docs/interface.md +57 -2
  12. package/docs/remote-access.en.md +99 -58
  13. package/docs/remote-access.md +99 -42
  14. package/dsh-push.mjs +67 -54
  15. package/lan-gate.mjs +88 -20
  16. package/lib/activity.js +168 -0
  17. package/lib/activity.js.map +1 -0
  18. package/lib/admin-routes.js +526 -0
  19. package/lib/admin-routes.js.map +1 -0
  20. package/lib/client-pairing.js +224 -0
  21. package/lib/client-pairing.js.map +1 -0
  22. package/lib/client-routes.js +674 -0
  23. package/lib/client-routes.js.map +1 -0
  24. package/lib/client.js +11498 -5990
  25. package/lib/config.js +346 -0
  26. package/lib/config.js.map +1 -0
  27. package/lib/fetch-route-intercept.js +418 -0
  28. package/lib/fetch-route-intercept.js.map +1 -0
  29. package/lib/fingerprint.js +365 -0
  30. package/lib/fingerprint.js.map +1 -0
  31. package/lib/http.js +69 -0
  32. package/lib/http.js.map +1 -0
  33. package/lib/index.js +675 -54
  34. package/lib/index.js.map +1 -1
  35. package/lib/intercept-shape.js +81 -0
  36. package/lib/intercept-shape.js.map +1 -0
  37. package/lib/intercept.js +3181 -0
  38. package/lib/intercept.js.map +1 -0
  39. package/lib/lan-gate-server.cjs +532 -121
  40. package/lib/merge-streams.js +1172 -0
  41. package/lib/merge-streams.js.map +1 -0
  42. package/lib/relay-access.js +496 -0
  43. package/lib/relay-access.js.map +1 -0
  44. package/lib/relay-client.js +1303 -0
  45. package/lib/relay-client.js.map +1 -0
  46. package/lib/relay-filter.js +282 -0
  47. package/lib/relay-filter.js.map +1 -0
  48. package/lib/relay-server.js +1813 -0
  49. package/lib/relay-server.js.map +1 -0
  50. package/lib/restart-fields.js +34 -0
  51. package/lib/restart-fields.js.map +1 -0
  52. package/lib/restart-watcher.js +106 -0
  53. package/lib/restart-watcher.js.map +1 -0
  54. package/lib/session-reference.js +157 -0
  55. package/lib/session-reference.js.map +1 -0
  56. package/lib/share-export.js +16 -1
  57. package/lib/share-export.js.map +1 -1
  58. package/lib/share-ops.js +86 -0
  59. package/lib/share-ops.js.map +1 -0
  60. package/lib/share-store.js +294 -0
  61. package/lib/share-store.js.map +1 -0
  62. package/lib/types/activity.d.ts +99 -0
  63. package/lib/types/activity.d.ts.map +1 -0
  64. package/lib/types/admin-routes.d.ts +129 -0
  65. package/lib/types/admin-routes.d.ts.map +1 -0
  66. package/lib/types/client/MobileDrawerFooter.d.ts.map +1 -1
  67. package/lib/types/client/MobileHome.d.ts +1 -1
  68. package/lib/types/client/MobileHome.d.ts.map +1 -1
  69. package/lib/types/client/MobileHomeChips.d.ts +1 -1
  70. package/lib/types/client/MobileHomeChips.d.ts.map +1 -1
  71. package/lib/types/client/MobileSessionHeader.d.ts +13 -2
  72. package/lib/types/client/MobileSessionHeader.d.ts.map +1 -1
  73. package/lib/types/client/MobileSessionInfo.d.ts +20 -3
  74. package/lib/types/client/MobileSessionInfo.d.ts.map +1 -1
  75. package/lib/types/client/RemoteComposerBanner.d.ts +42 -0
  76. package/lib/types/client/RemoteComposerBanner.d.ts.map +1 -0
  77. package/lib/types/client/RemoteHeaderIcon.d.ts +37 -0
  78. package/lib/types/client/RemoteHeaderIcon.d.ts.map +1 -0
  79. package/lib/types/client/RemoteShareMenu.d.ts +28 -0
  80. package/lib/types/client/RemoteShareMenu.d.ts.map +1 -0
  81. package/lib/types/client/RemoteStatusIcon.d.ts +36 -0
  82. package/lib/types/client/RemoteStatusIcon.d.ts.map +1 -0
  83. package/lib/types/client/compat/desktop.d.ts +21 -0
  84. package/lib/types/client/compat/desktop.d.ts.map +1 -0
  85. package/lib/types/client/compat/types.d.ts +57 -0
  86. package/lib/types/client/compat/types.d.ts.map +1 -1
  87. package/lib/types/client/current-session-report.d.ts +37 -0
  88. package/lib/types/client/current-session-report.d.ts.map +1 -0
  89. package/lib/types/client/effects/header-status.d.ts.map +1 -1
  90. package/lib/types/client/effects/keyboard-guard.d.ts.map +1 -1
  91. package/lib/types/client/effects/model-group-side.d.ts +13 -0
  92. package/lib/types/client/effects/model-group-side.d.ts.map +1 -0
  93. package/lib/types/client/effects/permission-sheet.d.ts +39 -0
  94. package/lib/types/client/effects/permission-sheet.d.ts.map +1 -0
  95. package/lib/types/client/effects/remote-session.d.ts +25 -0
  96. package/lib/types/client/effects/remote-session.d.ts.map +1 -0
  97. package/lib/types/client/effects/turn-fold.d.ts +9 -0
  98. package/lib/types/client/effects/turn-fold.d.ts.map +1 -1
  99. package/lib/types/client/index.d.ts.map +1 -1
  100. package/lib/types/client/locales.d.ts +187 -2
  101. package/lib/types/client/locales.d.ts.map +1 -1
  102. package/lib/types/client/remote-fetch.d.ts +51 -0
  103. package/lib/types/client/remote-fetch.d.ts.map +1 -0
  104. package/lib/types/client/remote-group-expansion.d.ts +41 -0
  105. package/lib/types/client/remote-group-expansion.d.ts.map +1 -0
  106. package/lib/types/client/remote-share-css.d.ts +19 -0
  107. package/lib/types/client/remote-share-css.d.ts.map +1 -0
  108. package/lib/types/client/remote-share-register.d.ts +113 -0
  109. package/lib/types/client/remote-share-register.d.ts.map +1 -0
  110. package/lib/types/client/remote-status-css.d.ts +15 -0
  111. package/lib/types/client/remote-status-css.d.ts.map +1 -0
  112. package/lib/types/client/remote-status-register.d.ts +46 -0
  113. package/lib/types/client/remote-status-register.d.ts.map +1 -0
  114. package/lib/types/client/settings/SettingsSection.d.ts +46 -0
  115. package/lib/types/client/settings/SettingsSection.d.ts.map +1 -0
  116. package/lib/types/client/settings/register-settings.d.ts +58 -0
  117. package/lib/types/client/settings/register-settings.d.ts.map +1 -0
  118. package/lib/types/client/settings/settings-css.d.ts +10 -0
  119. package/lib/types/client/settings/settings-css.d.ts.map +1 -0
  120. package/lib/types/client/share/share-preview.d.ts.map +1 -1
  121. package/lib/types/client/styles/composer.css.d.ts +1 -1
  122. package/lib/types/client/styles/composer.css.d.ts.map +1 -1
  123. package/lib/types/client/styles/header.css.d.ts +1 -1
  124. package/lib/types/client/styles/header.css.d.ts.map +1 -1
  125. package/lib/types/client/styles/info.css.d.ts +1 -1
  126. package/lib/types/client/styles/info.css.d.ts.map +1 -1
  127. package/lib/types/client/styles/remote-session.css.d.ts +2 -0
  128. package/lib/types/client/styles/remote-session.css.d.ts.map +1 -0
  129. package/lib/types/client-data/current-session.d.ts +41 -0
  130. package/lib/types/client-data/current-session.d.ts.map +1 -0
  131. package/lib/types/client-data/model-group-side.d.ts +44 -0
  132. package/lib/types/client-data/model-group-side.d.ts.map +1 -0
  133. package/lib/types/client-data/remote-group-expansion.d.ts +76 -0
  134. package/lib/types/client-data/remote-group-expansion.d.ts.map +1 -0
  135. package/lib/types/client-data/remote-status.d.ts +148 -0
  136. package/lib/types/client-data/remote-status.d.ts.map +1 -0
  137. package/lib/types/client-data/settings-form.d.ts +774 -0
  138. package/lib/types/client-data/settings-form.d.ts.map +1 -0
  139. package/lib/types/client-data/shares.d.ts +211 -0
  140. package/lib/types/client-data/shares.d.ts.map +1 -0
  141. package/lib/types/client-pairing.d.ts +114 -0
  142. package/lib/types/client-pairing.d.ts.map +1 -0
  143. package/lib/types/client-routes.d.ts +112 -0
  144. package/lib/types/client-routes.d.ts.map +1 -0
  145. package/lib/types/config.d.ts +200 -0
  146. package/lib/types/config.d.ts.map +1 -0
  147. package/lib/types/fetch-route-intercept.d.ts +156 -0
  148. package/lib/types/fetch-route-intercept.d.ts.map +1 -0
  149. package/lib/types/fingerprint.d.ts +166 -0
  150. package/lib/types/fingerprint.d.ts.map +1 -0
  151. package/lib/types/http.d.ts +41 -0
  152. package/lib/types/http.d.ts.map +1 -0
  153. package/lib/types/index.d.ts +56 -24
  154. package/lib/types/index.d.ts.map +1 -1
  155. package/lib/types/intercept-shape.d.ts +32 -0
  156. package/lib/types/intercept-shape.d.ts.map +1 -0
  157. package/lib/types/intercept.d.ts +442 -0
  158. package/lib/types/intercept.d.ts.map +1 -0
  159. package/lib/types/merge-streams.d.ts +398 -0
  160. package/lib/types/merge-streams.d.ts.map +1 -0
  161. package/lib/types/relay-access.d.ts +310 -0
  162. package/lib/types/relay-access.d.ts.map +1 -0
  163. package/lib/types/relay-client.d.ts +274 -0
  164. package/lib/types/relay-client.d.ts.map +1 -0
  165. package/lib/types/relay-filter.d.ts +103 -0
  166. package/lib/types/relay-filter.d.ts.map +1 -0
  167. package/lib/types/relay-server.d.ts +208 -0
  168. package/lib/types/relay-server.d.ts.map +1 -0
  169. package/lib/types/restart-fields.d.ts +22 -0
  170. package/lib/types/restart-fields.d.ts.map +1 -0
  171. package/lib/types/restart-watcher.d.ts +83 -0
  172. package/lib/types/restart-watcher.d.ts.map +1 -0
  173. package/lib/types/session-reference.d.ts +66 -0
  174. package/lib/types/session-reference.d.ts.map +1 -0
  175. package/lib/types/share-export.d.ts +14 -1
  176. package/lib/types/share-export.d.ts.map +1 -1
  177. package/lib/types/share-ops.d.ts +79 -0
  178. package/lib/types/share-ops.d.ts.map +1 -0
  179. package/lib/types/share-store.d.ts +87 -0
  180. package/lib/types/share-store.d.ts.map +1 -0
  181. package/lib/types/virtual-id.d.ts +52 -0
  182. package/lib/types/virtual-id.d.ts.map +1 -0
  183. package/lib/virtual-id.js +66 -0
  184. package/lib/virtual-id.js.map +1 -0
  185. package/llms.txt +17 -14
  186. package/package.json +60 -50
  187. package/scripts/build-client.mjs +5 -3
  188. package/scripts/check-host-shadow.mjs +19 -2
  189. package/scripts/check-settings-form.mjs +933 -0
  190. package/scripts/check-shares.mjs +397 -0
  191. package/scripts/check-upload-endpoint.mjs +59 -3
  192. package/scripts/sync-doc-version.mjs +8 -15
  193. package/scripts/verify-fingerprint-live.mjs +111 -0
  194. package/src/activity.ts +218 -0
  195. package/src/admin-routes.ts +627 -0
  196. package/src/client/MobileAttachButton.tsx +2 -2
  197. package/src/client/MobileAttachChips.tsx +3 -3
  198. package/src/client/MobileDrawerFooter.tsx +7 -4
  199. package/src/client/MobileHome.tsx +20 -16
  200. package/src/client/MobileHomeChips.tsx +16 -16
  201. package/src/client/MobileNavOverlay.tsx +2 -2
  202. package/src/client/MobileNavToggle.tsx +3 -3
  203. package/src/client/MobileSessionHeader.tsx +63 -42
  204. package/src/client/MobileSessionInfo.tsx +118 -17
  205. package/src/client/RemoteComposerBanner.tsx +76 -0
  206. package/src/client/RemoteHeaderIcon.tsx +103 -0
  207. package/src/client/RemoteShareMenu.tsx +57 -0
  208. package/src/client/RemoteStatusIcon.tsx +91 -0
  209. package/src/client/compat/desktop.ts +22 -0
  210. package/src/client/compat/types.ts +62 -0
  211. package/src/client/current-session-report.ts +164 -0
  212. package/src/client/effects/header-status.ts +21 -17
  213. package/src/client/effects/keyboard-guard.ts +65 -1
  214. package/src/client/effects/model-group-side.ts +132 -0
  215. package/src/client/effects/permission-sheet.ts +149 -0
  216. package/src/client/effects/remote-session.ts +68 -0
  217. package/src/client/effects/turn-fold.ts +52 -0
  218. package/src/client/index.tsx +158 -36
  219. package/src/client/locales.ts +396 -2
  220. package/src/client/remote-fetch.ts +125 -0
  221. package/src/client/remote-group-expansion.ts +190 -0
  222. package/src/client/remote-share-css.ts +54 -0
  223. package/src/client/remote-share-register.ts +244 -0
  224. package/src/client/remote-status-css.ts +64 -0
  225. package/src/client/remote-status-register.ts +107 -0
  226. package/src/client/settings/SettingsSection.tsx +1229 -0
  227. package/src/client/settings/register-settings.ts +116 -0
  228. package/src/client/settings/settings-css.ts +236 -0
  229. package/src/client/share/share-preview.tsx +6 -3
  230. package/src/client/share/share-sheet.tsx +3 -3
  231. package/src/client/styles/composer.css.ts +119 -26
  232. package/src/client/styles/header.css.ts +7 -7
  233. package/src/client/styles/info.css.ts +74 -0
  234. package/src/client/styles/remote-session.css.ts +274 -0
  235. package/src/client-data/current-session.ts +57 -0
  236. package/src/client-data/model-group-side.ts +65 -0
  237. package/src/client-data/remote-group-expansion.ts +145 -0
  238. package/src/client-data/remote-status.ts +374 -0
  239. package/src/client-data/settings-form.ts +1371 -0
  240. package/src/client-data/shares.ts +467 -0
  241. package/src/client-pairing.ts +244 -0
  242. package/src/client-routes.ts +732 -0
  243. package/src/config.ts +447 -0
  244. package/src/fetch-route-intercept.ts +510 -0
  245. package/src/fingerprint.ts +410 -0
  246. package/src/http.ts +68 -0
  247. package/src/index.ts +719 -62
  248. package/src/intercept-shape.ts +87 -0
  249. package/src/intercept.ts +3510 -0
  250. package/src/merge-streams.ts +1279 -0
  251. package/src/relay-access.ts +672 -0
  252. package/src/relay-client.ts +1514 -0
  253. package/src/relay-filter.ts +291 -0
  254. package/src/relay-server.ts +2063 -0
  255. package/src/restart-fields.ts +36 -0
  256. package/src/restart-watcher.ts +140 -0
  257. package/src/session-reference.ts +156 -0
  258. package/src/share-export.ts +26 -1
  259. package/src/share-ops.ts +114 -0
  260. package/src/share-store.ts +373 -0
  261. package/src/virtual-id.ts +74 -0
package/README.md CHANGED
@@ -1,10 +1,17 @@
1
1
  <h1 align="center">dsh-zen-remote</h1>
2
- <p align="center">Turn DeepSeek Harness into a phone app you can safely reach from the public internet: a mobile UI, a pairing-code gateway, install-to-home-screen, and lock-screen push.</p>
2
+ <p align="center">One DeepSeek Harness plugin, two roles. The <b>host</b> turns DSH into a phone-reachable PWA (mobile UI, pairing gateway, lock-screen push) and shares chosen sessions to paired desktops. The <b>client</b> runs inside another DSH desktop app and puts the host's shared sessions right into its own sidebar — full history, live progress, messages, approvals, terminal, everything executes on the host.</p>
3
3
 
4
4
  <p align="center">
5
+ <a href="https://www.npmjs.com/package/dsh-zen-remote"><img src="https://img.shields.io/npm/v/dsh-zen-remote?style=flat-square&color=5B4CF0&logo=npm&label=npm" alt="npm version"></a>
6
+ <a href="https://www.npmjs.com/package/dsh-zen-remote"><img src="https://img.shields.io/npm/dm/dsh-zen-remote?style=flat-square&color=0B7285&label=downloads" alt="npm downloads"></a>
7
+ <a href="https://github.com/KyoMio/dsh-zen-remote/actions/workflows/publish.yml"><img src="https://img.shields.io/github/actions/workflow/status/KyoMio/dsh-zen-remote/publish.yml?style=flat-square&label=publish&logo=githubactions&logoColor=white" alt="publish workflow"></a>
5
8
  <a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-0B7285?style=flat-square" alt="MIT"></a>
6
- <img src="https://img.shields.io/badge/release-v1.1.16-5B4CF0?style=flat-square" alt="v1.1.16">
7
- <img src="https://img.shields.io/badge/DSH-Web%20Profile-5B4CF0?style=flat-square" alt="DSH Web Profile">
9
+ <br>
10
+ <img src="https://img.shields.io/badge/DeepSeek%20Harness-0.1.7%20%E2%80%93%200.2.x-4D6BFE?style=flat-square" alt="DeepSeek Harness 0.1.7 – 0.2.x">
11
+ <img src="https://img.shields.io/node/v/dsh-zen-remote?style=flat-square&color=339933&logo=nodedotjs&logoColor=white" alt="Node">
12
+ <a href="https://www.npmjs.com/package/dsh-zen-remote#provenance"><img src="https://img.shields.io/badge/npm-provenance%20signed-CB3837?style=flat-square&logo=npm&logoColor=white" alt="npm provenance"></a>
13
+ <img src="https://img.shields.io/badge/PWA-Web%20Push-5A0FC8?style=flat-square&logo=pwa&logoColor=white" alt="PWA + Web Push">
14
+ <img src="https://img.shields.io/badge/TypeScript-strict-3178C6?style=flat-square&logo=typescript&logoColor=white" alt="TypeScript strict">
8
15
  </p>
9
16
 
10
17
  <p align="center"><a href="README.zh-CN.md">中文文档</a></p>
@@ -17,215 +24,233 @@
17
24
  | --- | --- |
18
25
  | ![Composer permission sheet](assets/sheet.png) | ![Pairing page](assets/pairing.png) |
19
26
 
20
- > Screenshots are a 390×844 phone viewport in the light theme; both themes are supported. The pairing page is drawn by the gateway itself and is always dark.
27
+ > Screenshots are a 390×844 phone viewport in the light theme; both themes are supported. The pairing page is drawn by the gateway in DSH's official style, following the system light / dark appearance.
21
28
 
22
29
  ---
23
30
 
24
- ## Install
31
+ ## One plugin, two roles
25
32
 
26
- ```sh
27
- dsh plugin add dsh-zen-remote
28
- ```
33
+ Same package, same plugin row. What runs is decided by the `role` setting in the DSH plugin settings page:
29
34
 
30
- Restart `dsh web` afterwards. The mobile UI and the gateway both come up — there is no config line to hand-write.
35
+ | | **Host** (`role: host`, the default) | **Client** (`role: client`) |
36
+ | --- | --- | --- |
37
+ | Machine | The one that runs the sessions | Another computer running the DSH desktop app |
38
+ | What it loads | Mobile UI, gateway (child process), Web Push, session sharing, the relay server | The relay client and the sidebar/session-page remote parts |
39
+ | What it does NOT load | — | No gateway, no push, no phone UI — it never fights the host for the gateway port |
31
40
 
32
- > **Compatibility — one build, both runtimes.** Verified live on DSH `0.1.1-rc.2` and `0.1.2-rc.1` (web profile), 2026-09-04.
33
- >
34
- > 0.1.2 removed `@deepseek-ai/dsh-client-runtime` and put the web UI behind a signed cookie, which would have broken the mobile UI and locked the phone out of the gateway. The plugin carries its own store engine instead of importing that package, probes for the services 0.1.2 moved (`uiWorkspace`, `uiSession`) and falls back to the 0.1.1 shapes when they are absent, reads a turn's events through either `session.events` or `session.snapshotEvents()`, and lets the gateway do the browser-token handshake on the phone's behalf — all of it inert on 0.1.1, where the token endpoint does not exist.
35
- >
36
- > One thing to know on 0.1.2: the web UI now rejects requests whose `Host` is not trusted. Reaching DSH through this plugin's gateway is unaffected (the gateway presents every request as `127.0.0.1`), but pointing a browser straight at `http://<LAN-IP>:3080` returns 403 unless you start `dsh web --trusted-host <host>`.
41
+ A paired **desktop client** sees exactly the sessions the host has turned remote access on for, grouped under "server name · workspace name" after the local workspaces. A server workspace only appears once at least one of its sessions has remote access on, so a workspace you never touched never shows up as an empty group; a group that was already showing and later loses all its sessions stays as an empty group until the page is reloaded. Local sessions on the client machine are untouched.
37
42
 
38
- To uninstall: `dsh plugin remove dsh-zen-remote` (or delete the two lines from your profile's `dependencies` and `bundles`) and restart `dsh web`. To also wipe the pairing data, delete `~/.dsh/lan-gate-state.json` and `~/.dsh/lan-gate.config.json`.
43
+ ---
39
44
 
40
- <details>
41
- <summary>Manual install / local development</summary>
45
+ ## Remote connection
46
+
47
+ **What it is for**: one always-on computer runs the sessions (the host), and the DSH desktop app on another computer (the client) watches and carries on with them remotely — history, live progress, messages, approvals, the terminal all work, and everything executes on the host. Phones and tablets take a different road: a browser reaching the host through the gateway (a web app device), see [Setting up public access](#setting-up-public-access-host).
48
+
49
+ **How it works**, in four steps:
50
+
51
+ 1. **Pair**: on the host's settings page, generate a 桌面应用端 (desktop app device) pairing code; on the client's settings page, enter the server address and the code. Step by step in the [settings page walkthrough](#settings-page-walkthrough) below.
52
+ 2. **Turn remote access on**: remote access is per session. On the host, pick 开启远程 (turn remote on) from the session's "…" menu (or use the globe icon in the title row, or the list in the settings page — see [Session sharing](#session-sharing)).
53
+
54
+ ![The host turning remote access on from a session's "…" menu](docs/images/host-share-menu.png)
55
+
56
+ 3. **Use it on the client**: after the local workspaces, the sidebar shows a "host device name · workspace name" group; sessions in it open like local ones, and the globe icon in the title row shows the connection state (online / offline / version differs). A host workspace with no remote-enabled session never shows up.
57
+
58
+ ![The client sidebar with the "书房 · Desktop" group and the remote status icon in the title row](docs/images/client-sidebar.png)
59
+
60
+ 4. **Closing and coming back**: when the host closes remote access, the session idle-sleeps (48 hours without activity by default), or you close it from the client, an open session page shows a "remote closed" banner with input disabled, and the session hides from the list once you switch away; turning remote access back on brings it back. During a disconnect the page holds still and resumes by itself once the link is back.
61
+
62
+ ![The "remote closed" banner above the composer after remote access was closed](docs/images/client-remote-closed.png)
63
+
64
+ **Toggle it from your phone too**: when a phone (web app device) opens a session on the host, the session info card has a 远程访问 (remote access) row showing whether it is on and the idle time left; the switch turns it on or off.
65
+
66
+ <p align="center"><img src="docs/images/mobile-info-remote.png" alt="The remote access switch in the phone's session info card" width="320"></p>
67
+
68
+ What it can and cannot do: see [Features](#features) and [Known limitations](#known-limitations). A paired desktop client is treated as a trusted device — the trust boundary is in [Security model](#security-model).
69
+
70
+ > In these screenshots the red frames mark what this plugin adds to the DSH interface; session titles are demo names and conversation content is blurred. The UI is shown in Chinese.
71
+
72
+ ---
73
+
74
+ ## Install
75
+
76
+ **From the UI** (desktop app and web UI alike; the desktop app is how a 2.0 host actually runs): open the **Plugins** page in the sidebar → **Add plugin** → type `dsh-zen-remote` → click **Enable now** when the install finishes, then restart the desktop app (or `dsh web`).
77
+
78
+ **Web service from the command line** (host on a headless `dsh web` box, also where phone access still points):
79
+
80
+ ```sh
81
+ dsh plugin --profile web add dsh-zen-remote
82
+ # then restart dsh web
83
+ ```
84
+
85
+ `dsh plugin` requires `--profile` to name the profile to install into and errors out without it. The desktop app's `desktop` profile is managed exclusively by the app — the CLI refuses `--profile desktop` — so on the desktop app use the UI above.
42
86
 
43
- Edit `~/.dsh/profiles/web/package.json` by hand — one line under `dependencies`, one under `bundles`:
87
+ **Editing the profile by hand** (local development, `link:` installs): add the dependency to the profile's `package.json` (`~/.dsh/profiles/desktop` for the desktop app, `~/.dsh/profiles/web` for the web service), install, restart:
44
88
 
45
89
  ```jsonc
90
+ // ~/.dsh/profiles/desktop/package.json
46
91
  {
47
92
  "dependencies": {
48
- "dsh-zen-remote": "^1.1.16" // for local development: "link:/path/to/dsh-zen-remote"
49
- },
50
- "dsh": { "profile": { "bundles": [
51
- "@deepseek-ai/dsh-base",
52
- "@deepseek-ai/dsh-web-app",
53
- "dsh-zen-remote"
54
- ] } }
93
+ "dsh-zen-remote": "^2.0.1" // for local development: "link:/path/to/dsh-zen-remote"
94
+ }
55
95
  }
56
96
  ```
57
97
 
58
98
  ```sh
59
- cd ~/.dsh/profiles/web && pnpm install
60
- # restart dsh web
99
+ cd ~/.dsh/profiles/desktop && pnpm install
100
+ # then restart the DSH desktop app (or dsh web)
61
101
  ```
62
102
 
63
- If you would rather not go through the profile install flow at all, see the static mount in [`cordis.patch.yml.example`](cordis.patch.yml.example).
103
+ `pnpm install` only installs the package; it does not switch it on. Turn it on in the **Plugins** page, or append `"dsh-zen-remote"` to the end of the `dsh.profile.bundles` array in the same file (leave the existing entries alone). The UI and `dsh plugin` write that entry for you; a hand edit has to add it itself.
104
+
105
+ Either way there is exactly **one** bundle row to mount. Since 2.0.0 the composition layer is a single line — `dsh-zen-remote` — and the main entry loads the gateway and push sub-plugins itself, according to the role. Do **not** re-add the old `dsh-zen-remote-gateway` / `dsh-zen-remote-push` rows: the loader warns and skips them, and two gateway rows would mean two gateway processes fighting over port 3088.
106
+
107
+ <details>
108
+ <summary>Static mount / local development</summary>
109
+
110
+ Copy [`cordis.patch.yml.example`](cordis.patch.yml.example) into your profile's `cordis.patch.yml` with an absolute path — it is the same single row, with an optional `config:` block showing the row fields. A normal install from the UI or with `dsh plugin` never needs it.
64
111
 
65
112
  </details>
66
113
 
114
+ To uninstall: open dsh-zen-remote in the **Plugins** page and click **Uninstall**, then confirm; on the web service you can also run `dsh plugin --profile web remove dsh-zen-remote`; for a hand-edited install, delete it from the profile's `dependencies` and `dsh.profile.bundles`. Then restart. To also wipe pairing and share state, delete `~/.dsh/lan-gate-state.json`, `~/.dsh/lan-gate.config.json`, `~/.dsh/zen-remote-shares.json` and `~/.dsh/zen-remote-server.json` (the server id — once it is gone, paired clients see a brand-new server).
115
+
67
116
  ---
68
117
 
69
- ## Setting up public access
118
+ ## Upgrading from 1.x
70
119
 
71
- Once installed, the mobile UI already works at `127.0.0.1:3080` on the machine itself. To reach it from outside, three steps.
120
+ One-time facts, in order of likelihood to bite:
72
121
 
73
- ### 1. Put a reverse proxy in front for HTTPS
122
+ - **The three bundle rows collapse into one.** Old `dsh-zen-remote-gateway` / `dsh-zen-remote-push` rows left in a profile patch are warned about and skipped by the loader. Any `config:` you had written **on those old rows** no longer applies — move it onto the `dsh-zen-remote` row or, better, set it in the plugin settings page.
123
+ - **If you hand-inserted a `dsh-zen-remote/dsh-push.mjs` line into a profile patch back in 1.0, remove it** — the main entry loads the push half by role now, and a leftover line loads push twice.
124
+ - **`~/.dsh/lan-gate.config.json` still works.** Nothing to migrate; its values show up in the settings page. Priority per field: environment variable > plugin row settings > `lan-gate.config.json` > built-in default. The file is never written or deleted by the plugin.
125
+ - **Paired devices become Web 应用端 automatically** and keep working; no re-pairing.
126
+ - **The admin page moved.** `/lan-gate/admin` is now just a notice pointing at the plugin settings page (it still 403s anything that is not a direct local connection). Pairing codes, device management, push test and sharing all live in the DSH Plugins page → dsh-zen-remote settings block.
127
+ - With no configuration touched at all, the plugin comes up as a host and the gateway, push and phone UI behave exactly as 1.1.x did.
74
128
 
75
- The gateway listens on `127.0.0.1:3088` only — exposing it is your reverse proxy's job. **If your home connection has no public IP, or you'd rather not open a port on your router**, skip nginx/Caddy and jump to the third block (Cloudflare Tunnel).
129
+ ---
76
130
 
77
- <details open>
78
- <summary><b>nginx</b></summary>
131
+ ## Setting up public access (host)
79
132
 
80
- ```nginx
81
- # once, inside the http {} block
82
- map $http_upgrade $connection_upgrade { default upgrade; '' close; }
133
+ Once installed, the mobile UI already works at `127.0.0.1:3080` on the machine itself. To reach it from outside: put your own TLS-terminating reverse proxy in front of the gateway, verify it with a 403 self-check, then pair devices. The full walkthroughs — nginx / Caddy / Cloudflare Tunnel / Lucky — live in [docs/remote-access.en.md](docs/remote-access.en.md). The short form:
83
134
 
84
- server {
85
- listen 443 ssl http2;
86
- server_name dsh.example.com;
135
+ 1. The gateway listens on `127.0.0.1:3088` only; a reverse proxy (nginx, Caddy, Cloudflare Tunnel, Lucky) terminates HTTPS and forwards to it. **Self-check**: from **mobile data** (not home Wi-Fi), open `https://your-domain/lan-gate/admin` — the correct result is a **403**. If you can see the notice page, your proxy is not forwarding `X-Forwarded-*` headers and public requests are being treated as local ones — fix that before continuing.
136
+ 2. Open the DSH Plugins page **on the host machine**, open the dsh-zen-remote settings block, pick the device role for the code you are about to mint, and generate an 8-character pairing code (valid 10 minutes, single use).
137
+ 3. On the phone, open your HTTPS domain and enter the code on the pairing page. Once paired you land in DSH; identity lives in a long-lived cookie. Use "Add to Home Screen" to install it as an app, grant notification permission, and the agent reaches your lock screen when it needs you.
87
138
 
88
- ssl_certificate /etc/letsencrypt/live/dsh.example.com/fullchain.pem;
89
- ssl_certificate_key /etc/letsencrypt/live/dsh.example.com/privkey.pem;
139
+ ---
90
140
 
91
- location / {
92
- proxy_pass http://127.0.0.1:3088;
93
- proxy_http_version 1.1;
141
+ ## Pairing devices
94
142
 
95
- proxy_set_header Upgrade $http_upgrade;
96
- proxy_set_header Connection $connection_upgrade;
143
+ A pairing code is minted **for one device role**, and the role is checked at redemption — a code offered to the wrong channel is refused with a clear message, is not consumed, and does not count toward the wrong-code lockout.
97
144
 
98
- proxy_set_header Host $host;
99
- proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
100
- proxy_set_header X-Forwarded-Proto $scheme;
145
+ | | **Web 应用端 (web app device)** | **桌面应用端 (desktop app device)** |
146
+ | --- | --- | --- |
147
+ | Shape | Phones, tablets, browsers — anything through the gateway | A computer running the DSH desktop app with this plugin in its `client` role |
148
+ | Sees | **All** server sessions | **Only** the sessions the host has enabled remote access on (enforced server-side) |
149
+ | How to pair | Open the gateway URL in a browser, enter the code on the pairing page | Fill the server URL + code into the client's plugin settings page |
101
150
 
102
- proxy_buffering off;
103
- proxy_read_timeout 3600s;
104
- }
105
- }
106
- ```
151
+ **Desktop-client pairing**, step by step:
107
152
 
108
- </details>
153
+ 1. On the host's plugin settings page, choose 桌面应用端 and generate a code.
154
+ 2. On the other computer's DSH desktop app, open the plugin settings page, set the role to **client**, enter the server address and the code.
155
+ 3. The address must be `https://` unless it is a private-network host — LAN ranges (`192.168.x`, `10.x`, `172.16–31.x`), Tailscale/CGNAT (`100.64.x`), loopback, `localhost` / `*.local`, or IPv6 `::1` / `fc00::/7` / `fe80::/10`. A public address over plain http is refused, so the device token never travels in the clear.
156
+ 4. On success the token is stored in the row (a secret field) and the client connects automatically. The settings block shows the connection state — connected / offline / version mismatch / token revoked — and can unpair.
109
157
 
110
- <details open>
111
- <summary><b>Caddy</b></summary>
158
+ The server-side settings page can rename devices, change a device's role (`set-role`), and revoke one or all. Revocation is immediate: open connections and push subscriptions die with it. Desktop-client devices can never reach any admin route — the gateway only ever forwards them into the relay prefix.
112
159
 
113
- ```
114
- dsh.example.com {
115
- reverse_proxy 127.0.0.1:3088
116
- }
117
- ```
160
+ **Device name**: the 设备名称 (device name) field in the settings page's role card — the `serverName` config — is shared by both roles. On a host it is the name that titles the group in other devices' sidebars (default: the computer's name); on a client it is the name registered with the server when pairing. The name syncs both ways by itself: the server renaming itself reaches the clients' group titles in about 15 seconds; a client saving a new name updates the server's device list; the server renaming a client in its device list reaches that client in about 30 seconds. A client pushes its name only when you explicitly save the settings — everything else defers to the server's record, and a rename made while offline is dropped if the process exits before it can be pushed. Names are 1–40 characters.
118
161
 
119
- </details>
162
+ ---
120
163
 
121
- <details open>
122
- <summary><b>No public IP? Use a Cloudflare Tunnel</b></summary>
164
+ ## Session sharing
123
165
 
124
- For when your home connection gets no public IP, or you don't want to open a router port: `cloudflared` dials out from your machine, and Cloudflare handles the domain, the certificate and the entry point. Not a single port needs opening. The free tier is enough.
166
+ Remote access is per session, with four toggle entries:
125
167
 
126
- Prerequisite: the domain is hosted on Cloudflare (nameservers pointed at it).
168
+ 1. 开启远程 / 关闭远程 (remote on / off) in the sidebar session row's "…" menu;
169
+ 2. the remote icon in the session title row (click to toggle, with a confirmation; hover shows the remaining idle time; a dot on the icon means a desktop client is watching right now);
170
+ 3. the shared-sessions list in the plugin settings page, including a close-all;
171
+ 4. the 远程访问 (remote access) switch in the phone's (web app device's) session info card.
127
172
 
128
- 1. Open the [Zero Trust dashboard](https://one.dash.cloudflare.com/) → **Networks → Tunnels → Create a tunnel** → pick **Cloudflared**, name it. After creating it the page hands you an install command with a token in it;
129
- 2. Run that command on the machine running DSH (it looks like this, with the token from the page):
173
+ **Idle sleep**: a remote-enabled session closes its remote access by itself after **48 hours** (configurable, `idleHours`) with no session activity — turns starting or ending, messages, approvals or question answers, from either the server or a client. Sessions that are mid-turn or waiting on an approval/question are never swept, and merely having a viewer open does not keep one awake. The remaining time is hoverable on the title-row icon and listed in the settings page.
130
174
 
131
- ```sh
132
- # macOS / Linux: install as a service that starts at boot
133
- cloudflared service install eyJhIjoi...your-token
134
- ```
175
+ **New sessions**: with `autoShareNewSessions` on, every session the host creates is shared automatically; sessions created **through** the relay (from a client's remote group) are always auto-shared, which is what makes the entry safe. Subagent and fork sessions follow their parent — no per-session toggling.
135
176
 
136
- 3. Back on the tunnel's detail page → **Public Hostname** → **Add a public hostname**:
177
+ **What "remote off" means**: an unshared session's list row, history, live progress and approval/question events are all refused or filtered at the relay — not merely hidden in the UI. @-mention candidates list **only shared sessions**, and a `dsh-session:` reference embedded in a message, a queue edit or a slash command is refused for the whole call when it names an unshared session.
137
178
 
138
- | Field | Value |
139
- | --- | --- |
140
- | Subdomain / Domain | `dsh` / `example.com` (i.e. `dsh.example.com`) |
141
- | Service Type | `HTTP` |
142
- | URL | `127.0.0.1:3088` |
179
+ **What a closed remote looks like on the client**: when the server closes a session's remote access (or the session idle-sleeps), the session is hidden from the client's sidebar list — it counts as archived, so DSH's "show archived" filter brings it back into view. The one exception is the session page you have open right now: it stays where it is, a "remote closed" banner with the reason (server closed / idle sleep / turned off on this machine) appears above the composer, and input is disabled; once you switch to another session, it hides too. When the server turns remote access back on (or re-shares), the session returns to its original workspace and an open page recovers by itself — the banner clears and input comes back.
143
180
 
144
- Save, and `https://dsh.example.com` is live with a Cloudflare-issued certificate. Leave the gateway's `LAN_GATE_HOST` at its `127.0.0.1` default — `cloudflared` runs on the same machine.
181
+ **Disconnects and recovery**: while the server (or the link) is down, an open remote session page holds its ground — the banner says offline and input is disabled; once the connection is back the page resumes by itself: the session stream and task-list-type panels pull a fresh snapshot automatically, no manual refresh needed. The exceptions are the file-tree changes and terminal panels, which must be reopened after a disconnect.
145
182
 
146
- **Step 2's 403 self-check is mandatory**, and it matters most for a tunnel: `cloudflared` reaches the gateway over a loopback connection, so the only thing separating "a visitor from the internet" from "you, sitting at this computer" is whether the tunnel forwards `X-Forwarded-For`. `cloudflared` does by default, so the pairing wall works; but if your version or config strips it, public requests get treated as the local admin and the pairing wall is decorative. **Load `/lan-gate/admin` over mobile data — a 403 is the only safe answer.**
183
+ ---
147
184
 
148
- > Note: you do not need `LAN_GATE_TRUSTED_PROXIES` here. The gateway already trusts a loopback connection as a reverse proxy, so setting it to `127.0.0.1` is a no-op — and it is **not** a substitute for the self-check above.
149
- >
150
- > Cloudflare's free tier supports WebSockets (DSH's conversation stream needs them) and caps a request body at 100MB, above this plugin's 20MB upload default, so nothing is affected.
185
+ ## Configuration
151
186
 
152
- The CLI route (`cloudflared tunnel login` / `create` / `route dns` plus an `ingress` block in `config.yml`) is in [docs/remote-access.en.md](docs/remote-access.en.md#cloudflare-tunnel--getting-in-without-a-public-ip).
153
- </details>
187
+ ### Settings page walkthrough
154
188
 
155
- For Lucky (routers/NAS), see [docs/remote-access.en.md](docs/remote-access.en.md#lucky). When the proxy and the gateway are on different machines, put the proxy's egress IP in `LAN_GATE_TRUSTED_PROXIES`.
189
+ **Where it is**: in DSH, click 插件 (Plugins) in the left bar → click dsh-zen-remote in the plugin list → click dsh-zen-remote once more under 包含的组件 (components); the settings block is below.
156
190
 
157
- ### 2. Self-check
191
+ **Common controls**: 已覆盖 (overridden) at a field's top right means the value is saved in the plugin settings (overriding the config file or the default); 重置 (reset) falls back to the next layer. Click 保存 (save) at the bottom of the card when done. Fields that need a reload say so, and the plugin reloads itself after saving — no app restart.
158
192
 
159
- From **mobile data** (not your home Wi-Fi), open `https://your-domain/lan-gate/admin`. The correct result is a **403**.
193
+ **Host**
160
194
 
161
- If you can see the admin page, your proxy isn't sending the `X-Forwarded-*` headers and public requests are being treated as local ones. Go back and fix the forwarded headers before continuing.
195
+ 1. Set 运行角色 (role) to 主服务端 (host) and give it a recognizable 设备名称 (device name), e.g. "书房" (study) — it titles the group in clients' sidebars and shows on the phone. Save.
162
196
 
163
- ### 3. Pair a device
197
+ ![Host settings: role and device name in the role card](docs/images/host-settings-role.png)
164
198
 
165
- ```sh
166
- # on the machine running DSH, in a browser on that machine
167
- open http://127.0.0.1:3088/lan-gate/admin
168
- ```
199
+ 2. 网关与反代 (gateway and reverse proxy): keep the defaults for local use and phone access through a proxy; for public access see [Setting up public access](#setting-up-public-access-host). For clients on the LAN to reach the gateway **directly**, set 监听地址 (listen address) to `0.0.0.0` (it must include loopback — the settings page manages the gateway over loopback).
200
+ 3. 配对 (pairing): choose Web 应用端 (phones, browsers) or 桌面应用端 (another DSH desktop app), click 生成配对码 (generate code) and enter it on the other device within 10 minutes. The 设备 (devices) list renames devices, changes their role and revokes them.
169
201
 
170
- 1. Click "Generate pairing code" for an 8-character code (valid 10 minutes, single use);
171
- 2. On the phone, open your HTTPS domain and enter the code on the pairing page;
172
- 3. Once paired you land in DSH. Identity lives in a long-lived cookie, so switching networks doesn't log you out;
173
- 4. Use the browser's "Add to Home Screen" to install it as an app;
174
- 5. Grant notification permission, and the agent reaches your lock screen when it needs you.
202
+ ![Generating a pairing code, and the device list](docs/images/host-pairing.png)
175
203
 
176
- The admin page can also rename devices, change a device's type, and revoke devices one at a time or all at once.
204
+ 4. 远程共享 (remote sharing): the idle-sleep window and whether new sessions are shared automatically. Below the card, 已开启远程的会话 (remote-enabled sessions) lists what is shared right now, the idle time left and whether a device is watching; close them one by one or all at once.
177
205
 
178
- ---
206
+ ![The remote-enabled sessions list](docs/images/host-settings-shares.png)
179
207
 
180
- ## Optional configuration
208
+ 5. 推送 (push): push summaries, turn-end pushes and so on, as you like — see [When notifications fire](#when-notifications-fire).
181
209
 
182
- Environment variables, or `~/.dsh/lan-gate.config.json` (keys are the variable names minus the prefix, camelCased — `port`, `trustedProxies`; an explicit env var wins). Restart `dsh web` after changing anything.
210
+ **Client**
183
211
 
184
- | Variable | Default | What it does |
185
- | --- | --- | --- |
186
- | `LAN_GATE_PORT` | `3088` | Gateway port; if taken it retries upward (up to +20) |
187
- | `LAN_GATE_HOST` | `127.0.0.1` | Listen address; only open this up when the proxy is on another machine |
188
- | `LAN_GATE_TARGET_PORT` | `3080` | Local DSH Web UI port |
189
- | `LAN_GATE_RATE_LIMIT` | `120` | Per-minute cap on unpaired requests, counted per real client IP |
190
- | `LAN_GATE_TRUSTED_PROXIES` | empty | Comma-separated IPs; required when the proxy is on another machine |
191
- | `LAN_GATE_VAPID_SUBJECT` | `mailto:admin@localhost` | Push contact. **On iOS this must be a real email or https URL**, or Apple refuses to deliver |
192
- | `LAN_GATE_LANG` | `auto` | Language of the pairing page, the admin page and the push opt-in card. `auto` follows the browser's `Accept-Language` (falling back to Chinese when there is none); `zh`/`en` pin it |
193
- | `DSH_PUSH_TURN_END` | **off** | Set `1` to also push when a turn ends. Off by default — a finished turn doesn't mean you're needed (it pushed by default before 1.0.3; this was a behaviour change). Approval-pending and question-pending notifications are unaffected and always fire |
194
- | `DSH_PUSH_EVENTS` | `agent/turn-stopping` | Which events count as "turn ended", comma-separated; only meaningful with `DSH_PUSH_TURN_END=1` |
195
- | `DSH_PUSH_DEBOUNCE_MS` | `15000` | Minimum gap between two automatic pushes; approval/question notifications are never suppressed by it |
196
- | `DSH_PUSH_SUMMARY` | off | Set `1` to include this turn's final reply (prose only, never the reasoning; clipped to 120 chars) and the question text |
197
- | `DSH_PUSH_TOOL` | on | Set `0` to remove the model-callable `push_notify` tool |
198
- | `DSH_PUSH_LANG` | `zh` | Language of the notification copy. A notification carries no signal about who will read it, and the host process has no reliable system locale either (launchd starts it without `LANG`), so this is not autodetected: set `en` for English |
199
- | `DSH_PUSH_APPROVAL_GRACE_MS` | `5000` | How long to wait before pushing "approval pending". With a plugin that answers approvals automatically (dsh-auto-approve and the like) installed, the push waits for its verdict — answered means no push. A slower judge still gets pushed over, so raise this if yours is |
200
-
201
- The upload size limit (20MB by default) is `config.maxUploadBytes` on the plugin row.
202
-
203
- To enable turn-process folding on desktop too (it only applies at phone widths by default), set `config.turnFoldDesktop` to `true` on the plugin row — that is, add this to the profile's `cordis.patch.yml`:
204
-
205
- ```yaml
206
- - id: dsh-zen-remote
207
- config:
208
- turnFoldDesktop: true
209
- ```
212
+ 1. Set 运行角色 (role) to 子客户端 (client) and save — the plugin reloads and the page switches to the client's settings.
213
+ 2. Give this computer a 设备名称 (device name), e.g. "客厅" (living room); it is registered in the host's device list when pairing. Save.
214
+ 3. 连接服务端 (connect to server): enter the host's gateway address (on a LAN e.g. `http://192.168.1.10:3088`; public addresses must be `https://`) and a 桌面应用端 pairing code from the host, then click 配对 (pair).
215
+ 4. Done when 连接状态 (connection state) reads 已连接到「host name」 (connected to "host name"); the host's groups appear in the sidebar right away. The 诊断 (diagnostics) area lists the request interceptor state and recent remote-call failures — the first place to look when something is off.
210
216
 
211
- Restart `dsh web` afterwards. Without touching the server config, a single browser can also turn it on for itself by visiting `?mobile-nav-turn-fold=1` once (`=0` turns it off; remembered per browser).
217
+ ![Client settings: role, device name, server connection and connection state](docs/images/client-settings.png)
212
218
 
213
- **Three calibration values for the soft-keyboard lift.** On a few phones the system never tells the browser how tall the keyboard is (measured: certain third-party IMEs plus Chrome; the PWA shell Xiaomi's browser installs). With no measurable signal at all, the plugin has to lift the input box by estimate. The estimate is tuned to one reported device, so it may be too high or too low on yours — all three numbers are adjustable on the plugin row:
219
+ > Addresses and pairing codes in the screenshots are demo values.
214
220
 
215
- | Setting | Default | Meaning | Allowed range |
216
- | --- | --- | --- | --- |
217
- | `keyboardLiftRatio` | `0.42` | Estimate the lift as this fraction of screen height | 0 – 1 |
218
- | `keyboardLiftMaxPx` | `400` | Cap on the estimate (pixels), so the input box doesn't end up mid-screen on a tall phone | 0 – 2000 |
219
- | `keyboardSafetyPadPx` | `15` | A little extra clearance above the keyboard, **Android only**. Third-party IMEs routinely under-report their own height (leaving out the toolbar strip above the keys); this makes up for it | 0 – 200 |
221
+ ### Field reference
220
222
 
221
- ```yaml
222
- - id: dsh-zen-remote
223
- config:
224
- keyboardLiftRatio: 0.45
225
- keyboardSafetyPadPx: 30
226
- ```
223
+ Everything is edited in the DSH Plugins page → dsh-zen-remote settings block. Per field the first legal value wins in this order: **environment variable > plugin row settings > `~/.dsh/lan-gate.config.json` > default**. A value that fails its field's check (type, range, enum) makes that layer transparent. Saving a field that needs a gateway/push restart (`role`, `port`, `host`, `targetPort`, `rateLimit`, `trustedProxies`, `vapidSubject`, `lang`, and the push fields) reloads the plugin row automatically — no app restart required; everything else takes effect immediately.
227
224
 
228
- How to tune: if the box is lifted **too little** (the keyboard still covers part of it), raise `keyboardLiftRatio` in steps of 0.03; if it's lifted **too far** (a gap opens between the box and the keyboard), lower it. If it's off by only a little (tens of pixels, on Android), reach for `keyboardSafetyPadPx` first. Leaving all three unset keeps today's behaviour exactly; out-of-range values are clamped to the table above, so the input box can never be pushed off-screen. Phones that report properly take the measured path and none of these values affect them.
225
+ Row fields (matching `src/config.ts`):
226
+
227
+ | Field | Default | What it does |
228
+ | --- | --- | --- |
229
+ | `role` | `host` | `host` runs gateway + push + sharing; `client` connects to a server. Anything not exactly `client` degrades to `host` |
230
+ | `port` | `3088` | Gateway port; if taken it retries upward (up to +20) |
231
+ | `host` | `127.0.0.1` | Gateway listen address; only open this up when the proxy is on another machine |
232
+ | `targetPort` | *(auto)* | Local DSH Web UI port. Leave unset: the gateway discovers the host's real listening port (desktop builds use a configurable one) |
233
+ | `rateLimit` | `120` | Per-minute cap on unpaired requests, per real client IP |
234
+ | `trustedProxies` | empty | Comma-separated IPs; required when the proxy is on another machine |
235
+ | `vapidSubject` | `mailto:admin@localhost` | Push contact. **On iOS this must be a real email or https URL**, or Apple refuses to deliver |
236
+ | `lang` | `auto` | Language of the gateway's pages, the push opt-in card and the notification copy. `auto` follows the browser's `Accept-Language` (falling back to Chinese); `zh`/`en` pin it |
237
+ | `pushTurnEnd` | off | Also push when a turn ends. Off by default — a finished turn doesn't mean you're needed. Approval/question pushes are unaffected and always fire |
238
+ | `pushEvents` | `agent/turn-stopping` | Which events count as "turn ended", comma-separated; only meaningful with `pushTurnEnd` |
239
+ | `pushDebounceMs` | `15000` | Minimum gap between two automatic pushes; approval/question notifications are never suppressed by it |
240
+ | `pushSummary` | off | Include the turn's final reply (prose only, never the reasoning; clipped to 120 chars) and the question text in the body |
241
+ | `pushTool` | on | Set `false` to remove the model-callable `push_notify` tool |
242
+ | `serverName` | computer name | **Device name** (≤ 40 chars), shared by both roles: on a host it is the display name in clients' group titles; on a client it is the name registered when pairing. Syncs both ways with the server automatically |
243
+ | `idleHours` | `48` | Idle-sleep window in hours for remote-enabled sessions; range (0, 8760] |
244
+ | `autoShareNewSessions` | off | Auto-enable remote on every session the host creates |
245
+ | `serverUrl` *(client)* | empty | The server's gateway address, normalized per the rules above |
246
+ | `deviceToken` *(client)* | empty | The paired device token; stored as a secret field, redacted everywhere |
247
+ | `turnFoldDesktop` | off | Turn-process folding at every width, not just phone widths |
248
+ | `keyboardLiftRatio` / `keyboardLiftMaxPx` / `keyboardSafetyPadPx` | `0.42` / `400` / `15` | Soft-keyboard lift calibration for browsers that never report the keyboard (see the known-issues note below) |
249
+ | `maxUploadBytes` | 20 MB | Attachment upload cap |
250
+
251
+ The legacy environment variables keep working and outrank the row: `LAN_GATE_PORT`, `LAN_GATE_HOST`, `LAN_GATE_TARGET_PORT`, `LAN_GATE_RATE_LIMIT`, `LAN_GATE_TRUSTED_PROXIES`, `LAN_GATE_VAPID_SUBJECT`, `LAN_GATE_LANG`, `DSH_PUSH_TURN_END`, `DSH_PUSH_EVENTS`, `DSH_PUSH_DEBOUNCE_MS`, `DSH_PUSH_SUMMARY`, `DSH_PUSH_TOOL`, `DSH_PUSH_LANG` (notification-copy override; without it `lang` decides), and `DSH_PUSH_APPROVAL_GRACE_MS` (how long to wait before pushing "approval pending" so a faster auto-approver's verdict wins; default 5000; it and the file-only `pushApprovalGraceMs` key are not part of the settings surface). `LAN_GATE_RELAY_SECRET` is **not** settable: the plugin mints it per load and hands it to the gateway itself.
252
+
253
+ The upload-size, keyboard-calibration and turn-fold knobs behave exactly as in 1.x — just moved from "plugin row YAML" to "settings page" (a hand-edited row works too).
229
254
 
230
255
  ---
231
256
 
@@ -240,62 +265,79 @@ By default only when you are **actually needed**, along two independent lines.
240
265
  | A tool is waiting for your authorization | "DSH needs your approval", with the tool name |
241
266
  | The model called `ask_user_question` and is waiting | "DSH is waiting for your answer" |
242
267
 
243
- Neither looks at session depth — a subagent stuck on an approval still calls out, because it's still you it's waiting for. Neither is suppressed by `DSH_PUSH_DEBOUNCE_MS` either: "something needs your nod" is the one notification that must never be swallowed.
268
+ Neither looks at session depth — a subagent stuck on an approval still calls out, because it's still you it's waiting for. Neither is suppressed by the debounce either: "something needs your nod" is the one notification that must never be swallowed. Since 2.0.0 this includes sessions running inside the desktop app: the host-side hook fires wherever the session lives.
244
269
 
245
- **Timing when a machine answers**: approval events arrive as "record asked → consult the answerer → record decided", so the push doesn't fire the moment `asked` appears — it waits `DSH_PUSH_APPROVAL_GRACE_MS` (5 seconds by default) and skips anything answered within it. That window used to be 1.5s, designed around "every answerer settles in the same tick". True for a synchronous answerer, not for a model-backed one (measured: 2.4s average), and the result was a "needs your approval" push for a request that was auto-allowed — notification delivered, dialog never shown. Raise it if you switch to a slower judge.
246
-
247
- Approvals a policy waves through don't disturb you: after the request lands, the push waits 1.5s and cancels if the matching "decided" arrives. Only genuinely unattended ones go out.
270
+ **Timing when a machine answers**: approval events arrive as "record asked → consult the answerer → record decided", so the push waits `DSH_PUSH_APPROVAL_GRACE_MS` (5 seconds by default) and skips anything answered within it. Raise it if you run a slower model-backed approver (measured: 2.4s average). Approvals a policy waves through don't disturb you.
248
271
 
249
272
  **2. Decided by the model**
250
273
 
251
- The `push_notify` tool. The model should call it when you explicitly asked to be told when something finished, when it needs you to continue, or when something unexpected happened that you'd probably want to know right away. The tool description also spells out when *not* to call it (routine turn ends, progress reports, anything it can push forward on its own) — listing only the former turns it into a per-turn reflex. The same guidance is injected as standing session context, from one shared constant, so the two cannot drift apart.
252
-
253
- **What does not fire by default**
274
+ The `push_notify` tool. The model should call it when you explicitly asked to be told when something finished, when it needs you to continue, or when something unexpected happened that you'd probably want to know right away. The tool description also spells out when *not* to call it, and the same guidance is injected as standing session context from one shared constant, so the two cannot drift apart.
254
275
 
255
- - **A plain finished turn does not push** (changed in 1.0.3; before that every turn did). Getting work done isn't the same as needing you. Set `DSH_PUSH_TURN_END=1` for the old behaviour.
256
- - **A subagent finishing never pushes**, regardless of that switch.
276
+ **What does not fire by default**: a plain finished turn does not push (set `pushTurnEnd` / `DSH_PUSH_TURN_END=1` for the old behaviour); a subagent finishing never pushes, regardless of that switch.
257
277
 
258
- **What's in a notification**: by default the title only, with no conversation content. With `DSH_PUSH_SUMMARY=1` the body carries this turn's final reply — prose only, never the reasoning; a turn that produced no prose falls back to "Last executed: <tool>" rather than padding it out with thinking text. The push payload is aes128gcm end-to-end encrypted.
278
+ **What's in a notification**: by default the title only. With `pushSummary` the body carries this turn's final reply — prose only, never the reasoning; a turn that produced no prose falls back to "Last executed: <tool>". The push payload is aes128gcm end-to-end encrypted.
259
279
 
260
280
  ---
261
281
 
262
282
  ## Features
263
283
 
284
+ **Host (phone access, unchanged in spirit from 1.x)**
285
+
264
286
  - Two-level page stack — session list home plus a standalone session page, pushed in and out horizontally
265
287
  - Plugin entry chips on the home screen, appearing automatically for what you have installed, individually hideable
266
288
  - Reworked composer: controls become icons, the permission and model menus become bottom sheets
267
- - Session info card: six stats plus export log / rename / fork / archive
268
- - Reasoning and tool calls within one turn fold into a single "process · N steps" row by default
269
- - Gestures: swipe right from the left edge to go back, swipe down to dismiss a bottom sheet. Android's system back gesture is taken over as "close the overlay → back to the list → leave the app", instead of quitting the PWA on the first press
270
- - Local attachment upload from the phone: files land in the session's working directory under `.dsh-uploads/` and an `@` reference is appended to the composer — sending it is still your call
271
- - Share image export from the session info card: the whole conversation or the last 3/5/10 turns (counted by real prompts) as ONE long PNG — slices are only an internal render budget and are streamed-stitched into a single file, past the length cap the tail is kept and the omission marked, wide code lines and tables are never cropped sideways, assistant prose renders as a markdown subset, and the file goes through the system share panel with multi-file download as the legacy-browser fallback
272
- - A pairing code buys a long-lived device token; identity follows the token, not the IP, and can be revoked at any time
273
- - The admin surface (generate a code / manage devices / trigger a push) only accepts direct local connections; anything through the proxy gets a 403
274
- - A real PWA: manifest plus service worker, installable to the home screen, opens offline
275
- - Real Web Push: VAPID plus aes128gcm, no conversation body by default; fires only for approvals and questions, no longer on every turn end (see above)
276
- - The `push_notify` tool: the model can push at a moment that matters, rate-limited
277
- - The "internal testing notice" dialog gets a "don't show again" option: remote access re-shows the notice on every reload, and one click makes this device remember and dismiss it from then on
278
-
279
- In depth: [interface](docs/interface.md) · [public access](docs/remote-access.en.md)
289
+ - Session info card: six stats plus export log / rename / fork / archive; share-image export as ONE long PNG; since 2.0 a remote access switch row
290
+ - Turn-process folding, left-edge swipe back, Android back-gesture takeover, phone-local attachment upload
291
+ - A pairing code buys a long-lived device token; identity follows the token, not the IP, revocable at any time
292
+ - Real PWA + real Web Push (VAPID + aes128gcm), firing only for approvals and questions by default
293
+
294
+ **Host (2.0 additions)**
295
+
296
+ - Session sharing with four toggle entries (including the phone info card), idle sleep, auto-share and subagent/fork following
297
+ - Everything above works when DSH runs as the **desktop app**: the gateway forwards to the desktop backend's real port automatically, and approval/question pushes fire for desktop sessions
298
+
299
+ **Desktop client (2.0 additions)**
300
+
301
+ - Remote groups in the sidebar ("server name · workspace name"), remote sessions open like local ones: full history, live progress, messages, cancels, queue edits, approvals and question answers, model selection, file tree, changes list plus changes summary/diff, goals, slash commands, agent presets, subagent prompts/interrupts, attachments and @ references, terminals running server-side
302
+ - Remote sessions can attach **non-image files**: the upload request is caught by the sub-client's own backend and streamed through the relay to the server session; the 100 MiB cap is the relay's own (local uploads have no such limit), an over-limit pick is refused locally on the client with a readable message, and slow links are bounded by the 300-second timeout
303
+ - The model picker in a remote session lists the **server's** model catalog (groups named "server name · group name"), local sessions list only local models; the last-fetched server groups survive while the relay is offline; server groups never appear in local settings such as the vision router. Known boundary: a session created on the server that has never sent a message shows the local default model in the picker
304
+ - New sessions created inside a remote group run on the server's workspace and are shared automatically; remote sessions can be renamed, archived, pinned, forked, and their remote access closed (with confirmation) from the client
305
+ - Title-row connection icon per remote session (online / offline / version differs); a "remote closed" banner with the reason when a session is slept or closed server-side; offline groups go grey with input disabled and reconnect automatically with backoff
306
+ - First-come-first-served approvals/questions: if the server's own UI answers first, the client syncs to "already handled"
307
+
308
+ In depth: [interface](docs/interface.md) · [public access + relay protocol](docs/remote-access.en.md)
280
309
 
281
310
  ---
282
311
 
283
- ## Third-party plugins with mobile support
312
+ ## Version tolerance
284
313
 
285
- The mobile UI has specific adaptations for the plugins below. Every adaptation is anchored on that plugin's own DOM markers: if you don't have it installed the rules simply don't match, and installing a plugin that isn't listed here can't be caught in the crossfire.
314
+ Three layers, strictest first:
286
315
 
287
- | Plugin | What the mobile adaptation does | Version tested |
288
- | --- | --- | --- |
289
- | [dsh-better-sidebar](https://github.com/omdsh-dev/DSH-better-sidebar) | 0.19+: all its tabs live in DSH 0.1.5's native right sidebar, which the host takes full screen on a phone (padded clear of the notch / home indicator) — the session header keeps an entry button for it, the left-edge swipe-back closes it, and tapping an @-reference in the file tree closes it. ≤ 0.18 (own right panel, DSH < 0.1.5 only): full-width phone drawer with notch safe area and a centred close pill | 0.19.1 (legacy rules: 0.15.0) |
290
- | [@nanmicoder/dsh-agent-teams](https://github.com/NanmiCoder/dsh-agent-teams) | The AgentTeams activity overlay moves below the session header (its original position covered the header buttons) and hides itself on the session list; a subagent session keeps a tappable parent-session title in its header for jumping back | 0.1.9 |
291
- | [@ychris12138/dsh-usage-stats](https://github.com/Ychris12138/dsh-usage-stats) | Usage and balance entries fold into the home-screen chips row | 0.2.9 |
292
- | [@opendsh/dsh-plugin-scheduled-tasks](https://github.com/Ceelog/dsh-plugins) | The scheduled-tasks entry folds into the home-screen chips row | 0.2.3 |
293
- | dsh-at-file | `@` file references, used alongside attachment upload's `@` paths. It and this plugin's attachment chips read the same draft token, so on the phone its `.dsh-uploads/` rows are hidden to stop one file being drawn twice (thumbnail plus filename); other `@` references are left alone | 0.6.7 |
294
- | [@ace-zone/dsh-market](https://www.npmjs.com/package/@ace-zone/dsh-market) | The plugin market dialog's top bar doesn't fit on a phone and the × gets squeezed out of the panel (no Esc on a touchscreen, so it can't be closed at all). Three decorative slots — tagline, version, homepage link — are hidden, the title shrinks with an ellipsis, and the language switch and × stay with a bigger tap target | 0.1.66 |
295
- | [dsh-vision-toolkit](https://www.npmjs.com/package/@anionex/dsh-vision-toolkit) | Image Q&A / OCR, used with phone-side attachment upload | — |
296
- | [dsh-web-ui suite](https://www.npmjs.com/package/@linxin666/dsh-web-ui-all) | Inherits the compatibility rules from upstream dsh-web-mobile (file tree, width-capped centred preview overlay, and so on) | — |
316
+ 1. **Relay protocol version** must match — in practice, **both ends need zen-remote 2.0.x**. A 1.x server's gateway doesn't know the client's bearer token and answers its requests with the generic pairing wall, which the client reports as "token invalid (or the server's zen-remote is older than 2.0.0)". DSH versions may differ.
317
+ 2. **DSH interface fingerprints** decide the rest: both ends compute normalized fingerprints of the remote-interface groups the relay uses and compare them at handshake. Identical is fully compatible even when version strings differ; a difference connects anyway, annotates the remote group "(版本有差异)" (version differs), lists the differing groups in the settings diagnostics, and degrades only the affected panels.
318
+ 3. **Runtime degradation**: a forwarded call that fails parameter/result validation shows "incompatible with the server version" in that one panel and is recorded in the diagnostics — everything else keeps working.
297
319
 
298
- The technical detail behind each adaptation — anchor selectors, breakpoints, what was traded away — is in the "compatible plugins" section of the [interface doc](docs/interface.md).
320
+ ---
321
+
322
+ ## Security model
323
+
324
+ What a paired device is trusted with, stated plainly:
325
+
326
+ - **A paired desktop client is a trusted device.** The remote session's terminal is a shell running as the **server's user**, without the agent sandbox or approval restrictions. The file-preview interfaces (`workspaceFiles/read` and friends) are **not** contained to the session directory — anything the server process can read is readable. A paired client can create sessions in **any** of the server's workspaces (not limited to remote-enabled ones; the created session is auto-shared).
327
+ - **"Only remote-enabled sessions are visible" constrains session data**: an unshared session's list, history, live progress and approval/question events are refused or filtered by the relay; @-mention candidates list **only shared sessions**; and a `dsh-session:` reference embedded in a message, a queue edit or a slash command refuses the whole call when it names a session without remote access. It is not a sandbox around the machine.
328
+ - **Admin actions stay local**: generating pairing codes, changing roles and revoking devices only work from the settings page on the server machine itself — requests that arrived through the gateway can view status but mutations are refused — the one exception is toggling a session's remote access, which a web app device (a phone) may do, and that is what the phone info card's remote access switch uses; desktop clients cannot reach the admin routes at all.
329
+
330
+ ---
331
+
332
+ ## Known limitations
333
+
334
+ - With several DSH windows open at the same time (including the desktop app's backend page opened in a browser), "which session is currently open" can be judged wrongly on a client: a closed-remote session may hide at the wrong moment, and in the worst case a session you have open is taken for a background one and dismissed to the home page.
335
+ - Switching to a different server and back to the original one — or a server deleting a workspace and recreating one under the same id — leaves the affected remote groups invisible until the client page is **reloaded** (DSH's sidebar never re-accepts a removed workspace id within one page lifetime).
336
+ - "Export session" is disabled in a remote session (the menu item is hidden, and the backend refuses virtual session ids); the changes panel's **summary and diff do work** (relayed), while the entries that would pop a dialog on the server machine — "open", "reveal in Finder", "open in app", on the changes panel and deliverable cards alike — are hidden there.
337
+ - The host truncates @-mention candidates to the **first 50 rows before** share-filtering: on a server with many sessions, a shared one may be missing from the candidate list (the reference check itself is unaffected — references you type out are still verified one by one).
338
+ - Third-party plugins' own non-standard endpoints are not forwarded — their panels degrade or hide in remote sessions. A plugin's **tools** run on the server and are unaffected; what breaks is the part of its UI that calls the plugin's own backend with a session id (dsh-better-sidebar is the prime example, handled by the next bullet).
339
+ - With [dsh-better-sidebar](https://github.com/omdsh-dev/DSH-better-sidebar) installed, its file browser, editor, git, tasks, bottom workbench and similar surfaces are hidden in remote sessions — they read and write the **client machine's own** files; and because it takes over the host's "Files" tab, a remote session has no sidebar file browser for now; file links in chat or deliverables open an empty right column in a remote session.
340
+ - Both ends need zen-remote **2.0.x** (identical relay protocol version). DSH versions may differ; interface differences are annotated "(版本有差异)".
299
341
 
300
342
  ---
301
343
 
@@ -303,23 +345,40 @@ The technical detail behind each adaptation — anchor selectors, breakpoints, w
303
345
 
304
346
  **iOS 26.x standalone PWA viewport shrinkage**: after adding to the home screen, the viewport loses a status-bar's height at the bottom; an ordinary Safari tab is fine. This is an iOS defect — the missing region is outside the document and CSS cannot reach it. The plugin ships three layers of mitigation (light manifest background, safe-area compensation, forced reflow) which reduce it without guaranteeing a fix. Only quitting and reopening the whole app restores it fully.
305
347
 
306
- **In a few environments the soft keyboard is completely invisible to the browser, and the lift falls back to an estimate**: in some combinations (measured: certain third-party IMEs plus Chrome; the PWA shell installed by Xiaomi's browser) the system never tells the page the keyboard's height when it opens or closes — the viewport doesn't change and no event fires (visualViewport and the VirtualKeyboard API both fail; both ruled out by measurement). The fallback: probe for about 1.2s after focus, and if the keyboard is judged invisible, lift the input box by the estimated height (the verdict is remembered per browser, so later focuses lift immediately). Two costs: the lift is an estimate and can be tens of pixels off the real keyboard, and there's no signal for the keyboard closing either, so the box only drops back once you tap or scroll outside it. Normal environments never take this path. If the lift is visibly off you don't need to change code — `keyboardLiftRatio` / `keyboardLiftMaxPx` / `keyboardSafetyPadPx` on the plugin row are there to be tuned to your device; see the configuration section above.
348
+ **In a few environments the soft keyboard is completely invisible to the browser, and the lift falls back to an estimate**: in some combinations the system never tells the page the keyboard's height (visualViewport and the VirtualKeyboard API both fail; both ruled out by measurement). The fallback probes for ~1.2s after focus and lifts the input box by an estimated height when the keyboard is judged invisible. If the lift is visibly off, tune `keyboardLiftRatio` / `keyboardLiftMaxPx` / `keyboardSafetyPadPx` in the settings page. Normal environments never take this path.
307
349
 
308
- **Settings pages don't open through the reverse proxy (the plugin config list is blank, model cards report "settings are unavailable in this browser")**: connecting directly to `127.0.0.1:3080/3088` works fine.
350
+ **Settings pages don't open through the reverse proxy** (the plugin config list is blank, model cards report "settings are unavailable in this browser"): DSH's settings RPCs are loopback-only by design — a remote browser's settings mirror starts out `unavailable`. Workaround: change settings from a browser on the machine running DSH. The gateway-forwarded plugin settings page shows the same limitation. Nothing to do with this plugin.
309
351
 
310
- The root cause is DSH's own design, not the gateway: settings RPCs are **loopback-only**. The client decides from `location.hostname` (`isLoopback` in `dsh-client-connection`), and off-loopback `dsh-client-ui-settings` degrades persistence to `memory`, so the settings mirror starts out `unavailable` — the upstream source comment reads, verbatim, "remote browsers remain process-local because settings RPCs are loopback-only". Every card that depends on that mirror (models, plugin config) goes blank together. Nothing to do with this plugin or with service worker caching (verified 2026-08-20 by USB-debugging a real device against a local control).
352
+ ---
311
353
 
312
- Workaround: change settings from a browser on the machine running DSH. The config lives in the backend, and nothing else on the phone side is affected. Making settings remotely editable requires upstream to relax that restriction.
354
+ ## Permissions and data
355
+
356
+ - **Network**: the gateway listens on the local machine only (`127.0.0.1:3088` by default); what gets exposed is entirely up to your reverse proxy or tunnel. Push travels through the browser vendor's push service (aes128gcm end-to-end encrypted). The relay between a desktop client and the host runs over the same gateway; its authorization is re-checked per request server-side. The plugin itself reports nothing to any third party.
357
+ - **Files**: attachment uploads are written only to `.dsh-uploads/` inside the current session's working directory; pairing state lives in `~/.dsh/lan-gate-state.json`, config in `~/.dsh/lan-gate.config.json`, the share table in `~/.dsh/zen-remote-shares.json`, the server id in `~/.dsh/zen-remote-server.json`.
358
+ - **Credentials**: no account or password is ever collected or stored; a device's identity is a random token this plugin issues itself (HttpOnly cookie for web devices, a secret row field for desktop clients), plus a per-load relay shared secret that never leaves the machine pair.
359
+
360
+ Troubleshooting: runtime logs are in `~/.dsh/logs/web.log` (gateway and push lines are prefixed `[dsh-zen-remote-*]`; desktop builds print to the app console). Security issues: please report privately through GitHub Security Advisories rather than opening a public issue.
313
361
 
314
362
  ---
315
363
 
316
- ## Permissions and data
364
+ ## Third-party plugins with mobile support
365
+
366
+ The mobile UI has specific adaptations for the plugins below. Every adaptation is anchored on that plugin's own DOM markers: if you don't have it installed the rules simply don't match, and installing a plugin that isn't listed here can't be caught in the crossfire.
317
367
 
318
- - **Network**: the gateway listens on the local machine only (`127.0.0.1:3088` by default); what gets exposed is entirely up to your reverse proxy or tunnel. Push travels through the browser vendor's push service (the content is aes128gcm end-to-end encrypted, so the vendor can't read it). The plugin itself reports nothing to any third party.
319
- - **Files**: attachment uploads are written only to `.dsh-uploads/` inside the current session's working directory; pairing state and config live in `~/.dsh/lan-gate-state.json` and `lan-gate.config.json`.
320
- - **Credentials**: no account or password is ever collected or stored; a device's identity is a random token this plugin issues itself, in an HttpOnly cookie.
368
+ | Plugin | What the mobile adaptation does | Version tested |
369
+ | --- | --- | --- |
370
+ | [dsh-better-sidebar](https://github.com/omdsh-dev/DSH-better-sidebar) | 0.19+: all its tabs live in DSH 0.1.5's native right sidebar, which the host takes full screen on a phone (padded clear of the notch / home indicator) — the session header keeps an entry button for it, the left-edge swipe-back closes it, and tapping an @-reference in the file tree closes it. ≤ 0.18 (own right panel, DSH < 0.1.5 only): full-width phone drawer with notch safe area and a centred close pill | 0.19.1 (legacy rules: 0.15.0) |
371
+ | [@nanmicoder/dsh-agent-teams](https://github.com/NanmiCoder/dsh-agent-teams) | The AgentTeams activity overlay moves below the session header (its original position covered the header buttons) and hides itself on the session list; a subagent session keeps a tappable parent-session title in its header for jumping back | 0.1.9 |
372
+ | [@ychris12138/dsh-usage-stats](https://github.com/Ychris12138/dsh-usage-stats) | Usage and balance entries fold into the home-screen chips row | 0.2.9 |
373
+ | [@opendsh/dsh-plugin-scheduled-tasks](https://github.com/Ceelog/dsh-plugins) | The scheduled-tasks entry folds into the home-screen chips row | 0.2.3 |
374
+ | dsh-at-file | `@` file references, used alongside attachment upload's `@` paths. It and this plugin's attachment chips read the same draft token, so on the phone its `.dsh-uploads/` rows are hidden to stop one file being drawn twice (thumbnail plus filename); other `@` references are left alone | 0.6.7 |
375
+ | [@ace-zone/dsh-market](https://www.npmjs.com/package/@ace-zone/dsh-market) | The plugin market dialog's top bar doesn't fit on a phone and the × gets squeezed out of the panel (no Esc on a touchscreen, so it can't be closed at all). Three decorative slots — tagline, version, homepage link — are hidden, the title shrinks with an ellipsis, and the language switch and × stay with a bigger tap target | 0.1.66 |
376
+ | [dsh-vision-toolkit](https://www.npmjs.com/package/@anionex/dsh-vision-toolkit) | Image Q&A / OCR, used with phone-side attachment upload | — |
377
+ | [dsh-web-ui suite](https://www.npmjs.com/package/@linxin666/dsh-web-ui-all) | Inherits the compatibility rules from upstream dsh-web-mobile (file tree, width-capped centred preview overlay, and so on) | — |
321
378
 
322
- Troubleshooting: runtime logs are in `~/.dsh/logs/web.log` (gateway and push lines are prefixed `[dsh-zen-remote-*]`); the phone UI has a debug badge for self-checks (tap the home screen's top bar five times to toggle it). Please report security issues privately through GitHub Security Advisories rather than opening a public issue.
379
+ The technical detail behind each adaptation — anchor selectors, breakpoints, what was traded away — is in the "compatible plugins" section of the [interface doc](docs/interface.md).
380
+
381
+ ---
323
382
 
324
383
  ## Upstream credits
325
384