Yaojia Wang 242a4e0dc1
Some checks failed
ios / package-tests (APIClient) (push) Has been cancelled
ios / package-tests (HostRegistry) (push) Has been cancelled
ios / package-tests (SessionCore) (push) Has been cancelled
ios / package-tests (WireProtocol) (push) Has been cancelled
ios / testsupport-tests (push) Has been cancelled
ios / app-tests (push) Has been cancelled
ios / ipad-tests (push) Has been cancelled
ios / integration-tests (push) Has been cancelled
ios / ui-test (push) Has been cancelled
ios / ios17-floor-tests (push) Has been cancelled
relay-tripwire / cross-tenant-tripwire (push) Has been cancelled
docs: log Orbit icon rollout across iOS/desktop/tray/web + desktop repackage
2026-07-06 11:54:48 +02:00

Web Terminal

A self-hosted, browser-based terminal and Claude-Code session/project workbench. It exposes the host machine's local shell over WebSocket: open http://<host-ip>:3000 from any device on your LAN (phone, tablet, another laptop) and you get a live, interactive shell. The core use case is vibe coding — hand Claude Code a task, walk away, then reconnect from anywhere to check on it, approve a tool call from your phone's lock screen, or kick off the next one.

Sessions survive disconnects: the shell (and whatever's running in it) keeps going when you close the tab; reconnect and the scrollback replays. The server is a byte-shuttle — it ferries raw bytes between the shell and the browser and never parses terminal/ANSI semantics; xterm.js renders, node-pty provides the TTY.

⚠️ This hands a full shell to anyone who can reach the port. LAN-only, no authentication. Never port-forward or tunnel it to the public internet. See Security & deployment.


Features

Core terminal

  • WebSocket terminal — xterm.js in the browser, node-pty on the host. Keystrokes shuttle to the shell; output streams back. No terminal parsing on the server.
  • Sessions survive disconnects — the PTY lifecycle is decoupled from the WebSocket. Closing a tab detaches a client; the shell keeps running. Reconnect replays a ~2 MB scrollback ring buffer.
  • Auto-reconnect — exponential backoff (1s/2s/4s… capped at 30s), carrying the localStorage session id, so a flaky network or a phone waking from sleep just resumes.
  • Mobile touch key-bar — one-tap Esc / Esc·Esc / ⇧Tab / arrows / Enter / ^C / ^O / ^T / ^B / ^R / ^L / ^D / Tab / / — the high-frequency Claude Code keys a soft keyboard can't produce, sent as raw bytes (no soft-keyboard pop).
  • Multi-device session mirroring — many devices can attach to the same session at once. Output, exit and status broadcast to all; any device can type (shared control). PTY sizing is latest-writer-wins: whichever device you're actively using drives the size and stays full-screen.

Tabs & home

  • Multi-tab — each tab is an independent shell. Tabs auto-name to the current folder (double-click to rename), show a connection dot and an unread-output dot, and can be drag-reordered. + opens a new tab in the active tab's directory.
  • Home Sessions chooser — opening the app lands on a chooser, not a blank shell. It shows the host's running sessions as live preview thumbnails (read-only renders of each screen) so you can see what each one is doing, plus a dashed + New session tile. Pick one to open (full scrollback replay) or start fresh.
  • Per-session Open / Kill — manage sessions right from the chooser; no separate manage page.
  • Sessions ↔ Projects toggle — a segmented control on the home screen flips between the running-sessions view and the projects view.
  • ⌂ Home overlay — a Home button in the tab bar overlays the chooser on top of the current terminal so you can start another session/project without closing your tabs.

Claude Code cockpit

  • Live per-tab status — Claude Code hooks POST to the server (loopback side-channel); each tab badge shows working / waiting-for-approval / idle / stuck in real time. Install once with npm run setup-hooks.
  • Remote approve / reject — when Claude asks for tool permission, the request is held server-side and an Approve / Reject bar appears on every attached device — resolve it with a tap, no typing. Works across multiple devices (closing one mirror doesn't cancel the prompt for the others).
  • Plan-mode / permission-mode relay — start a session in a chosen --permission-mode (default / acceptEdits / plan / auto). When Claude exits plan mode, the approval bar becomes a three-way gate (approve+auto / approve+review / keep planning). The high-risk auto mode is gated behind ALLOW_AUTO_MODE.
  • tmux keepalive — run the shell inside tmux so sessions survive a server/host restart, not just a disconnect. Auto-detected, or forced with USE_TMUX.
  • Session history / resume — browse past Claude Code sessions (from ~/.claude/projects) and resume one.

Projects (v0.6)

  • Auto-discovered git repos — scans configurable roots for .git, showing each repo's branch and dirty state. Read-only, cached, with a depth-bounded BFS that skips node_modules/dotdirs/symlinks.
  • Per-project launchers — each card has brand-logo buttons: Claude and Codex open a new tab running that CLI in the repo; VS Code asks the host to open the editor on that path. Projects with an active Claude session highlight the Claude button (with a count).
  • Project detail page — branch + a git worktrees list, the project's active sessions (open/kill), a CLAUDE.md viewer with a Generate / Update (/init) button, a read-only git diff viewer, and a create-worktree action.

Walk-away workbench (v0.7)

  • Mobile Web Push + lock-screen triage — the host actively notifies your phone on needs-input (high priority, with Allow / Deny action buttons) and done (low priority). Approve or deny a held tool request straight from the lock screen without opening the app, secured by a per-decision capability token. Optional ntfy / Pushover bridge for setups without HTTPS Web Push.
  • Voice dictation — push-to-talk mic on the input bar (Web Speech API); release to send the transcript as input (optional auto-Enter). Hidden where unsupported; audio never touches the server.
  • Quick-reply chips + saved-prompt palette — tap-to-send chips (yes, continue, 1/2/3, Esc) plus a persistent palette of your own named snippets.
  • Activity timeline — a human-readable, timestamped stream of what happened while you were gone ("ran Bash · edited 3 files · waiting for approval · done"), built from discrete hook events in a bounded per-session ring.
  • Stuck / idle alert — if a session goes silent past a threshold (no output, not idle, not exited) it fires a one-shot "possibly stuck" alert through the same notification channel. Runs on the existing reaper tick (no extra timer).
  • statusLine telemetry → per-tab gauges — a statusLine script feeds back context-usage, cumulative cost, model, lines +/, PR state, and rate-limit (5h/7d) telemetry, rendered as per-tab gauges on tabs, thumbnails and project cards. Stale telemetry greys out.

UX

  • Themed UI (Amber dark theme + light / solarized, adjustable font size), scrollback search, an all-sessions dashboard, share-session QR/link (?join=<id>), connect-device QR, a keyboard-shortcut cheat-sheet, an installable PWA, clickable links, and a line-icon toolbar with hover tooltips.

Clients

The browser is the reference client; two native clients and a remote-access service are also in the repo (all consume the same wire protocol — the server stays a byte-shuttle).

  • iOS / iPad app (ios/, branch feat/ios-client) — a native SwiftUI + SwiftTerm pure remote client built for the walk-away loop: native terminal with scrollback replay, QR/manual pairing (Keychain), the remote Approve / Reject gate + three-way plan gate, APNs push with lock-screen Allow / Deny behind Face ID, deep links, Projects, multi-session switcher, timeline, diff, quick-reply, and an adaptive iPhone/iPad split layout. Looks like the desktop (amber-gold, dark-first). P0 needs zero server changes; P1 adds two declared additive touch-points. See ios/README.md. (Not yet merged.)
  • Desktop app (desktop/) — a Mac/Windows Electron shell that embeds this Node server + node-pty (all-in-one), for native notifications, tray, deep links and launch-at-login. Design in docs/DESKTOP_PLAN.md. (Built; packaged & launch- verified on Apple Silicon.)
  • Rendezvous relay (term-relay/ · agent/ · relay-e2e/ · relay-web/ · relay-auth/ · relay-contracts/) — an "ngrok-for-Claude-Code" with end-to-end encryption to reach a self-hosted host from anywhere, without a VPN or port-forwarding. Security-audited but pre-production / not deployed; see docs/PLAN_RELAY_INDEX.md and docs/DEPLOY_RELAY.md. For remote access today, use Tailscale (§ Security & deployment below).

Quick start

Prerequisites

  • Node.js ≥ 18 (developed on v24).
  • macOS / Linux. On macOS, node-pty compiles a native addon — install Xcode Command Line Tools (xcode-select --install) if npm install fails. After a major Node version bump, run npm rebuild.

Install & run

npm install        # installs deps (incl. node-pty native build); postinstall fixes the spawn-helper bit
npm run build:web  # bundle the frontend (public/main.ts → public/build/) with esbuild
npm start          # serve on 0.0.0.0:3000

Then find your LAN IP and open it from any device on the same network:

ipconfig getifaddr en0          # macOS
# open http://<that-ip>:3000

For frontend development, run npm run dev:web (esbuild --watch) alongside npm start.

npm run setup-hooks                 # adds the hooks + statusLine to ~/.claude/settings.json (backs it up)
# npm run setup-hooks -- --remove   # to uninstall

This wires Claude Code's hooks → live per-tab status, the statusLine gauges, and push notifications. The hooks are a no-op outside web-terminal (they only fire when $WEBTERM_* env vars are set in spawned shells), so they're safe to leave installed. Then run claude inside a tab.

USE_TMUX=1 npm start keeps sessions alive across a server restart.

Tests

npm test           # vitest, all modules (~470 tests, 80% coverage gate)
npm run typecheck  # tsc (backend + frontend)
npm run build      # compile backend to dist/

Configuration

All config is via environment variables (no hardcoding). Invalid values fail fast at startup.

Core

Var Default Purpose
PORT 3000 Listen port.
BIND_HOST 0.0.0.0 Listen address.
SHELL_PATH $SHELL or /bin/zsh Shell to spawn.
IDLE_TTL 86400 (s) Reclaim a detached session after this idle time (no new output since detach).
SCROLLBACK_BYTES 2097152 (2 MB) Per-session replay ring buffer size.
MAX_PAYLOAD_BYTES 1048576 (1 MB) Max single WS frame; oversized frames are rejected.
WS_PATH /term The only path accepted for WS upgrade.
MAX_SESSIONS 50 Cap on concurrent sessions (DoS guard).
MAX_MSGS_PER_SEC 2000 Per-connection WS frame-rate cap; over-limit frames are dropped (not disconnected).
USE_TMUX auto 1/0/auto — run the shell inside tmux (keepalive across restart); auto = on if tmux is on PATH.
ALLOWED_ORIGINS (derived) Extra allowed WS origins, comma-separated. The base list is derived from the host's NIC IPs + localhost — never from BIND_HOST.
PERM_TIMEOUT_MS 300000 (5 min) How long a held tool-permission request waits for a remote decision before falling back to Claude's own prompt. Must be > 0.
REAP_INTERVAL_MS 60000 Idle-reaper / stuck-sweep tick interval.
PREVIEW_BYTES 24576 (24 KB) Tail of scrollback served for live preview thumbnails.

Projects (v0.6)

Var Default Purpose
PROJECT_ROOTS $HOME Comma-separated absolute roots to scan for git repos (~/~/... expanded; relative paths rejected).
PROJECT_SCAN_DEPTH 4 How deep to descend looking for .git.
PROJECT_SCAN_TTL 10000 (ms) Cache TTL for repo discovery.
PROJECT_DIRTY_CHECK 1 (on) Run git status --porcelain to flag dirty repos.
EDITOR_CMD code Command the host runs for the project "VS Code" button.

Walk-away workbench (v0.7)

Var Default Purpose
VAPID_PUBLIC_KEY (unset → push off) Web Push public key, exposed to the service worker.
VAPID_PRIVATE_KEY (unset, secret) Web Push private key; required to enable push. Never logged.
VAPID_SUBJECT mailto:admin@localhost VAPID sub.
PUSH_STORE_PATH ~/.web-terminal-push-subs.json Persisted push-subscription store.
PUSH_MAX_SUBS 50 Max stored push subscriptions (DoS guard).
NOTIFY_DONE 1 (on) Send the low-priority DONE push on Stop/SessionEnd.
NOTIFY_DND 0 (off) Global do-not-disturb default.
DECISION_TOKEN_TTL_MS = PERM_TIMEOUT_MS Lifetime of a lock-screen decision capability token.
TIMELINE_MAX 200 Per-session activity-timeline event ring cap.
TIMELINE_ENABLED 1 (on) Capture/serve the activity timeline.
STUCK_TTL 600 (s) Silence window before a "possibly stuck" alert; 0 disables.
STUCK_ALERT 1 (on) Master switch for stuck alerts.
DIFF_TIMEOUT_MS 2000 Timeout for a single git diff.
DIFF_MAX_BYTES 2097152 (2 MB) Patch truncation cap.
DIFF_MAX_FILES 300 Max files returned before the diff is marked truncated.
STATUSLINE_TTL_MS 30000 After this with no update, per-tab telemetry greys out as stale.
WORKTREE_ENABLED 1 (on) Master switch for the create-worktree feature (the only write-to-disk action).
WORKTREE_ROOT (unset → <repo>-worktrees) Root that new worktrees must land inside.
WORKTREE_TIMEOUT_MS 10000 Timeout for git worktree add.
DEFAULT_PERMISSION_MODE default --permission-mode used when a session is started without an explicit choice (default/acceptEdits/plan/auto).
ALLOW_AUTO_MODE 0 (off) Whether the high-risk auto (bypass-permissions) mode is offered/honored.

Spawn-injected env (set by the server / read by hook scripts — not user config)

WEBTERM_SESSION, WEBTERM_HOOK_URL, WEBTERM_STATUSLINE_URL are injected into each spawned shell so the hooks/statusLine know which session they belong to and where to POST. The optional ntfy/Pushover bridge reads WEBTERM_NTFY_URL / WEBTERM_NTFY_TOPIC / WEBTERM_NTFY_TOKEN and WEBTERM_PUSHOVER_TOKEN / WEBTERM_PUSHOVER_USER — secrets stay in env, never written into settings.json or command argv.


Security & deployment

This is a no-auth, LAN-only tool by design — it hands a full shell to anyone who can reach the port. The defenses that matter:

  • WS Origin validation (cannot be skipped): the WebSocket handshake rejects any Origin not on the allow-list (HTTP 401). This blocks Cross-Site WebSocket Hijacking — a malicious page in your browser trying to connect to ws://<your-lan-ip>:3000. Only WS_PATH is accepted for upgrade.
  • CSRF / Origin guards on state-changing routes: every route with a side effect (DELETE /live-sessions, POST /open-in-editor, POST /push/subscribe, POST /hook/decision, POST /projects/worktree) requires an allowed Origin (403 otherwise). Read-only discovery routes don't.
  • Loopback-only hook ingest: the side-channel ingest endpoints (/hook, /hook/permission, /hook/status) only accept loopback peers — the Claude process always runs on the host.
  • Per-IP rate limits on push subscribe and lock-screen decision routes; a per-connection WS frame-rate cap.
  • Safe git exec + per-decision capability tokens: all git calls use execFile (no shell) with timeouts and path validation; the lock-screen Allow/Deny is authorized by a token bound to that session's current pending request (it expires on resolve/timeout).
  • No authentication yet. Treat the whole app as "shell access for anyone on the network." Never expose it to the public internet. ws:// is unencrypted, so on an untrusted network keystrokes (passwords, API keys) can be sniffed. The recommended deployment is Tailscale (WireGuard-encrypted), which also gives you wss:// — the frontend auto-selects wss on HTTPS. Web Push requires HTTPS (a secure context), so the push features need the Tailscale/TLS deploy; bare LAN-over-HTTP falls back to the ntfy/Pushover bridge.

Architecture

The server is a byte-shuttle, not a terminal: node-pty gives the shell a real pseudo-terminal, the server shuttles raw bytes over the WebSocket, and xterm.js in the browser interprets ANSI and renders. The server never parses terminal semantics — every "smart" feature (status, timeline, telemetry, diff, push) rides an out-of-band side-channel (loopback HTTP/JSON from Claude Code hooks, or git subprocesses), keeping the terminal stream a pure pipe.

The other central design point: PTY lifecycle ≠ WebSocket lifecycle. A WS close detaches a client (the PTY keeps running for other devices and for reconnect); only an idle timeout (or explicit kill / server shutdown) ends a session.

Tested with vitest (~470 tests, 80% coverage gate across backend + the logic-bearing frontend modules), plus real-PTY integration tests that auto-skip where posix_spawn is unavailable (sandboxes) and run everywhere else.

Design and rationale: docs/TECH_DOC.md (the why) and docs/ARCHITECTURE.md (the how). Feature PRDs: docs/FEATURE_PROJECT_MANAGER.md (v0.6) and docs/FEATURE_WALKAWAY_WORKBENCH.md (v0.7).

Description
No description provided
Readme 12 MiB
Languages
TypeScript 61.2%
Swift 36.7%
CSS 1.1%
JavaScript 0.8%
HTML 0.1%
Other 0.1%