[PR #12000] feat(desktop): web server mirror — true 1:1 remote access via TCP reverse proxy #14021

Open
opened 2026-02-16 18:18:50 -05:00 by yindo · 0 comments
Owner

Original Pull Request: https://github.com/anomalyco/opencode/pull/12000

State: open
Merged: No


Summary

Adds a Web Server Mirror to the desktop app — a TCP reverse proxy that exposes the desktop's existing server to the local network, giving a true 1:1 mirror of the desktop UI accessible from any browser (phone, tablet, second laptop).

Closes #11997

Why not just use opencode web?

Running opencode web spawns a completely separate server instance. While it shares the filesystem, it has its own in-memory state — meaning SSE events, WebSocket streams, and live session updates don't sync between desktop and web. You'd see stale data, miss notifications, and have to manually manage the lifecycle.

A TCP reverse proxy is the right solution: it pipes raw bytes through, so the browser talks directly to the desktop server. Same auth, same events, same state — zero duplication.

opencode web (separate server) TCP proxy (this PR)
State sync Separate in-memory state Same server instance
Live updates (SSE/WS) Don't cross instances Pass through directly
Auth Separate credentials Same Basic Auth
Lifecycle Manual start/stop Auto-start/stop with desktop
Sidebar sync None One-way (Desktop → Mirror)

How it works

Browser → http://192.168.x.x:4096
  → TCP Proxy (Rust/tokio, 0.0.0.0:4096)
    → Sidecar (127.0.0.1:<random-port>)
      → Serves bundled mirror UI + API

One-way Sidebar Sync

The mirror shows the same projects in the same order as the desktop:

  1. Desktop pushes sidebar state to PUT /mirror/sidebar on every change (debounced 300ms)
  2. Server stores it and broadcasts via SSE (mirror.sidebar.updated)
  3. Mirror reads on startup + reacts to live SSE updates

Strictly one-way (Desktop → Mirror) — no reactive loops, no conflict resolution needed.

Security: What's disabled in the mirror and why

  • Terminal is hidden — HTTP Basic Auth alone isn't sufficient to expose a shell. Anyone with the password could execute arbitrary commands on the host. Terminal remains desktop-only.
  • "Add Project" / "+" button is hidden — Opening a directory picker requires a native file dialog with OS-level permission prompts (macOS/Linux). This can only be confirmed locally, not through a remote browser. Projects are added on the desktop and automatically synced to the mirror.

Mirror UI Detection

The mirror entry point sets platform: "desktop" (same UI) but without platform.storage (no Tauri APIs):

  • Real Desktop: platform === "desktop" && storage !== undefined
  • Mirror: platform === "desktop" && storage === undefined

Authentication

HTTP Basic Auth with credential resolution:

  1. Settings UI values (user-configured)
  2. Shell env vars (OPENCODE_SERVER_USERNAME / OPENCODE_SERVER_PASSWORD)
  3. Defaults (opencode / random UUID)

Features

  • TCP reverse proxy in Rust/tokio — lightweight, handles all protocols (HTTP, SSE, WebSocket)
  • Settings UI in Settings → General → Desktop (enable/disable, port)
  • Status indicator in Servers tab (green dot, local + network URL, credentials)
  • Auto-start after sidecar is ready (if enabled), auto-stop on exit
  • Orphan cleanup — kills zombie processes on the configured port from previous runs
  • Pre-built mirror UI — Vite builds dist-mirror/, embedded as Tauri resource
  • i18n — all 15 languages with native translations

Use Cases

  • Check on running AI sessions from your phone while away from your desk
  • Use OpenCode on a second monitor/laptop over the local network
  • Run OpenCode on a remote server, connect from anywhere via browser — it keeps working autonomously
  • Quick remote access without SSH or terminal setup

Files changed (39 files, +1362 lines)

Rust (desktop core)
  • lib.rsWebMirrorState, TCP proxy, 3 Tauri commands, auto-start/stop, orphan cleanup, credential resolution
  • cli.rsprobe_shell_env() for reading env vars from login shell
TypeScript (desktop bindings)
  • bindings.ts — Tauri command bindings + types
  • index.tsx — Platform integration
App (shared UI)
  • entry-mirror.tsx — Mirror entry point (platform: "desktop" without Tauri)
  • mirror.html + vite.mirror.config.ts — Mirror build
  • platform.tsx — Type definitions
  • settings-general.tsx — Settings section
  • status-popover.tsx — Status indicator
  • layout.tsx — One-way sidebar sync
  • global-sync.tsx — SSE field for mirror sidebar
  • session-header.tsx, home.tsx, layout.tsx (pages), session.tsx — Hide terminal / add-project in mirror
Server (Hono)
  • server.tsGET/PUT /mirror/sidebar + SSE broadcast
  • flag.tsOPENCODE_WEB_DIR env flag
Build
  • predev.ts / prepare.ts — Mirror build steps
  • tauri.conf.jsonweb-ui resource
i18n (15 files)

All language files with native translations (ar, br, da, de, en, es, fr, ja, ko, no, pl, ru, th, zh, zht)

**Original Pull Request:** https://github.com/anomalyco/opencode/pull/12000 **State:** open **Merged:** No --- ## Summary Adds a **Web Server Mirror** to the desktop app — a TCP reverse proxy that exposes the desktop's existing server to the local network, giving a **true 1:1 mirror** of the desktop UI accessible from any browser (phone, tablet, second laptop). Closes #11997 ## Why not just use `opencode web`? Running `opencode web` spawns a **completely separate server instance**. While it shares the filesystem, it has its own in-memory state — meaning SSE events, WebSocket streams, and live session updates **don't sync** between desktop and web. You'd see stale data, miss notifications, and have to manually manage the lifecycle. A TCP reverse proxy is the right solution: it pipes raw bytes through, so the browser talks **directly to the desktop server**. Same auth, same events, same state — zero duplication. | | `opencode web` (separate server) | **TCP proxy (this PR)** | |---|---|---| | State sync | ❌ Separate in-memory state | ✅ Same server instance | | Live updates (SSE/WS) | ❌ Don't cross instances | ✅ Pass through directly | | Auth | ❌ Separate credentials | ✅ Same Basic Auth | | Lifecycle | ❌ Manual start/stop | ✅ Auto-start/stop with desktop | | Sidebar sync | ❌ None | ✅ One-way (Desktop → Mirror) | ## How it works ``` Browser → http://192.168.x.x:4096 → TCP Proxy (Rust/tokio, 0.0.0.0:4096) → Sidecar (127.0.0.1:<random-port>) → Serves bundled mirror UI + API ``` ### One-way Sidebar Sync The mirror shows the same projects in the same order as the desktop: 1. **Desktop** pushes sidebar state to `PUT /mirror/sidebar` on every change (debounced 300ms) 2. **Server** stores it and broadcasts via SSE (`mirror.sidebar.updated`) 3. **Mirror** reads on startup + reacts to live SSE updates Strictly one-way (Desktop → Mirror) — no reactive loops, no conflict resolution needed. ### Security: What's disabled in the mirror and why - **Terminal is hidden** — HTTP Basic Auth alone isn't sufficient to expose a shell. Anyone with the password could execute arbitrary commands on the host. Terminal remains desktop-only. - **"Add Project" / "+" button is hidden** — Opening a directory picker requires a native file dialog with OS-level permission prompts (macOS/Linux). This can only be confirmed locally, not through a remote browser. Projects are added on the desktop and automatically synced to the mirror. ### Mirror UI Detection The mirror entry point sets `platform: "desktop"` (same UI) but **without** `platform.storage` (no Tauri APIs): - Real Desktop: `platform === "desktop" && storage !== undefined` - Mirror: `platform === "desktop" && storage === undefined` ### Authentication HTTP Basic Auth with credential resolution: 1. Settings UI values (user-configured) 2. Shell env vars (`OPENCODE_SERVER_USERNAME` / `OPENCODE_SERVER_PASSWORD`) 3. Defaults (`opencode` / random UUID) ## Features - **TCP reverse proxy** in Rust/tokio — lightweight, handles all protocols (HTTP, SSE, WebSocket) - **Settings UI** in Settings → General → Desktop (enable/disable, port) - **Status indicator** in Servers tab (green dot, local + network URL, credentials) - **Auto-start** after sidecar is ready (if enabled), auto-stop on exit - **Orphan cleanup** — kills zombie processes on the configured port from previous runs - **Pre-built mirror UI** — Vite builds `dist-mirror/`, embedded as Tauri resource - **i18n** — all 15 languages with native translations ## Use Cases - Check on running AI sessions from your phone while away from your desk - Use OpenCode on a second monitor/laptop over the local network - Run OpenCode on a remote server, connect from anywhere via browser — it keeps working autonomously - Quick remote access without SSH or terminal setup ## Files changed (39 files, +1362 lines) <details> <summary>Rust (desktop core)</summary> - `lib.rs` — `WebMirrorState`, TCP proxy, 3 Tauri commands, auto-start/stop, orphan cleanup, credential resolution - `cli.rs` — `probe_shell_env()` for reading env vars from login shell </details> <details> <summary>TypeScript (desktop bindings)</summary> - `bindings.ts` — Tauri command bindings + types - `index.tsx` — Platform integration </details> <details> <summary>App (shared UI)</summary> - `entry-mirror.tsx` — Mirror entry point (`platform: "desktop"` without Tauri) - `mirror.html` + `vite.mirror.config.ts` — Mirror build - `platform.tsx` — Type definitions - `settings-general.tsx` — Settings section - `status-popover.tsx` — Status indicator - `layout.tsx` — One-way sidebar sync - `global-sync.tsx` — SSE field for mirror sidebar - `session-header.tsx`, `home.tsx`, `layout.tsx` (pages), `session.tsx` — Hide terminal / add-project in mirror </details> <details> <summary>Server (Hono)</summary> - `server.ts` — `GET/PUT /mirror/sidebar` + SSE broadcast - `flag.ts` — `OPENCODE_WEB_DIR` env flag </details> <details> <summary>Build</summary> - `predev.ts` / `prepare.ts` — Mirror build steps - `tauri.conf.json` — `web-ui` resource </details> <details> <summary>i18n (15 files)</summary> All language files with native translations (ar, br, da, de, en, es, fr, ja, ko, no, pl, ru, th, zh, zht) </details>
yindo added the pull-request label 2026-02-16 18:18:50 -05:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: anomalyco/opencode#14021