dsh-zen-remote 2.3.2 → 2.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -28,387 +28,283 @@
28
28
 
29
29
  ---
30
30
 
31
- ## One plugin, two roles
32
-
33
- Same package, same plugin row. What runs is decided by the `role` setting in the DSH plugin settings page:
34
-
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 |
40
-
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.
31
+ ## Contents
32
+
33
+ 1. [Install](#1-install)
34
+ 2. [Quick Start (Unified Setup & Pairing Wizard)](#2-quick-start-unified-setup--pairing-wizard)
35
+ - [Scenario A: Set up as Host (Phone & Desktop Access)](#scenario-a-set-up-as-host-phone--desktop-access)
36
+ - [Scenario B: Set up another computer as Desktop Client](#scenario-b-set-up-another-computer-as-desktop-client)
37
+ 3. [Daily Usage: Session Sharing & Cross-Device Workflow](#3-daily-usage-session-sharing--cross-device-workflow)
38
+ 4. [Lock-Screen Push: When Notifications Fire](#4-lock-screen-push-when-notifications-fire)
39
+ 5. [Features](#5-features)
40
+ 6. [Advanced Settings & Field Reference](#6-advanced-settings--field-reference)
41
+ 7. [Security Model, Version Tolerance & Known Notes](#7-security-model-version-tolerance--known-notes)
42
42
 
43
43
  ---
44
44
 
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)
45
+ ## 1. Install
59
46
 
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.
47
+ ### Install from the UI (Recommended, Desktop App & Web UI)
61
48
 
62
- ![The "remote closed" banner above the composer after remote access was closed](docs/images/client-remote-closed.png)
49
+ Open the **Plugins** page in the DSH sidebar → click **Add plugin** → type `dsh-zen-remote` → click **Enable now** when the install finishes, then restart the DSH desktop app (or `dsh web`).
63
50
 
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):
51
+ ### Install from the CLI (Headless `dsh web` server)
79
52
 
80
53
  ```sh
81
54
  dsh plugin --profile web add dsh-zen-remote
82
55
  # then restart dsh web
83
56
  ```
84
57
 
85
- > **Upgrading the plugin with HMR on:** remove dsh-zen-remote first, then install the new version (`dsh plugin --profile <name> remove dsh-zen-remote` → `add dsh-zen-remote`, or Uninstall → Add in the Plugins page); an in-place install over the running copy is not picked up cleanly.
58
+ > `dsh plugin` requires `--profile` to name the target profile. The desktop app's `desktop` profile is managed exclusively by the app (`--profile desktop` is refused on the CLI), so use the UI install above on the desktop app.
86
59
 
87
- > **DSH 0.2.1-alpha.3+:** `dsh --profile <name> --from-default-profile <template>` now *replaces* an existing custom profile with a fresh template copy, deleting its `dependencies` (including dsh-zen-remote) and user `cordis.patch.yml`. Back up `~/.dsh/profiles/<name>` first (e.g. `cp -a ~/.dsh/profiles/<name> ~/.dsh/profiles/<name>.bak-$(date +%Y%m%d%H%M%S)`), then re-add the plugin afterwards. Pairing/share state under `~/.dsh/*.json` is outside the profile and is not touched.
88
-
89
- `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.
60
+ <details>
61
+ <summary>Manual Profile Edit / Local Development (<code>link:</code>), Upgrading & Uninstall</summary>
90
62
 
91
- **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:
63
+ **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) and append `"dsh-zen-remote"` to the `dsh.profile.bundles` array:
92
64
 
93
65
  ```jsonc
94
66
  // ~/.dsh/profiles/desktop/package.json
95
67
  {
96
68
  "dependencies": {
97
- "dsh-zen-remote": "^2.3.2" // for local development: "link:/path/to/dsh-zen-remote"
69
+ "dsh-zen-remote": "^2.3.3" // for local development: "link:/path/to/dsh-zen-remote"
98
70
  }
99
71
  }
100
72
  ```
101
73
 
102
74
  ```sh
103
75
  cd ~/.dsh/profiles/desktop && pnpm install
104
- # then restart the DSH desktop app (or dsh web)
76
+ # For a link: checkout, also run `npm install --omit=dev` inside the repository so the gateway subprocess can resolve `web-push`.
105
77
  ```
106
78
 
107
- `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.
108
-
109
- 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.
110
-
111
- <details>
112
- <summary>Static mount / local development</summary>
113
-
114
- 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.
79
+ **Upgrading & migration notes**:
80
+ - **Upgrading with HMR on**: remove `dsh-zen-remote` first, then install the new version (`dsh plugin --profile <name> remove dsh-zen-remote` → `add dsh-zen-remote`, or Uninstall → Add in the Plugins page).
81
+ - **DSH 0.2.1-alpha.3+ profile reset**: `dsh --profile <name> --from-default-profile <template>` replaces an existing custom profile with a fresh copy; back up `~/.dsh/profiles/<name>` first.
82
+ - **Upgrading from 1.x**: since 2.0.0 the composition layer is a single row (`dsh-zen-remote`). Remove any legacy `dsh-zen-remote-gateway`, `dsh-zen-remote-push`, or manual `dsh-push.mjs` lines from `cordis.patch.yml`. Existing `~/.dsh/lan-gate.config.json` settings and paired devices carry over automatically.
83
+ - **Uninstalling**: click **Uninstall** on the `dsh-zen-remote` plugin page and 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`.
115
84
 
116
85
  </details>
117
86
 
118
- 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).
119
-
120
87
  ---
121
88
 
122
- ## Upgrading from 1.x
89
+ ## 2. Quick Start (Unified Setup & Pairing Wizard)
123
90
 
124
- One-time facts, in order of likelihood to bite:
91
+ The same plugin supports two roles:
92
+ - **Server (`role: host`, default)**: runs your sessions, spawns the local security gateway and Web Push service, and lets your phone (PWA) or another computer connect.
93
+ - **Desktop Client (`role: client`)**: runs inside another computer's DSH desktop app, connects to the host, and mounts the host's shared sessions directly into its own sidebar (without spawning a local gateway or fighting for port 3088).
125
94
 
126
- - **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.
127
- - **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.
128
- - **`~/.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.
129
- - **Paired devices become Web 应用端 automatically** and keep working; no re-pairing.
130
- - **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.
131
- - 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.
95
+ Open **Plugins → `dsh-zen-remote`** in DSH. The **Remote Access & Device Pairing** card sits right at the top of the page:
132
96
 
133
- ---
134
-
135
- ## Setting up public access (host)
136
-
137
- 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). **Prefer a guided setup?** The plugin detail page on the host has a three-step wizard (“Connect your phone”) that automates every path below — a managed Caddy with DNS-01 certificates, snippet generation + verification for your existing proxy, and a fully automatic Cloudflare Tunnel. The short form:
138
-
139
- 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.
140
- 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).
141
- 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.
97
+ ![Remote Access & Device Pairing overview card on the plugin detail page](docs/images/wizard-overview.png)
142
98
 
143
99
  ---
144
100
 
145
- ## Pairing devices
146
-
147
- 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.
101
+ ### Scenario A: Set up as Host (Phone & Desktop Access)
148
102
 
149
- | | **Web 应用端 (web app device)** | **桌面应用端 (desktop app device)** |
150
- | --- | --- | --- |
151
- | Shape | Phones, tablets, browsers — anything through the gateway | A computer running the DSH desktop app with this plugin in its `client` role |
152
- | Sees | **All** server sessions | **Only** the sessions the host has enabled remote access on (enforced server-side) |
153
- | 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 |
154
-
155
- **Desktop-client pairing**, step by step:
156
-
157
- 1. On the host's plugin settings page, choose 桌面应用端 and generate a code.
158
- 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.
159
- 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.
160
- 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.
161
-
162
- 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.
163
-
164
- **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.
103
+ Click **Start Setup Wizard** (or **Reconfigure Wizard** if already configured) to complete setup in three guided steps:
165
104
 
166
- ---
105
+ #### Step 1: Choose "Act as Server (Host)" & an Access Method
167
106
 
168
- ## Session sharing
107
+ ![Wizard Step 1: Choose machine role and server access method](docs/images/wizard-step1-role-path.png)
169
108
 
170
- Remote access is per session, with four toggle entries:
109
+ Pick one of the four access methods based on your network environment (switching methods automatically stops any managed process from the previous mode and updates the gateway bind address):
171
110
 
172
- 1. 开启远程 / 关闭远程 (remote on / off) in the sidebar session row's "…" menu;
173
- 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);
174
- 3. the shared-sessions list in the plugin settings page, including a close-all;
175
- 4. the 远程访问 (remote access) switch in the phone's (web app device's) session info card.
111
+ | Access method | Best for | What the wizard automates |
112
+ | --- | --- | --- |
113
+ | **Managed Caddy (Recommended)** | Public IP (or router port forwarding) + domain on Cloudflare | Automatically downloads a prebuilt `Caddy + caddy-dns/cloudflare` binary (auto-accelerated via mainland China mirrors when needed) and issues/renews TLS certificates via DNS-01; gateway listens only on `127.0.0.1:3088` |
114
+ | **Existing Reverse Proxy** | Already running Nginx, Caddy, or Lucky | Generates ready-to-paste proxy snippets with WebSocket and `X-Forwarded-*` headers, plus one-click `trustedProxies` registration (full manual guide: [docs/remote-access.en.md](docs/remote-access.en.md)) |
115
+ | **Cloudflare Tunnel** | No public IP / no router port forwarding + domain on Cloudflare | Automatically downloads `cloudflared` (with mainland China mirror failover), creates or reuses a tunnel via the Cloudflare API, writes the DNS CNAME record, and starts the tunnel; gateway listens only on `127.0.0.1:3088` |
116
+ | **LAN / Tailscale Direct** | Same Wi-Fi / LAN or shared Tailscale virtual network | No domain or TLS certificate needed; auto-detects the machine's LAN / Tailscale IPv4 address and binds `0.0.0.0:3088`, while the gateway's built-in **private-IP wall** rejects any unproxied direct connection from a public internet IP (`403 public-direct-forbidden`) |
176
117
 
177
- **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.
118
+ #### Step 2: Fill in Connection Parameters
119
+ - **Managed Caddy / Cloudflare Tunnel**: enter your public domain (e.g. `dsh.example.com`) and paste your Cloudflare API Token (click *"How do I get an API token?"* for step-by-step instructions and required permissions, or *"Domain not on Cloudflare yet?"* for onboarding steps).
120
+ - **Existing Reverse Proxy**: enter the external `https://` URL of your proxy and copy the generated Nginx / Caddy / Lucky configuration snippet.
121
+ - **LAN / Tailscale Direct**: the wizard automatically detects and pre-fills `http://<your-LAN-or-Tailscale-IP>:3088`.
178
122
 
179
- **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.
123
+ #### Step 3: Verify Connectivity & Pair Devices
180
124
 
181
- **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.
125
+ ![Wizard Step 3: Automatic connectivity check, phone QR pairing, and desktop client pairing](docs/images/wizard-step3-verify-pair.png)
182
126
 
183
- **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.
127
+ 1. **Start Ingress Service & Auto-Probe**:
128
+ - For Managed Caddy or Cloudflare Tunnel, click **Start Caddy** or **Create & Start Tunnel** in the service bar.
129
+ - The wizard automatically probes **Local gateway ready** and **Access URL reachable** (verifying that `/` serves the `401` pairing page and `/lan-gate/admin` returns `403`).
130
+ 2. **Pair Phone / Browser (Web App)**:
131
+ - On the **Pair Phone / Browser (QR)** tab, click **Generate Pairing QR**.
132
+ - Scan the QR code with your phone camera or browser, enter the 8-character pairing code, and use **"Add to Home Screen"** to install DSH as a standalone PWA with lock-screen push notifications.
133
+ 3. **Pair Another Computer (Desktop Client)**:
134
+ - Switch to the **Pair Desktop Client** tab, click **Generate Desktop Pairing Code**, and click **Copy Server URL & Code** to send to your other computer.
184
135
 
185
- **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.
136
+ > **Quick actions after setup**: once configured, you can directly click **Pair Phone (QR)**, **Pair Desktop Client**, or **Manage Devices** (rename devices, switch device roles, revoke one or all devices, or send a test push) right from the overview card footer without re-running the wizard.
186
137
 
187
138
  ---
188
139
 
189
- ## Configuration
190
-
191
- ### Settings page walkthrough
140
+ ### Scenario B: Set up another computer as Desktop Client
192
141
 
193
- **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.
142
+ On the second computer running the DSH desktop app:
194
143
 
195
- **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.
144
+ 1. Open **Plugins → `dsh-zen-remote`** and click **Start Setup Wizard**.
145
+ 2. **Step 1**: select **Act as Desktop Client** and click **Next**.
146
+ 3. **Step 2**: enter a name for this computer (e.g. `Living-Room-Mac`), the host's gateway URL (`https://` for public URLs, or `http://192.168.x.x:3088` / `http://100.x.x.x:3088` for LAN / Tailscale), and the 8-character desktop pairing code from the host, then click **Pair & Connect**.
147
+ 4. **Step 3**: once all three status lights (**Pairing token**, **Server connection**, **Session & plugin sync**) turn green, the host's shared workspaces and sessions appear in your left sidebar immediately.
196
148
 
197
- **Host**
198
-
199
- 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.
149
+ ---
200
150
 
201
- ![Host settings: role and device name in the role card](docs/images/host-settings-role.png)
151
+ ## 3. Daily Usage: Session Sharing & Cross-Device Workflow
202
152
 
203
- 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).
204
- 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.
153
+ | | **Web App Device (Phone / Tablet / Browser)** | **Desktop Client (Another DSH Desktop App)** |
154
+ | --- | --- | --- |
155
+ | Visible sessions | **All** sessions on the host | **Only** sessions with remote access enabled on the host (enforced server-side) |
156
+ | Credential | Long-lived HttpOnly cookie | Secret device token stored in the plugin config (`Bearer` token) |
205
157
 
206
- ![Generating a pairing code, and the device list](docs/images/host-pairing.png)
158
+ ### Enabling or Disabling Remote Access on a Session
207
159
 
208
- 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.
160
+ Remote access is controlled per session (or automatically enabled for new sessions if you turn on *Auto-share new sessions* in Step 2 of the wizard). You can toggle it from four places:
209
161
 
210
- ![The remote-enabled sessions list](docs/images/host-settings-shares.png)
162
+ 1. **Host sidebar**: open a session's `"…"` menu and click **开启远程 / 关闭远程** (Enable / Disable remote);
211
163
 
212
- 5. 推送 (push): push summaries, turn-end pushes and so on, as you like — see [When notifications fire](#when-notifications-fire).
164
+ ![Enabling remote access from a session's menu on the host](docs/images/host-share-menu.png)
213
165
 
214
- **Client**
166
+ 2. **Host session header**: click the globe icon in the session title row (hover to see remaining idle time; a dot indicates a desktop client is currently viewing it);
167
+ 3. **Phone (Web App) session info card**: open the info card at the top right of a session and toggle **远程访问** (Remote access);
215
168
 
216
- 1. Set 运行角色 (role) to 子客户端 (client) and save — the plugin reloads and the page switches to the client's settings.
217
- 2. Give this computer a 设备名称 (device name), e.g. "客厅" (living room); it is registered in the host's device list when pairing. Save.
218
- 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).
219
- 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.
169
+ <p align="center"><img src="docs/images/mobile-info-remote.png" alt="Remote access toggle inside the phone session info card" width="320"></p>
220
170
 
221
- ![Client settings: role, device name, server connection and connection state](docs/images/client-settings.png)
171
+ 4. **Host settings page**: view and close shared sessions individually or all at once under the shared sessions list.
222
172
 
223
- > Addresses and pairing codes in the screenshots are demo values.
173
+ ### Working in Remote Sessions on a Desktop Client
224
174
 
225
- ### Field reference
175
+ - **Sidebar groups**: once a host workspace has at least one remote-enabled session, a **"Server Name · Workspace Name"** group appears in the client's sidebar. Click any session to view full history, stream responses, approve tools, use the server terminal, browse files and diffs, download deliverables, or drag-and-drop files to upload.
226
176
 
227
- 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.
177
+ ![Remote workspace group in the desktop client sidebar](docs/images/client-sidebar.png)
228
178
 
229
- Row fields (matching `src/config.ts`):
179
+ - **Closing & idle sleep**: a shared session automatically closes remote access after **48 hours** (`idleHours`) of inactivity (mid-turn sessions or sessions waiting on approvals/questions never sleep). When remote access is closed—or when the client unpairs or its token is revoked—remote sessions are automatically archived/hidden from the sidebar; an actively open session displays a "remote closed" banner and recovers automatically if re-shared.
230
180
 
231
- | Field | Default | What it does |
232
- | --- | --- | --- |
233
- | `role` | `host` | `host` runs gateway + push + sharing; `client` connects to a server. Anything not exactly `client` degrades to `host` |
234
- | `port` | `3088` | Gateway port; if taken it retries upward (up to +20) |
235
- | `host` | `127.0.0.1` | Gateway listen address; only open this up when the proxy is on another machine |
236
- | `targetPort` | *(auto)* | Local DSH Web UI port. Leave unset: the gateway discovers the host's real listening port (desktop builds use a configurable one) |
237
- | `rateLimit` | `120` | Per-minute cap on unpaired requests, per real client IP |
238
- | `trustedProxies` | empty | Comma-separated IPs; required when the proxy is on another machine |
239
- | `vapidSubject` | `mailto:admin@localhost` | Push contact. **On iOS this must be a real email or https URL**, or Apple refuses to deliver |
240
- | `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 |
241
- | `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 |
242
- | `pushEvents` | `agent/turn-stopping` | Which events count as "turn ended", comma-separated; only meaningful with `pushTurnEnd` |
243
- | `pushDebounceMs` | `15000` | Minimum gap between two automatic pushes; approval/question notifications are never suppressed by it |
244
- | `pushSummary` | off | Include the turn's final reply (prose only, never the reasoning; clipped to 120 chars) and the question text in the body |
245
- | `pushTool` | on | Set `false` to remove the model-callable `push_notify` tool |
246
- | `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 |
247
- | `idleHours` | `48` | Idle-sleep window in hours for remote-enabled sessions; range (0, 8760] |
248
- | `autoShareNewSessions` | off | Auto-enable remote on every session the host creates |
249
- | `pluginBridgeConsent` | `{}` | The plugin bridge's whole-machine-data consent map: plugin name → boolean. Managed by the settings page's Plugin bridge section, rarely hand-edited; leftover keys for unregistered plugins are never pruned |
250
-
251
- | `serverUrl` *(client)* | empty | The server's gateway address, normalized per the rules above |
252
- | `deviceToken` *(client)* | empty | The paired device token; stored as a secret field, redacted everywhere |
253
- | `turnFoldDesktop` | off | Turn-process folding at every width, not just phone widths |
254
- | `keyboardLiftRatio` / `keyboardLiftMaxPx` / `keyboardSafetyPadPx` | `0.42` / `400` / `15` | Soft-keyboard lift calibration for browsers that never report the keyboard (see the known-issues note below) |
255
- | `maxUploadBytes` | 20 MB | Attachment upload cap |
256
-
257
- 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.
258
-
259
- 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).
181
+ ![Banner shown when remote access is closed on an open session](docs/images/client-remote-closed.png)
260
182
 
261
183
  ---
262
184
 
263
- ## When notifications fire
264
-
265
- By default only when you are **actually needed**, along two independent lines.
266
-
267
- **1. Decided by the system (always on, never suppressed by the debounce)**
268
-
269
- | Situation | Notification |
270
- | --- | --- |
271
- | A tool is waiting for your authorization | "DSH needs your approval", with the tool name |
272
- | The model called `ask_user_question` and is waiting | "DSH is waiting for your answer" |
273
-
274
- 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.
275
-
276
- **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.
277
-
278
- **2. Decided by the model**
185
+ ## 4. Lock-Screen Push: When Notifications Fire
279
186
 
280
- 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.
187
+ Notifications fire only when **you are actually needed**, along two independent paths (all payloads are end-to-end encrypted with `aes128gcm`):
281
188
 
282
- **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.
283
-
284
- **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.
189
+ 1. **System-triggered (always on, never suppressed by debounce)**:
190
+ - **Tool waiting for approval**: pushes *"DSH needs your approval"* with the tool name. Waits `DSH_PUSH_APPROVAL_GRACE_MS` (default 5s) before sending so fast auto-approver plugins can answer first without buzzing your phone.
191
+ - **`ask_user_question` waiting for your answer**: pushes *"DSH is waiting for your answer"*.
192
+ - Works for both top-level sessions and subagent sessions across Web and Desktop hosts.
193
+ 2. **Model-triggered (`push_notify` tool)**:
194
+ - Called by the agent when you asked to be notified upon completion, or when it hits a blocker that requires your decision.
195
+ - Ordinary turn completions do not push by default (enable `pushTurnEnd` in advanced settings if you want a push after every turn; enable `pushSummary` to include up to 120 characters of the final reply in the notification body).
285
196
 
286
197
  ---
287
198
 
288
- ## Features
289
-
290
- **Host (phone access, unchanged in spirit from 1.x)**
291
-
292
- - Two-level page stack — session list home plus a standalone session page, pushed in and out horizontally
293
- - Plugin entry chips on the home screen, appearing automatically for what you have installed, individually hideable
294
- - Reworked composer: controls become icons, the permission and model menus become bottom sheets
295
- - 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
296
- - Turn-process folding, left-edge swipe back, Android back-gesture takeover, phone-local attachment upload
297
- - A pairing code buys a long-lived device token; identity follows the token, not the IP, revocable at any time
298
- - Real PWA + real Web Push (VAPID + aes128gcm), firing only for approvals and questions by default
299
- - Cold start paints an app-shaped skeleton from the first HTML byte; content-hashed build assets are cached for a year, so a repeat open reuses the shell instead of re-downloading it
300
-
301
- **Host (2.0 additions)**
302
-
303
- - Session sharing with four toggle entries (including the phone info card), idle sleep, auto-share and subagent/fork following
304
- - 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
305
-
306
- **Desktop client (2.0 additions)**
307
-
308
- - 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
309
- - Artifacts a remote session produces can be **downloaded to this computer**: the deliverable card and the document preview header each carry a "download to this computer" action that reads the host file through the relay in byte windows (any file size, never held whole in memory) and hands it to the browser; Word/Excel/PPT previews are now rendered to PDF by the host's LibreOffice (`officeToPdf/*` is on the relay allowlist) instead of failing with "not supported in remote sessions"
310
- - 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 500 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 client's 15-minute upload budget (the host HTTP server's 5-minute request deadline is the practical ceiling: 500 MiB needs ~14 Mbps sustained).
311
- - 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
312
- - 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
313
- - 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
314
- - First-come-first-served approvals/questions: if the server's own UI answers first, the client syncs to "already handled"
315
-
316
- **Both ends (2.1 additions)**
317
-
318
- - The **plugin bridge**: another plugin registers one bridge declaration and its panel data in remote sessions is served by the HOST — the sub-client reroutes the declared GET requests to the server, which executes them over loopback and streams the answer back; local data never leaks into a remote session. Bridged requires the plugin name, exact version and declaration to agree on BOTH ends; whole-machine data additionally needs the server owner's per-plugin consent in the settings page. Author guide: [the plugin bridge](docs/plugin-bridge.md)
319
-
320
- **Desktop client (2.2 additions)**
321
-
322
- - Dragging files into a remote session attaches them by **upload**: the desktop app's same-machine shortcut (a dragged file keeps its disk path and would become an `@absolute path` mention naming a file on YOUR machine, unreadable on the server) is intercepted in the browser and the drop is re-shaped into an ordinary upload — the same relay path as the attach button, so the server session gets the bytes. Dragging a **folder** into a remote session is not supported: the drop is stopped with a short on-screen notice. Local sessions are untouched — the shortcut keeps working there — and the guard's status is visible in the settings page diagnostics.
323
-
324
- In depth: [interface](docs/interface.md) · [public access + relay protocol](docs/remote-access.en.md)
199
+ ## 5. Features
200
+
201
+ - **Mobile PWA UI (Host)**:
202
+ - Two-level page stack (session list home + standalone session page with left-edge swipe back, Android back-gesture support, first-frame boot skeleton, and immutable asset caching)
203
+ - Home-screen plugin entry chips (auto-discovered per installed plugin, individually toggleable)
204
+ - Compact mobile composer with bottom sheets for permissions/models and direct phone attachment uploads
205
+ - Session info card (6-tile stats, log export, rename, fork, archive, one-tap full-conversation PNG share image, and remote access switch)
206
+ - **Unified Setup & Pairing Wizard (2.3)**:
207
+ - Top-of-page 2×2 live overview card and 3-step modal wizard covering all four host ingress modes (Managed Caddy, Existing Reverse Proxy, Cloudflare Tunnel, LAN / Tailscale Direct) and Desktop Client pairing
208
+ - Automatic download of prebuilt `Caddy + caddy-dns/cloudflare` and `cloudflared` binaries with mainland China network auto-detection, mirror acceleration (`gh-proxy.com` / `ghfast.top`), connect/stall failover watchdogs, and daily upstream release tracking via GitHub Actions
209
+ - Automatic LAN / Tailscale IPv4 discovery and local network probing, backed by the gateway's private-IP wall (`403 public-direct-forbidden`) against accidental public exposure
210
+ - Dedicated **Manage Devices** modal and automatic sidebar cleanup on client unpair/revocation
211
+ - **Cross-Desktop Remote Sessions (Client)**:
212
+ - Full-featured remote sessions in the sidebar: history, streaming, approvals/questions, server model catalog, file tree, changes summary/diff, server-side terminal, windowed artifact downloads, server-side LibreOffice PDF preview, up to 500 MiB file uploads, and drag-and-drop file upload interception
213
+ - **Plugin Bridge**:
214
+ - Cooperative plugins and built-in adapters (such as `dsh-better-sidebar`) route their panel requests in remote sessions to the host machine (with explicit owner consent for whole-machine data). See [Plugin Bridge Guide](docs/plugin-bridge.md).
215
+
216
+ In depth: [Interface Design](docs/interface.md) · [Public Access & Relay Protocol](docs/remote-access.en.md)
325
217
 
326
218
  ---
327
219
 
328
- ## Version tolerance
329
-
330
- Three layers, strictest first:
331
-
332
- 1. **Relay protocol version** must match — in practice, **both ends need the same zen-remote version**. 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.
333
- 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.
334
- 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.
335
-
336
- ---
220
+ ## 6. Advanced Settings & Field Reference
337
221
 
338
- ## Security model
222
+ Day-to-day setup and device management are handled entirely by the **Remote Access & Device Pairing Wizard** at the top of the `dsh-zen-remote` page. To fine-tune individual low-level fields, click `dsh-zen-remote` a second time under **Included components** on the plugin detail page.
339
223
 
340
- What a paired device is trusted with, stated plainly:
224
+ <details>
225
+ <summary>View underlying component settings screenshots & full field reference table</summary>
341
226
 
342
- - **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).
343
- - **"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.
344
- - **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.
227
+ Each field resolves in order: **environment variable > plugin row settings > `~/.dsh/lan-gate.config.json` > default**. Saving a field that requires a gateway/push restart reloads the plugin row automatically without restarting the app.
345
228
 
346
- ---
229
+ | Host Role & Pairing Cards | Shared Sessions List | Client Settings & Diagnostics |
230
+ | --- | --- | --- |
231
+ | ![Host role](docs/images/host-settings-role.png) ![Host pairing](docs/images/host-pairing.png) | ![Shared sessions](docs/images/host-settings-shares.png) | ![Client settings](docs/images/client-settings.png) |
347
232
 
348
- ## Known limitations
233
+ | Field | Default | What it does |
234
+ | --- | --- | --- |
235
+ | `role` | `host` | `host` runs gateway + push + sharing; `client` connects to a remote host |
236
+ | `serverName` | computer name | **Device name** (≤ 40 chars): sidebar group title on a host, registered device name on a client; syncs both ways automatically |
237
+ | `port` | `3088` | Gateway port; retries upward (up to +20) if occupied |
238
+ | `host` | `127.0.0.1` | Gateway bind address (`127.0.0.1` for local proxy/tunnel, `0.0.0.0` for LAN/Tailscale direct) |
239
+ | `targetPort` | *(auto)* | Local DSH Web UI port; leave empty to auto-discover the host's listening port |
240
+ | `rateLimit` | `120` | Per-minute cap on unpaired requests per client IP |
241
+ | `trustedProxies` | empty | Comma-separated trusted proxy IPs when the reverse proxy runs on another machine |
242
+ | `vapidSubject` | `mailto:admin@localhost` | Web Push contact (**must be a real `mailto:` or `https://` URL for iOS delivery**) |
243
+ | `lang` | `auto` | Gateway & notification language: `auto` (browser `Accept-Language`), `zh`, or `en` |
244
+ | `pushTurnEnd` | off | Also send a push when an ordinary turn finishes (approvals/questions always push) |
245
+ | `pushEvents` | `agent/turn-stopping` | Comma-separated events that count as turn end when `pushTurnEnd` is on |
246
+ | `pushDebounceMs` | `15000` | Minimum interval (ms) between automatic pushes |
247
+ | `pushSummary` | off | Include up to 120 chars of the final reply in notification bodies |
248
+ | `pushTool` | on | Register the model-callable `push_notify` tool |
249
+ | `idleHours` | `48` | Idle-sleep window in hours for shared sessions; range `(0, 8760]` |
250
+ | `autoShareNewSessions` | off | Automatically enable remote access on new sessions created on the host |
251
+ | `pluginBridgeConsent` | `{}` | Whole-machine data consent map (`pluginName -> boolean`) for the plugin bridge |
252
+ | `serverUrl` *(client)* | empty | Remote server gateway URL (`https://` required for public hosts; `http://` allowed on private LAN/Tailscale ranges) |
253
+ | `deviceToken` *(client)* | empty | Paired client token (secret field, redacted everywhere) |
254
+ | `turnFoldDesktop` | off | Enable turn-process folding on desktop widths as well as mobile |
255
+ | `keyboardLiftRatio` / `keyboardLiftMaxPx` / `keyboardSafetyPadPx` | `0.42` / `400` / `15` | Soft-keyboard lift calibration fallback for environments where the OS hides keyboard height |
256
+ | `maxUploadBytes` | 20 MB | Mobile attachment upload cap |
257
+
258
+ Legacy environment variables (outrank row settings): `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`, `DSH_PUSH_APPROVAL_GRACE_MS`.
349
259
 
350
- - 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.
351
- - 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).
352
- - "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.
353
- - 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).
354
- - A third-party plugin's own endpoints work in remote sessions only through the [plugin bridge](docs/plugin-bridge.md): cooperative plugins register themselves, third-party plugins are covered by built-in adapters. A plugin with neither has its panels degraded or hidden in remote sessions (its **tools** still run on the server, unaffected).
355
- - With [dsh-better-sidebar](https://github.com/omdsh-dev/DSH-better-sidebar) installed, remote sessions have TWO treatments: **0.25.0, 0.26.0, 0.26.1, 0.28.0-alpha.0 and 0.28.0** are bridged by zen-remote's built-in adapter — its file / git / task panels read and write the SERVER's data in a remote session (consent required, see the security boundary doc). Any OTHER version keeps the hide-local-surfaces treatment: its surfaces are hidden in remote sessions because they would read and write the **client machine's own** files; and because 0.19+ takes over the host's "Files" tab, an unbridged remote session has no sidebar file browser for now; file links in chat or deliverables open an empty right column in a remote session.
356
- - Both ends need the **same zen-remote version** (identical relay protocol version). DSH versions may differ; interface differences are annotated "(版本有差异)".
260
+ </details>
357
261
 
358
262
  ---
359
263
 
360
- ## Known issues
361
-
362
- **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.
264
+ ## 7. Security Model, Version Tolerance & Known Notes
363
265
 
364
- **iOS 26+ standalone PWA system top progressive blur**: when opened from the home screen on iOS 26+, WebKit draws a system-level scroll-edge progressive blur (`WKScrollPocket` / `UIScrollEdgeEffect`) across the top ~95px of the window (status bar plus the upper half of the top bar). Because it is an OS compositor layer outside the DOM, page CSS cannot remove it directly; entering a session and returning to the session list clears it on the list page. Android and regular Safari tabs are unaffected.
266
+ ### Security Model
365
267
 
366
- **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.
268
+ - **A paired desktop client is a trusted device**: the remote session terminal runs as the **host's OS user**, file-preview endpoints can read files accessible to the host process, and the client can create sessions in any host workspace.
269
+ - **Unshared sessions are strictly isolated at the relay**: list rows, history, live events, approvals/questions, and embedded `dsh-session:` references for unshared sessions are filtered or refused server-side.
270
+ - **Public direct-connection guard on `0.0.0.0`**: even when the gateway binds `0.0.0.0` for LAN / Tailscale mode, `isPrivateOrLoopbackIp` only admits direct connections from loopback, RFC1918 LAN ranges, link-local, and Tailscale (`100.64.0.0/10` / `fd7a:115c:a1e0::/48`); any unproxied direct connection from a public internet IP is rejected with `403 public-direct-forbidden`.
271
+ - **Admin actions are local-only**: minting pairing codes, changing device roles, running the setup wizard, and revoking devices are only permitted directly on the host machine.
272
+ - **Data & privacy**: the plugin reports nothing to any third party. Pairing state lives in `~/.dsh/lan-gate-state.json`, shared sessions in `~/.dsh/zen-remote-shares.json`, and Cloudflare tokens in `0600` files under `~/.dsh/zen-remote-ingress/`.
367
273
 
368
- **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.
369
-
370
- ---
274
+ ### Version Tolerance
371
275
 
372
- ## Permissions and data
276
+ 1. **Relay protocol version**: both host and client must run the same `dsh-zen-remote` version.
277
+ 2. **DSH host version**: DSH versions may differ between host and client. Normalized interface fingerprints are compared during handshake; any differing group is annotated `"(版本有差异)"` and degrades only the affected panel.
373
278
 
374
- - **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.
375
- - **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`.
376
- - **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.
279
+ ### Known Limitations & Third-Party Plugin Support
377
280
 
378
- 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.
281
+ <details>
282
+ <summary>Expand known limitations, iOS PWA notes & adapted third-party plugins</summary>
379
283
 
380
- ---
284
+ **Known limitations**
285
+ - With multiple DSH windows open simultaneously on a client, active-session detection may occasionally dismiss a closed session early.
286
+ - Switching between different servers or recreating a workspace with the same ID requires reloading the client page once.
287
+ - "Export session" and server-local OS dialogs ("Reveal in Finder" / "Open in app") are hidden in remote sessions.
288
+ - DSH's own Settings RPCs are loopback-only by upstream design, so plugin settings must be edited on the machine running DSH rather than through a remote browser.
381
289
 
382
- ## Third-party plugins with mobile support
290
+ **iOS standalone PWA notes**
291
+ - **iOS 26.x viewport shrinkage**: home-screen PWAs may lose a status-bar height at the bottom (an iOS WebKit bug); three mitigation layers are built in, and restarting the PWA restores full height.
292
+ - **iOS 26+ top progressive blur**: opening from the home screen may show a system `WKScrollPocket` blur across the top ~95px; entering any session and navigating back to the list clears it.
383
293
 
384
- The mobile UI has specific adaptations for the plugins below. Every adaptation is anchored on that plugin's own DOM markers; the plugin bridge is a separate lane — the plugin registers a declaration itself (see [the plugin bridge](docs/plugin-bridge.md)) and none of this table applies: 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.
294
+ **Third-party plugins with mobile & bridge adaptations**
385
295
 
386
- | Plugin | What the mobile adaptation does | Version tested |
296
+ | Plugin | Adaptation summary | Version tested |
387
297
  | --- | --- | --- |
388
- | [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. **Plugin bridge**: **0.25.0, 0.26.0, 0.26.1, 0.28.0-alpha.0 and 0.28.0** are bridged by zen-remote’s built-in adapter — in remote sessions its file / git / task panels read and write the SERVER’s data (whole-machine data, per-plugin consent on the server’s settings page; that equals whole-disk access and git writes on the server, see the [security boundary](docs/remote-access.md)); any other version stays unbridged and keeps the hide-local-surfaces treatment | 0.19.1 (legacy rules: 0.15.0; adapter: 0.25.0, 0.26.0, 0.26.1, 0.28.0-alpha.0, 0.28.0) |
389
- | [@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 |
390
- | [@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 |
391
- | [@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 |
392
- | 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 |
393
- | [@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 |
394
- | [dsh-vision-toolkit](https://www.npmjs.com/package/@anionex/dsh-vision-toolkit) | Image Q&A / OCR, used with phone-side attachment upload | — |
395
- | [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) | — |
396
-
397
- **Mixed versions** — the bridging decision is made per side: whether the plugin bridges at all follows the **server's** installed better-sidebar version (present in the adapter's version table + the owner's consent); what requests the UI sends follows the **client's own** version (the UI loads locally); and if the two ends run different zen-remote versions, the adapter digest comparison fails and the plugin falls back to the hidden treatment:
398
-
399
- (Old zen-remote + server BS 0.25 + client BS 0.25 is the **currently released** combination — bridged as usual.)
400
-
401
- | Two-end zen-remote | Server BS | Client BS | Result |
402
- | --- | --- | --- | --- |
403
- | New (has the 0.26 adapter) | 0.26 | 0.26 | Fully bridged; every new feature works |
404
- | New | 0.25 | 0.25 | Bridged as before; behavior identical to the previous release |
405
- | New | 0.25 | 0.26 | Bridged; the 0.26-only methods and `/sidebar/write` get 404 from the 0.25 server, `ws/sidechat` is refused so side chat falls back to 15s polling — single-feature degradation: core side chat works, no mis-routing, nothing written locally |
406
- | New | 0.26 | 0.25 | Bridged; the UI never sends the new requests — equivalent to the 0.25 experience |
407
- | Old (≤ 2.2.1) | 0.26 | any | Not in the adapter's version table → unbridged (hidden treatment) |
408
- | Old | 0.25 | 0.26 | Bridged as 0.25; new methods are refused with the adapter's `adapter-method-unknown`, and `/sidebar/write` carrying a virtual id errors against the local plugin (no file written) — single-feature degradation |
409
- | One new, one old | any | any | Core remote sessions work; the adapter digests differ → the bridge comparison fails → hidden treatment, until both ends run the same version |
410
-
411
- 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).
298
+ | [dsh-better-sidebar](https://github.com/omdsh-dev/DSH-better-sidebar) | Full-screen mobile right-sidebar drawer with notch safe-area padding and swipe-back; **Plugin Bridge** built-in adapter for `0.25.0` through `0.28.2` (seven versions) (reads/writes host file/Git/task panels in remote sessions with host owner consent) | 0.19.1 / adapter 0.25–0.28.2 |
299
+ | [@nanmicoder/dsh-agent-teams](https://github.com/NanmiCoder/dsh-agent-teams) | Repositions AgentTeams floating pill below session header and preserves parent session back-link | 0.1.9 |
300
+ | [@ychris12138/dsh-usage-stats](https://github.com/Ychris12138/dsh-usage-stats) | Moves usage & balance entry into the mobile home chips bar | 0.2.9 |
301
+ | [@opendsh/dsh-plugin-scheduled-tasks](https://github.com/Ceelog/dsh-plugins) | Moves scheduled tasks entry into the mobile home chips bar | 0.2.3 |
302
+ | dsh-at-file | Deduplicates `.dsh-uploads/` attachment rows on mobile | 0.6.7 |
303
+ | [@ace-zone/dsh-market](https://www.npmjs.com/package/@ace-zone/dsh-market) | Compacts marketplace modal header on mobile so the close button stays reachable | 0.1.66 |
304
+ | [dsh-vision-toolkit](https://www.npmjs.com/package/@anionex/dsh-vision-toolkit) | Works with mobile attachment uploads for image Q&A/OCR | — |
305
+ | [dsh-web-ui bundle](https://www.npmjs.com/package/@linxin666/dsh-web-ui-all) | Mobile width-constrained centered overlays for file tree / preview | — |
306
+
307
+ </details>
412
308
 
413
309
  ---
414
310