src/dashboard/sse.ts.
- const delay = 1000; + const delay = Math.min(30_000, 1_000 * 2 ** attempts); + scheduleHeartbeat(15_000);
[sse] 4 pass · 0 fail · 0 skip [runtime] ✓ reconnects with jittered backoff [runtime] ✓ heartbeat fires every 15s
Web dashboard with multi-project tabs, live SSE streaming, Telegram and Web Push notifications, optional public tunnel, and a mobile PWA to follow your sessions from anywhere. Works natively with OpenCode and with Codex CLI via a hook bridge.
OpenCode (and Codex) are agents that live in your terminal. They work great while you sit in front of them — but the moment you walk away, everything stops. The agent asks for permission to run a command, no one is there to approve. You want to follow the run from your phone while doing something else, you can't. You open another project in another terminal and lose the thread.
Dangerous tools need approval, but you're not there.
From mobile, another laptop, or any device on your network.
Each one with its own tab. Switch context without losing sessions.
Web Push or Telegram — in real time, even if the dashboard is closed.
Async pair programming behind a public tunnel.
OpenCode and Codex usually live in separate tabs — here they coexist.
The integrations/ layer implements a single AgentIntegration port. Each CLI lives in its own folder and the composition root wires it with one line. Adding a new agent (Cursor, Aider, etc.) is one folder + one line.
Native integration via OpenCode SDK hooks. The plugin loads inside the TUI process: it sees session events, permission.ask, tool.start/end, and emits each one to the internal event bus.
integrations/opencode/
├── index.ts # opencodeIntegration
└── hooks/
├── event.ts
├── permission.ask.ts
└── tool.ts Codex has no plugin SDK — it uses HTTP hooks. The plugin exposes POST /codex/hooks/:event and Codex posts there. Same event bus, same dashboard, same permission queue.
# ~/.codex/config.toml
[hooks]
endpoint = "http://127.0.0.1:4097/codex/hooks"
token = "${PILOT_HOOK_TOKEN}" | Method | Path | Description |
|---|---|---|
| POST | /codex/hooks/SessionStart | Codex opened a session — emits pilot.session.started |
| POST | /codex/hooks/UserPromptSubmit | Prompt received — emits pilot.prompt.received |
| POST | /codex/hooks/PreToolUse | Tool about to run — emits pilot.tool.started |
| POST | /codex/hooks/PostToolUse | Tool finished — emits pilot.tool.completed |
| POST | /codex/hooks/PermissionRequest | Blocking — waits for your approve/deny on the dashboard |
| POST | /codex/hooks/Stop | Session ended — emits pilot.session.stopped |
An HTTP+SSE server runs inside the plugin process. The dashboard is a vanilla ES-modules SPA served from the same origin — no React, no build, no latency.
Tabs for every project you opened with OpenCode or Codex. Switch context without losing sessions. Each tab remembers its own sessions, messages, and state.
Every token shows up as the agent generates it. No reload, no polling, no latency. Real typewriter effect.
When the agent (OpenCode or Codex) asks permission, a banner appears on the dashboard — even on your phone. Approve or reject with one tap. If there's a queue, the 1/N counter shows up.
Ctrl/Cmd + K opens the palette. Switch agent, model, provider, create a new session, switch project — all from the keyboard.
Configure port, host, tunnel, Telegram, VAPID keys, hookToken, timeouts and more from a modal. Saved atomically to ~/.opencode-pilot/config.json.
Explore the project tree, search by glob pattern with debounce, and open files to view contents (opt-in).
Receive notifications even with the dashboard closed. Generate VAPID keys from Settings with a click.
Configure token + chat ID (with hints in the dashboard). Get permission, error, and end-of-session alerts on Telegram.
Uses the native Notifications API when permission is granted. Inline notifications without losing the session.
Responsive interface. Install the dashboard as an app from the browser menu. Service worker with versioned cache and network change detection.
One QR for LAN, another for public tunnel. Pair in seconds from your phone.
/remote opens the dashboard and automatically focuses the tab for the folder you're working in. If it doesn't exist, it creates it.
Send prompts, approve permissions, and switch agents as if you were at the desktop.
PILOT_TUNNEL=cloudflared (or ngrok) and the plugin spins up a tunnel automatically, detects the URL, shows it on the dashboard, and renders a QR.
POST /codex/hooks/:event receives Codex CLI's 6 lifecycle hooks. PermissionRequest is blocking until you approve from the dashboard.
If OpenCode restarted and your token went stale, the "invalid token" screen lets you paste the fresh URL and extracts the new token by itself.
bunx @lesquel/opencode-pilot doctor runs 6 health checks in <2 seconds. uninstall reverts the whole installation (optional --keep-config).
Each Settings field has a hint with where to get the value. On blur, validates basic format without blocking save.
Click on the sessions to see how the transcript changes. Everything runs from the same origin as the CLI process — zero artificial latency.
src/dashboard/sse.ts.
- const delay = 1000; + const delay = Math.min(30_000, 1_000 * 2 ** attempts); + scheduleHeartbeat(15_000);
[sse] 4 pass · 0 fail · 0 skip [runtime] ✓ reconnects with jittered backoff [runtime] ✓ heartbeat fires every 15s
Since v1.18.0, folders announce their purpose, not their technology. The dependency rule is strict: infra → core → (transport, integrations, notifications) → server. Adding a new CLI is one folder + one line in the composition root.
┌─────────────────────┐ ┌─────────────────────────┐
│ OpenCode TUI │ │ Codex CLI (hooks) │
│ (SDK plugin) │ │ POST /codex/hooks/* │
└──────────┬──────────┘ └──────────┬──────────────┘
│ │
▼ ▼
┌──────────────────────────────────────────┐
│ integrations/ ports.ts │
│ ├── opencode/ (native SDK) │
│ └── codex/ (HTTP bridge) │
└─────────────────┬────────────────────────┘
│
┌─────────────────▼────────────────────────┐
│ core/ │ ← domain
│ permissions · events · audit · settings │
└─────────────────┬────────────────────────┘
│
┌─────────────────▼────────────────────────┐
│ transport/http ←→ notifications/ │
│ Bun.serve + SSE Telegram + Push │
└─────────────────┬────────────────────────┘
│
┌─────────────────▼────────────────────────┐
│ dashboard/ (vanilla SPA, same origin) │
└──────────────────────────────────────────┘ Permissions, events, audit, settings, state, errors. Zero HTTP / Telegram / Codex.
Bun.serve + routes + handlers + middlewares. The way the core is exposed to the world.
OpenCode (SDK hooks) and Codex (HTTP hooks). Single port: AgentIntegration.
Telegram and Web Push. Single port: NotificationChannel. Pipeline orchestrates the fan-out.
Tunnel, QR, banner, logger, network, auth, paths, dotenv, circuit breaker.
Vanilla SPA served by transport. No React, no build, native ES modules.
Slash commands for OpenCode TUI + CLI binary (init, doctor, uninstall).
Façade: the only file that imports across all layers. Wires and returns the handle.
The installer locates your OpenCode config dir (XDG, APPDATA, etc.), registers the plugin in opencode.json and tui.json, and cleans up old wrappers. For Codex, you add the endpoint in ~/.codex/config.toml.
OpenCode (npm i -g opencode) or Codex CLI installed. Bun (recommended) or modern Node.
A single line. Detects XDG_CONFIG_HOME / APPDATA automatically.
Close every TUI session and reopen. You'll see a toast: "Remote control plugin loaded".
Type /remote in the TUI. The browser opens. The banner prints URL, token, and QR.
Your agent is running a long refactor. You step away for coffee. On your phone, a Web Push notification tells you it's asking permission to run rm -rf node_modules. You approve with one tap.
You enable the tunnel (PILOT_TUNNEL=cloudflared). The dashboard becomes accessible via a public link. A teammate follows the session live for async pair programming.
You have OpenCode working on a refactor and Codex generating tests on another terminal. Both events flow into the same dashboard, in separate tabs, with coordinated permissions.
Three terminals, three projects, one dashboard. Tabs save state across switches. Zero context-switch tax.
You need to see what the agent did 2 hours ago on the CI server. You connect via the tunnel, open the session, read the full history, download the diff.
Telegram bot connected to a team channel. Whenever an agent finishes a long task, the channel gets a summary. Everyone sees progress without opening the dashboard.
Same WiFi with PILOT_HOST=0.0.0.0, or expose a public tunnel with Cloudflare or ngrok. The dashboard is mobile-first: drawer sidebar, bottom-sheets, 44×44 tap targets.
PILOT_HOST=0.0.0.0 PILOT_TUNNEL=cloudflared 🔒 The tunnel URL contains the token. Treat it like a password — rotate it with /pilot-token.
For people who prefer typing over clicking. Press ? inside the dashboard for the full palette.
tui.json::plugin includes the spec. OpenCode uses two loaders — one for the server, one for the TUI — and they need separate registrations. Since v1.12 you edit everything from the gear icon. Values live in ~/.opencode-pilot/config.json. If you prefer files, a .env works too — and shell env vars always win.
| Variable | Default | Description |
|---|---|---|
| PILOT_PORT | 4097 | HTTP server port |
| PILOT_HOST | 127.0.0.1 | Bind address. 0.0.0.0 for LAN. |
| PILOT_TUNNEL | — | cloudflared or ngrok for public access |
| PILOT_PERMISSION_TIMEOUT | 300000 | Permission request timeout (ms) |
| PILOT_HOOK_TOKEN new | — | Dedicated bearer token for /codex/hooks/*. Accepted alongside the main token. |
| PILOT_CODEX_PERMISSION_TIMEOUT_MS new | 250000 | Timeout for Codex PermissionRequest (max 250000 — Bun.serve idleTimeout cap) |
| PILOT_PROJECT_STATE new | auto | auto | always | off. Controls per-project pilot-state writes |
| PILOT_DEV new | false | Re-reads dashboard HTML on every request (DX) |
| PILOT_TELEGRAM_TOKEN | — | @BotFather bot token for Telegram notifications |
| PILOT_TELEGRAM_CHAT_ID | — | Chat ID to send alerts to |
| PILOT_VAPID_PUBLIC_KEY | — | Web Push public key (generate with one click in Settings) |
| PILOT_VAPID_PRIVATE_KEY | — | Web Push private key |
| PILOT_ENABLE_GLOB_OPENER | false | Enables /fs/glob and /fs/read for glob search from the dashboard |
| PILOT_FETCH_TIMEOUT_MS | 10000 | Timeout for outbound HTTP calls (Telegram, push) |
OpenCode Pilot started as a remote dashboard. Today it's the bridge between your agent and any device. The next step is clear: integrate it where we spend the most time.
Two real integrations in production. 6 Codex hook endpoints, 9+ shared event types, same dashboard, same permission queue.
A native VS Code extension. Embedded webview with the dashboard, palette commands, status bar with active sessions, notifications via the VS Code Notifications API, and a URI handler to open from external links. Goal: opening the dashboard becomes a shortcut instead of a /remote.
Multi-tenant relay service so NAT and firewalls stop being a blocker (design in docs/CLOUD_RELAY_v2_DESIGN.md). More adapters: Cursor, Aider, Continue.dev — the AgentIntegration port is ready, only each bridge has to be written.
Want to contribute or suggest? Open issues at GitHub Issues — especially the VS Code integration.
The repo's docs/ folder covers everything from the loader architecture to the future relay design.
Every env var, with sample .env files for common scenarios.
read →Why OpenCode needs two plugin entries and how to debug them.
read →Full bridge setup: config.toml, hookToken, payload examples.
read →Screaming Architecture, dependency rules, and why no classes.
read →End-to-end verification guide for the tunnel, with security checklist.
read →Architecture for the future centralized service. Design, not implementation.
read →