Phone-friendly remote control page: list Sessions, watch a running turn, send prompts, answer questions.
The plugin will be installed here. Keep web if you are unsure.
npx -y @deepseek-ai/dsh plugin --profile web add github:Dynesshely/dsh-plugin-remote#db5106fe573a810f1d8c89683fd9a35437042909
Compatibility and provenance
Remote Control is published as @local/dsh-remote-control and currently resolves to version 1.0.0. The Hub verifies its manifest and preserves the exact installation source for reproducible installs.
DeepSeek Remote is a plugin for DeepSeek Harness that serves the desktop app's conversation as a
web page, so you can read answers, send prompts and manage sessions from a phone, a tablet or another
machine on the same network. It talks to the Host it runs inside — no separate daemon, no cloud relay —
and every request is gated by a token you own.
Features
The conversation, live. Transcript, streaming answers, reasoning chains, tool calls and grouped tool
runs arrive over Server-Sent Events.
Prompts from anywhere. Markdown composer with live preview (CodeMirror 6) and image attachments.
Session management. Create, rename, pin, fork and archive — changes are mirrored both ways, so a pin
made on the phone shows up on the desktop.
Model control. A picker with real brand marks, plus a catalogue browser showing context windows and
available reasoning effort.
Balance and usage. One card fed either by an aggregated gateway endpoint or by per-vendor API keys.
Restart or quit DSH from the page, through an out-of-process helper that outlives the app.
Pictures that actually load. A same-origin image proxy resolves both real URLs and workspace paths,
with click-to-zoom.
Built for a phone. Glass UI, prompt rail, lazy-rendered transcript, and scroll compensation so
content that renders above the viewport never throws the reader off.
Token-gated, single process. One Host half with no build step, one static SPA, one shared secret.
Requirements
DeepSeek Harness desktop app — the plugin runs inside it and drives its Remotes.
Node.js 18+ — only to rebuild the SPA or run the test suites.
A phone, tablet or another computer on the same network, or a tunnel such as WireGuard or Tailscale.
Install
Clone this repository and add it to your DSH profile as a bundle. cordis.patch.yml carries the ready
profile line (bind, port, token).
Start DSH. The Host log prints a URL you can open directly:
http://127.0.0.1:19400/?k=<token>
Open it on the device. ?k= is used once only — the page exchanges the token for an
HttpOnly; SameSite=Strict cookie and cleans the address bar.
The token is generated on first start and stored in ~/.dsh/remote-control/auth.json with 0600
permissions. Set token: in the config to pin your own instead.
Configuration
- id: remote-control
name: '@local/dsh-remote-control'
config:
bind: '0.0.0.0' # 127.0.0.1 keeps it local; 0.0.0.0 exposes it to the network
port: 19400
token: '' # empty = generated once, then persisted
Exposing it
Tunnel (recommended). Keep bind: 127.0.0.1 and reach it through WireGuard, Tailscale or an SSH
forward. Nothing is exposed, and the page still gets a secure context.
Direct LAN. Set bind: 0.0.0.0 and open http://<lan-ip>:19400/?k=<token>. Over plain HTTP on a
non-loopback address the browser treats the page as an insecure context: installing it as an app,
notifications and some clipboard APIs are unavailable. Terminate TLS in a reverse proxy if you need them.
Windows firewall
Direct LAN access also needs an inbound rule. The repository ships a script (needs administrator
rights; it will raise a UAC prompt):
# allow inbound TCP 19400 for the Domain/Private profiles, local subnet only
pwsh -File plugins\remote-control\tools\firewall-remote-control.ps1
# remove the rule again
pwsh -File plugins\remote-control\tools\firewall-remote-control.ps1 -Remove
The rule is named DSH Remote Control (TCP <port>). It only opens the firewall — the listener still
follows bind, so the rule exposes nothing while bind is 127.0.0.1.
How to Dev
Build the UI
The Host serves ui/dist, so the SPA has to be built before it is used:
Every suite is a standalone script; none of them needs DSH to be running. The integration suite boots the
plugin in-process and drives it over real HTTP and SSE.
Browser half. The SPA reloads like any Vite app; layout, styles and components need no restart.
host/hot.js and its siblings are imported with an mtime-derived query, so an edit takes effect on
the next HTTP request — POST /api/__reload forces it.
index.js is part of the Loader's module generation: changes there need a DSH restart. Toggling
the bundle only re-applies the cached module.
Project Structure
plugins/remote-control/
├── index.js Host half: HTTP server, auth, routes, SSE, static files
├── host/
│ ├── hot.js Hot-reloadable routes: session management, DSH control, presets
│ ├── usage.js Balance and usage aggregation, gateway first
│ └── model-meta.js Context windows, joined from the gateway's /v1/models
├── ui/ Vue 3 + Vite SPA
│ ├── src/components/ Transcript, composer, sidebar, usage card, model browser …
│ ├── src/lib/ Store, Markdown pipeline, lazy rendering, API client
│ └── public/ App icons
├── locale/ UI strings (en, zh)
├── tools/ Test suites, smoke probe, firewall and DSH control scripts
├── docs/poster.png README hero image
├── page.html Fallback page used when ui/dist is missing
└── package.json, cordis.patch.yml Bundle manifest and profile line
HTTP API
Every route except / and /healthz requires the session cookie or Authorization: Bearer <token>.
Route
Purpose
GET /healthz
Unauthenticated liveness check
POST /api/login, /api/logout
Exchange the token for a cookie, and back
GET /api/state
Sessions, workspaces, pending interactions, bind info
GET /api/dsh/info, POST /api/dsh/restart, /api/dsh/exit
Describe, restart or quit DSH
POST /api/send, /api/cancel, /api/answer
Send a prompt, stop a turn, answer a question
GET /api/permissions, POST /api/permission
Permission presets and switching
Security
A 32-byte token gates every /api/* route; comparisons are constant-time, and both the session cookie
and an Authorization: Bearer header are accepted.
Failed logins are rate-limited per peer (6 per minute); request bodies are capped; static-path traversal
is rejected; the page runs under default-src 'none' with frame-ancestors 'none'.
Model output is sanitised with DOMPurify before it reaches the DOM — the page holds agent-control
credentials, so no model text is trusted.
A token is equivalent to code execution on the machine that runs the agent. Do not share it, and put
TLS or a tunnel in front of any non-loopback binding.
Limits, by design
Tool approvals cannot be confirmed remotely. Approval is a single-slot waterfall owned by the
desktop answerer; hijacking it would leave the desktop panel waiting for an answer that never arrives.
The page shows what is waiting and asks you to confirm on the desktop.
Switching permission presets needs a live session — a cold one is resumed first.
This is not a mirror of the desktop UI: no file tree, terminal, trace view or queue editing.
Released under the GNU Affero General Public License v3.0. In short: use it, change it, share it —
but if you run a modified version as a network service, you have to offer its source to the people using
it.