Repository navigation
Description
Problem
The OpenCode desktop app runs a local server on 127.0.0.1, inaccessible from other devices. To access OpenCode remotely, users must manually run opencode web in a terminal — but this starts a completely separate server instance with its own in-memory state. That means:
- No live sync — SSE events, WebSocket updates, and session changes don't propagate between desktop and web
- No sidebar sync — projects opened/closed on desktop don't reflect in the web UI
- Separate auth — different credentials, different session cookies
- Manual lifecycle — users must start/stop the web server themselves
This makes remote access impractical for real use.
Solution: TCP Reverse Proxy (True 1:1 Mirror)
Instead of spawning a second server, we add a TCP reverse proxy written in Rust (using tokio::net) that forwards raw TCP bytes from 0.0.0.0:<port> to the desktop's local sidecar server. This gives a true 1:1 mirror — same sessions, same live updates (SSE/WebSocket), same auth, same everything — because it just pipes bytes through without interpreting them.
Why a TCP proxy instead of a separate server?
| Approach | State Sync | Live Updates | Auth | Complexity |
|---|---|---|---|---|
opencode web (separate server) |
❌ Separate in-memory state | ❌ SSE/WS don't cross instances | ❌ Separate credentials | Spawns full server |
| TCP reverse proxy (this PR) | ✅ Same server, same state | ✅ All events pass through | ✅ Same auth (Basic Auth) | Lightweight byte-pipe |
The TCP proxy is the right abstraction: it operates at the transport layer, so every HTTP request, SSE stream, and WebSocket connection from the browser goes directly to the same server the desktop uses. No protocol-level interpretation, no state duplication, no sync issues.
Sidebar Synchronization
On top of the TCP proxy, we add one-way sidebar sync (Desktop → Server → Mirror) so the browser mirror shows the same projects in the same order as the desktop:
- Desktop pushes its sidebar state (
worktree,expanded) toPUT /mirror/sidebaron every change (debounced 300ms) - Server stores it via
Storage.write()and broadcasts via SSE (mirror.sidebar.updated) - Mirror reads initial state from
GET /mirror/sidebaron startup, then reacts to SSE updates
This is strictly one-way — the mirror never writes back — which eliminates reactive loops and conflict resolution entirely.
Architecture
Browser (phone/tablet/laptop)
→ http://192.168.x.x:4096
→ TCP Proxy (Rust/tokio, binds 0.0.0.0:4096)
→ Sidecar (127.0.0.1:<random-port>)
→ If OPENCODE_WEB_DIR is set: serve bundled mirror UI
→ Else: proxy to https://app.opencode.ai
Platform Detection
The mirror runs as platform: "desktop" (unlocks all desktop UI sections) but without platform.storage (no Tauri APIs). This distinction is how the app knows it's a mirror:
- Real Desktop (Tauri):
platform.platform === "desktop" && platform.storage !== undefined - Mirror (browser):
platform.platform === "desktop" && platform.storage === undefined
Security Decisions
-
Terminal is hidden in the mirror UI — allowing terminal access via a browser with only HTTP Basic Auth would let anyone with the password execute arbitrary commands on the host machine. Terminal remains desktop-only.
-
"Add Project" button is hidden — on macOS/Linux, opening a directory picker requires a native file dialog that prompts for filesystem permissions. This can only be confirmed locally on the machine, not through a remote browser. Projects can only be added from the desktop; the mirror automatically syncs them.
-
Authentication uses HTTP Basic Auth (same as
opencode web). The browser shows its native login dialog and caches credentials per origin. Credential resolution priority:- Settings UI values (user-configured username/password)
- Shell environment variables (
OPENCODE_SERVER_USERNAME/OPENCODE_SERVER_PASSWORD) - Defaults (
opencode/ random UUID)
Use Cases
- Phone/tablet access — check on running sessions, review changes, read output while away from your desk
- Second monitor/laptop — run OpenCode on your main machine, interact from another device on the same network
- Remote server — run OpenCode on a powerful server, connect from your desktop via the web UI; it keeps working autonomously even when you disconnect
- Quick remote check — no SSH, no terminal setup, just open a browser and go
Features
- Settings UI in Settings → General → Desktop with enable/disable toggle and port configuration
- Status indicator in the Servers tab of the status popover (green dot + local/network URL + credentials)
- Auto-start — if enabled, starts automatically when the desktop app launches (after sidecar is ready)
- Auto-stop — stops when the desktop app exits
- Orphan cleanup — if a previous OpenCode instance left a zombie process on the port, it's automatically killed
- Pre-built mirror UI — Vite builds a separate
dist-mirror/bundle that gets embedded in the desktop app as a Tauri resource, served by the sidecar whenOPENCODE_WEB_DIRis set - i18n — all 15 languages with native translations
Technical Details
Files Changed (39 files, +1362 lines)
Rust (desktop core):
lib.rs—WebMirrorState,WebMirrorConfig,WebMirrorStatus, TCP proxy viatokio::net, 3 Tauri commands (start_web_mirror,stop_web_mirror,get_web_mirror_status), auto-start/stop lifecycle, orphan port cleanup, credential resolutioncli.rs—probe_shell_env()reads env vars from the user's login shell (.zshrc,.bashrcetc.)
TypeScript (desktop bindings):
bindings.ts— Tauri command bindings + typesindex.tsx— Platform integration
App (shared UI):
entry-mirror.tsx— Mirror entry point (setsplatform: "desktop"without Tauri APIs)mirror.html— HTML shell for the mirror buildvite.mirror.config.ts— Vite config for the mirror bundleplatform.tsx—WebMirrorConfig,WebMirrorStatustype definitionssettings-general.tsx— Web Mirror settings sectionstatus-popover.tsx— Status indicator with pollinglayout.tsx— One-way sidebar sync (Desktop push + Mirror apply)global-sync.tsx—mirrorSidebarSSE fieldserver.tsx—StoredProjecttype used by sidebar syncsession-header.tsx— Hide terminal in mirrorhome.tsx— Hide "Open Project" button in mirrorlayout.tsx(pages) — Hide sidebar "+" button in mirrorsession.tsx— Hide terminal panel in mirror
Server (Hono):
server.ts—GET/PUT /mirror/sidebarendpoints + SSE broadcastflag.ts—OPENCODE_WEB_DIRenvironment flag
Build:
predev.ts/prepare.ts— Mirror build stepstauri.conf.json—web-uiresource declaration
i18n (15 files):
- All language files with native translations for web mirror strings
Activity
github-actions commentedon Feb 3, 2026
This issue might be a duplicate of existing issues. Please check:
Feel free to ignore if none of these address your specific case.
mguttmann commentedon Feb 4, 2026
mguttmann commentedon Feb 4, 2026
mguttmann commentedon Feb 4, 2026
mguttmann commentedon Feb 4, 2026
Thanks for flagging these — both issues describe exactly the problem this feature solves, and then some:
#11934 — "Access web interface from different device" — asks for monitoring sessions from other devices over a VPN without relying on
localStorage. Our TCP reverse proxy does exactly this: same server, same state, same live SSE/WebSocket streams. NolocalStoragedependency because the mirror reads sidebar state from the server viaGET /mirror/sidebar+ SSE updates. Openhttp://<ip>:4096from your phone and you see everything live.#11331 — "Opencode web should display sessions history in the sidebar" — the core problem is that
opencode webstarts a separate server instance with its own in-memory state, so sessions from the main server don't show up. Our approach eliminates this entirely: the TCP proxy forwards to the same server the desktop uses, so all sessions, all history, all live updates are there automatically. No sync needed because there's nothing to sync — it's the same server.Both issues would be fully resolved by this PR (#12000).
github-actions commentedon Apr 5, 2026
To stay organized issues are automatically closed after 90 days of no activity. If the issue is still relevant please open a new one.
itlackey commentedon May 12, 2026
Why is every issue and PR around reusing existing server from desktop app closed with no resolution? It is obviously a highly requested feature the community is willing to contribute.