Streaming a desktop agent to your phone with zero npm dependencies A developer built DeepSeek Phone Harness, a mobile remote for DeepSeek Harness, using zero npm dependencies and a single Node process. The tool streams desktop agent events to a phone via WebSocket and REST, and the developer highlighted key architectural decisions, including a unified event stream and a fix for a deadlock caused by separate channels for approvals and questions. One WebSocket, one Node process, one HTML file. Here's the architecture behind DeepSeek Phone Harness, and the three decisions that made it work. Last weekend I built DeepSeek Phone Harness https://github.com/2903077918-lgtm/DeepSeek-phone-harness — a mobile remote for DeepSeek Harness. The constraint I set myself: zero npm dependencies . Not because I'm a purist, but because a tool like this should be "clone and run," not "install 47 packages and hope." Here's the architecture that made it work. Phone browser relay.html │ HTTPS / Tailscale ▼ Agent Node, :8788 ── http server + static page + WS relay │ 127.0.0.1:3080 ▼ DeepSeek Harness gateway dsh web The Agent is a translator, not a replacement: it speaks REST to the phone and RPC to DSH. DSH exposes events.mux — a WebSocket stream carrying approvals, questions, and session events in one pipe. Instead of opening one connection per concern, I keep one persistent connection and route frames locally: | Frame | Goes to | |---|---| approval/requested | pending table → phone approval cards | question/requested | pending table → phone answer cards | session/event | ring buffer 200/session → phone stream polling | The phone polls /api/events?afterSeq=N — each poll returns only new bytes, which keeps 4G streaming smooth. The single most important bug I fixed: DSH agents call ask user question through a separate channel from approvals. If you only listen for approval/requested , the moment the agent asks something the task deadlocks forever — it's waiting for a human answer that the phone never shows. The answer protocol had to match the Web GUI exactly: { "ok": true, "value": { "sessionId": "...", "answer": { "answers": { "id": "q1", "selected": "..." } } } } Every tool/call event creates a tool card name, icon, status, file path extracted from arguments . Every tool/result pairs back by callId and flips the card to done/failed. Tapping expands the full arguments and result. One trap: DSH's tool/result content is double-nested content .content .text — my first version read one level too shallow and results came back empty. Lesson: capture a real payload before trusting any schema. relay.html "Phone remote for a desktop agent" sounds like it needs a platform team. It doesn't. The barrier to entry for this whole category just collapsed — anyone with Node 22+ can build or extend one in a weekend. The code is open MIT : Questions about the architecture? Happy to go deeper in the comments.