dsh-mobi-web 0.0.0-stage → 0.1.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 rainfish-home
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,166 @@
1
- # Temporary Holding Version
1
+ # dsh-mobi-web
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ [![test](https://github.com/rainfish-home/dsh-mobi-web/actions/workflows/test.yml/badge.svg)](https://github.com/rainfish-home/dsh-mobi-web/actions/workflows/test.yml)
4
+
5
+ A phone-first web client for **DeepSeek Harness**, served by a sidecar gateway that runs *inside* the harness process.
6
+
7
+ > **Not `dsh-mobile-gateway`.** There is an unrelated npm package by that name ("scan a QR code + settings page + live sync"). This one is different: a **32 KB page** instead of a proxied UI, **approvals that stay on the phone when the phone started the turn**, and Mermaid rendered on the device. Package and repo are both `dsh-mobi-web`; the local checkout directory is still named `dsh-mobi-web-plugin`.
8
+
9
+ ```
10
+ phone browser ──HTTPS──▶ Cloudflare tunnel / Tailscale ──▶ 127.0.0.1:3083 (this plugin) ──▶ harness host services
11
+ ```
12
+
13
+ ## Read this first: what you are exposing
14
+
15
+ Holding the login token means **operating this machine through the harness**. The gateway proxies the whole harness, not just chat:
16
+
17
+ - send prompts, and read every session on the machine (history included)
18
+ - approve tool calls — **including shell commands** — and answer "yes" on your behalf
19
+ - with approvals, run anything the harness itself could run
20
+
21
+ So: **put it behind Tailscale** (a private tailnet) rather than a public tunnel unless you know what you are doing, use a token you generated for this and nothing else, and never reuse a password from another service. The token is stored in `$DSH_HOME/dsh-mobi-web-token` (mode 0600) and the log prints only its *path*.
22
+
23
+ ## What it looks like
24
+
25
+ ![The session list on a phone](docs/screenshot-sessions.png)
26
+
27
+ Sessions grouped by workspace, the running one marked, anything older than 24 h
28
+ folded away, archived ones hidden.
29
+
30
+ ![A conversation with a Mermaid diagram and a pending approval](docs/screenshot-conversation.png)
31
+
32
+ A conversation: Markdown, a Mermaid diagram rendered on the device, and an
33
+ approval card waiting at the bottom — the one the desktop never sees, because
34
+ the phone started this turn.
35
+
36
+ ## Why this exists
37
+
38
+ The official web UI ships ~4.6 MB of JavaScript. Over a relayed mobile link (0.4 MB/s in practice) that is ten-plus seconds of white screen, and the layout assumes a trackpad. This gateway serves a **~32 KB** page instead and talks to the same host services in-process, so it can do things a proxied UI cannot — like holding an approval the desktop never sees.
39
+
40
+ ## What you get
41
+
42
+ **Browse**
43
+ - Sessions grouped by workspace, newest activity first (same `SessionSummary.updatedAt` the desktop orders by)
44
+ - Split into *active* (running, or touched within 24 h) and *older* (collapsed, remembered per workspace)
45
+ - Archived sessions are filtered out; workspace collapse state persists
46
+
47
+ **Read**
48
+ - Markdown: headings, lists, tables, blockquotes, links, inline code, fenced code
49
+ - Mermaid fenced blocks render as SVG; tap one to open it full-screen (pan + pinch), tap again to close
50
+ - Long messages fold to ~460 px with "展开全文"
51
+ - Live token streaming: the reply grows word by word
52
+
53
+ **Do**
54
+ - Send multi-line text (Enter inserts a newline; the send button is the only way to send)
55
+ - Send images (up to 4, inline base64, 4 MB each)
56
+ - Stop a running turn
57
+ - Create a session in any workspace (`+` on the workspace header)
58
+ - Jump back to the newest message
59
+
60
+ **Approve**
61
+ - Approval requests arrive as cards over SSE
62
+ - **Origin affinity**: if the *phone* started the turn, the approval stays on the phone. It waits up to 15 minutes, then **cancels** — it never falls back to a desktop nobody is sitting at. Desktop-origin turns keep the old behaviour (20 s grace, then hand back).
63
+
64
+ **Stay up**
65
+ - SSE with exponential-backoff reconnect (2 s → 30 s) and a 6 s poll as fallback
66
+ - The gateway lives inside the harness process, so it starts and stops with it — no separate daemon to supervise
67
+
68
+ ## Install
69
+
70
+ The plugin is a DSH **bundle**: it ships its own `cordis.patch.yml`, so no profile file is edited by hand. There are two install paths, because the desktop profile is special.
71
+
72
+ **Web / other profiles** — the npm CLI:
73
+
74
+ ```bash
75
+ dsh plugin --profile web add dsh-mobi-web # from npm
76
+ dsh plugin --profile web add link:/path/to/dsh-mobi-web-plugin # from a checkout
77
+ ```
78
+
79
+ **The desktop profile** — `desktop` is reserved for the Electron-owned profile, and the *npm* CLI refuses to manage it. Install through the app's **plugin manager**, or with the Desktop-installed command (`dsh` inside the app bundle):
80
+
81
+ ```bash
82
+ dsh plugin --profile desktop add dsh-mobi-web
83
+ ```
84
+
85
+ Either way, after `add` the package is in `package.json` → `dsh.profile.bundles` (the CLI reconciles that list by inspecting each dependency's `dsh.bundle` declaration). **A new bundle only mounts on restart** — adding it changes nothing until the app or `dsh` starts again.
86
+
87
+ Then expose the port:
88
+
89
+ ```bash
90
+ tailscale serve --bg 3083 # phone on the same tailnet
91
+ # or a Cloudflare tunnel ingress: host → http://127.0.0.1:3083
92
+ ```
93
+
94
+ Open `https://<your-host>/m/`.
95
+
96
+ **The password.** If you do not set `token` in the patch config, one is minted on first start and written to `$DSH_HOME/dsh-mobi-web-token` (mode 0600). The startup log prints *where* it is, not the token itself — `cat` that file to read it. (The token is echoed only when the file could not be written, i.e. when that process holds the only copy.) Set `token: '…'` to choose your own, or `token: false` to disable the gate — only on a network you fully trust.
97
+
98
+ To override anything, add a row to your profile's `cordis.patch.yml`. A patch **replaces the row's whole `config` object**, so restate every non-default value:
99
+
100
+ ```yaml
101
+ - id: dsh-mobi-web
102
+ config:
103
+ listenHost: '127.0.0.1'
104
+ listenPort: 3083
105
+ blockPaths: ['/dsh-whale/']
106
+ ```
107
+
108
+ ## Configuration
109
+
110
+ | key | default | meaning |
111
+ | --- | --- | --- |
112
+ | `listenHost` | `127.0.0.1` | never bind wider; a tunnel does the rest |
113
+ | `listenPort` | `3083` (bundle patch; the code default is `3080`) | |
114
+ | `token` | auto-minted file | password for the cookie gate; `false` disables it |
115
+ | `allowTokenUrl` | `false` | accept `?t=…` links (leaks via history, off by default) |
116
+ | `blockPaths` | `[]` | path prefixes answered `204` to keep the page light |
117
+ | `approvalPriority` | `'phone-first'` | `'desktop'` disables interception entirely |
118
+ | `pwa` | `true` | inject manifest + icon into the proxied UI |
119
+ | `brotli` | `true` | recompress JS for the proxied UI |
120
+
121
+ ## Routes
122
+
123
+ | route | what |
124
+ | --- | --- |
125
+ | `GET /m/` | the mobile page (cookie-gated) |
126
+ | `GET /m/api/sessions` | workspace-grouped list (10 s cache) |
127
+ | `GET /m/api/session?id=` | messages (newest 40 / 120 k chars) + pending approvals |
128
+ | `GET /m/api/events?session=` | SSE: `state`, `message`, `astart`, `adelta`, `aend`, `tool`, `approval`, `approval-done` |
129
+ | `POST /m/api/prompt` | `{id, text, images[]}` — verifies the message actually landed |
130
+ | `POST /m/api/cancel` | interrupt the running turn |
131
+ | `POST /m/api/create` | `{cwd}` → new session |
132
+ | `POST /m/api/approve` | `{id, decision}` |
133
+ | `GET /__gw/health` | unauthenticated liveness |
134
+ | `GET /__gw/stats` | counters + approval diagnostics (cookie-gated) |
135
+
136
+ ## Design notes
137
+
138
+ - **No `0.0.0.0`.** `dsh` refuses to start on a non-loopback host, so a sidecar proxy is the only option; the tunnel terminates TLS outside.
139
+ - **Cookie minting happens server-side.** Letting the launch-token flow redirect through `303` dropped the `SameSite=Strict` cookie on iOS, producing `ERR_TOO_MANY_REDIRECTS`.
140
+ - **Every request handler is wrapped in try/catch.** dsh treats an uncaught exception as fatal; a sidecar must never crash the host.
141
+ - **The profile links the checkout.** Editing `lib/index.js` and restarting is the deploy; `lib/page.html` is read per request, so it needs no restart at all.
142
+
143
+ ## Limits worth knowing
144
+
145
+ - Single user, single host. No account model beyond one shared token.
146
+ - No push notifications: if the phone is asleep, an approval waits for you (up to 15 min) rather than pinging you.
147
+ - Images inside *historical* messages render as `[图片]` — only the fact they existed is kept.
148
+ - Mermaid is vendored (857 KB brotli), lazy-loaded on the first diagram.
149
+ - Sessions cannot be moved between workspaces — DSH derives membership from the session's `cwd` at creation.
150
+ - **Compatibility**: verified on DSH `0.2.2-rc.1`. The gateway reads host services through `ctx.get('sessionQuery')` and friends — private API, not a stable contract. A dsh upgrade may break it; the page and the HTTP layer would keep working.
151
+
152
+ ## Repo layout
153
+
154
+ ```
155
+ lib/index.js the gateway plugin (single file on purpose)
156
+ lib/page.html the mobile page, served from disk on every request
157
+ assets/ vendored mermaid (js + gz + br) and the app icon
158
+ cordis.patch.yml bundle patch — the install declaration
159
+ test/ 27 tests, fake host services, no network
160
+ ```
161
+
162
+ ## Tests
163
+
164
+ ```bash
165
+ node --test test/*.test.mjs # 27 tests, no network, fake host services
166
+ ```
@@ -0,0 +1,10 @@
1
+ <!doctype html><html><head><meta charset="utf-8"><style>
2
+ html,body{margin:0;width:180px;height:180px;overflow:hidden}
3
+ body{background:linear-gradient(150deg,#5b78ff 0%,#3547b8 62%,#26307a 100%);display:flex;align-items:center;justify-content:center}
4
+ svg{width:112px;height:112px}
5
+ </style></head><body>
6
+ <svg viewBox="0 0 24 24" fill="none">
7
+ <path d="M2.5 9.2c3.8 0 5.6-3.2 9.5-3.2s5.7 3.2 9.5 3.2" stroke="#fff" stroke-width="2.1" stroke-linecap="round"/>
8
+ <path d="M2.5 14.8c3.8 0 5.6-3.2 9.5-3.2s5.7 3.2 9.5 3.2" stroke="#fff" stroke-width="2.1" stroke-linecap="round" opacity=".72"/>
9
+ <circle cx="12" cy="20" r="1.35" fill="#fff" opacity=".92"/>
10
+ </svg></body></html>
Binary file