The detail view could kill sessions, but the grid card's session rows were enter-only. Add a ✕ kill button to each card session row (stopPropagation so it doesn't also enter the session); makeSessionRow gains an optional onKill, makeProjectCard an optional onKillSession, and mountProjects wires it to DELETE /live-sessions/:id followed by a refresh. Frontend-only. projects-panel tests +1 (51): ✕ present only with the callback, click kills the right id without entering. Verified in-browser: a card with 2 sessions dropped to 1 after clicking ✕.
Web Terminal
A browser-based terminal that exposes the host machine's local shell over WebSocket. Open http://<host-ip>:3000 from any device on your LAN (phone, tablet, another computer) and you get a live, interactive terminal — primarily for vibe coding: hand Claude Code a task, walk away, reconnect from anywhere to check on it.
Sessions survive disconnects: the shell (and whatever's running in it) keeps going when you close the tab; reconnect and the output replays.
⚠️ 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.
Features
- Multi-tab — each tab is an independent shell session. 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. - Claude Code cockpit (with hooks, see below) — each tab shows Claude's status (⚙ working / ⏳ needs approval / ✓ idle), sends a browser notification when it needs you, and lets you tap Approve / Reject on a tool request with no typing.
- tmux keepalive (optional) — run the shell inside tmux so sessions survive a server/host restart, not just a disconnect.
- Mobile + desktop shortcut bar — one-tap Esc / Esc·Esc / ⇧Tab / arrows / Enter / ^C / ^O / ^T / ^B / Tab /
/(the Claude Code keys a phone keyboard can't produce). - Session keepalive + replay — the PTY keeps running across disconnects; reconnect replays a ~2 MB scrollback ring buffer.
- Toolbar — 🔍 scrollback search · ⚙ themes & font size · ▦ all-sessions dashboard · 📱 QR connect (scan to open on another device). Clickable links. Installable as a PWA.
Requirements
- Node.js ≥ 18 (developed on v24)
- macOS/Linux. On macOS,
node-ptycompiles a native addon — install Xcode Command Line Tools (xcode-select --install) ifnpm installfails.
Install
npm install # installs deps; postinstall makes node-pty's spawn-helper executable
Run
npm run build:web # bundle the frontend (public/main.ts → public/build/main.js)
npm start # serve on 0.0.0.0:3000
Then:
# find your LAN IP (macOS):
ipconfig getifaddr en0
# open http://<that-ip>:3000 on any device on the same network
For frontend development, run npm run dev:web (esbuild --watch) alongside npm start.
Claude Code cockpit (optional)
To see Claude's status per tab and approve/reject tool calls from your phone, install the hooks once:
npm run setup-hooks # adds http hooks to ~/.claude/settings.json (backs it up)
# npm run setup-hooks -- --remove # to uninstall
Then run claude inside a tab. The hooks POST to the server (loopback only) when Claude starts/stops/needs permission; the tab badge updates live, and a PermissionRequest shows an Approve / Reject bar. The hooks are a no-op outside web-terminal (they curl $WEBTERM_HOOK_URL, which is only set in spawned shells), so they're safe to leave installed.
tmux keepalive — run with USE_TMUX=1 (or just have tmux on PATH; it auto-detects) to keep sessions alive across a server restart:
USE_TMUX=1 npm start
Configuration
All via environment variables (no hardcoding):
| Var | Default | Meaning |
|---|---|---|
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 |
SCROLLBACK_BYTES |
2097152 |
per-session replay ring buffer (bytes) |
MAX_PAYLOAD_BYTES |
1048576 |
max single WS frame |
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 |
allowedOrigins is derived from the host's network-interface IPs (plus localhost and anything in ALLOWED_ORIGINS) — never from BIND_HOST, since 0.0.0.0 is never a real browser Origin.
Security
This is a no-auth, LAN-only tool by design. The defenses that matter:
- Origin check (cannot be skipped): the WS handshake rejects any
Originnot on the allow-list (HTTP 401). This blocks Cross-Site WebSocket Hijacking — a malicious page in your browser trying to connect tows://<your-lan-ip>:3000. - Path-scoped upgrades: only
/termis accepted for WS upgrade. - Frame size cap: oversized frames are rejected (
MAX_PAYLOAD_BYTES). - Never expose to the public internet.
ws://is unencrypted — on an untrusted network (café/office Wi-Fi) traffic (including keystrokes: passwords, API keys) can be sniffed. The recommended deployment is Tailscale (WireGuard-encrypted), which also lets you usewss://(the frontend auto-selectswsson HTTPS).
Development
npm test # vitest, all modules
npm run typecheck # tsc (backend + frontend)
npm run build # compile backend to dist/
Real-PTY end-to-end tests (test/integration/) auto-skip where posix_spawn is unavailable (e.g. sandboxes) and run everywhere else.
How it works
The server is a byte-shuttle, not a terminal: node-pty provides a pseudo-terminal so the shell believes it has a real TTY, and xterm.js in the browser interprets the ANSI bytes and renders. The server never parses terminal semantics. PTY lifecycle is decoupled from the WebSocket — a disconnect detaches (the PTY keeps running) rather than killing it, which is what makes session survival work.
Design and rationale: docs/TECH_DOC.md (why) and docs/ARCHITECTURE.md (how).