ADR-169: Browser Terminal Emulation via PTY-over-WebSocket
Status: Proposed Date: 2026-02-09 Author: Claude (Opus 4.6) Deciders: Hal Casteel, Engineering Team Tags: terminal, pty, websocket, xterm, browser-ide
Context
The CODITECT Browser IDE must provide a full terminal experience in the browser for:
- Running build commands (
cargo build,npm install) - Executing Claude Code CLI (
claude --print --message) - Running tests (
pytest,cargo test) - Git operations (
git commit,git push) - General shell access (interactive
zsh/bash)
The existing sidecar has a TerminalCommand tool type that executes commands, but it's non-interactive (fire-and-forget). We need interactive PTY sessions with full terminal emulation.
Decision
Implement PTY-over-WebSocket bridge in the sidecar, with xterm.js rendering in the browser.
Architecture
Browser: xterm.js <-- WebSocket --> Sidecar: PTY Manager <-- PTY fd --> Shell (zsh/bash)
Server-Side: PTY Manager (Rust)
- Session creation:
fork()+openpty()+exec(shell)per terminal tab - I/O bridge:
tokio::io::AsyncReadon PTY master fd -> WebSocket binary frame - Resize:
ioctl(TIOCSWINSZ)when browser window resizes - Lifecycle: Track child PID, detect exit via
waitpid(), cleanup - Limit: Max 10 concurrent terminal sessions per sidecar instance
Client-Side: xterm.js
- Rendering: xterm.js v5 with WebGL renderer for performance
- Addons:
xterm-addon-fit(auto-resize),xterm-addon-web-links(clickable URLs) - Font: JetBrains Mono / Fira Code monospace
- Connection: Dedicated WebSocket per terminal (
/ws/terminal?session_id=X)
WebSocket Frame Protocol
| Direction | Frame Type | Content |
|---|---|---|
| Client -> Server | Binary | Raw terminal input (keystrokes) |
| Client -> Server | Text | JSON control: {"type":"resize","cols":120,"rows":40} |
| Server -> Client | Binary | Raw terminal output (ANSI escape sequences) |
| Server -> Client | Text | JSON control: {"type":"exit","code":0} |
Agent-Terminal Integration
The sidecar's agentic tool system can invoke terminal commands through the same PTY infrastructure:
Agent decides: ToolType::TerminalCommand("cargo test")
-> PTY Manager creates ephemeral session
-> Captures stdout/stderr
-> Returns ActionObservation to agent loop
-> Optionally streams output to browser terminal tab
Alternatives Considered
Alternative 1: ttyd / gotty
Use existing terminal-over-web tools (ttyd, gotty).
Rejected because:
- Separate process to manage (not integrated with sidecar)
- No integration with agent tool system
- Additional security surface (separate auth)
- Go/C dependency complicates Rust build
Alternative 2: WebContainer (e.g., StackBlitz)
Run a full Node.js environment in the browser via WebContainer API.
Rejected because:
- Only supports Node.js, not native compilation
- Cannot run
cargo,python, native binaries - 100MB+ download for WebContainer runtime
- Proprietary technology
Alternative 3: WASM Shell (wasmer shell)
Run a shell entirely in WASM.
Rejected because:
- WASM cannot fork processes
- No access to native filesystem
- Cannot run existing dev tools (git, cargo, npm)
- Extremely limited shell functionality
Alternative 4: SSH Tunnel
SSH from browser to localhost via a WebSocket-to-SSH proxy.
Rejected because:
- Requires SSH server running on developer machine
- Authentication complexity (keys, passwords)
- Overhead of SSH protocol for localhost communication
- Over-engineered for same-machine PTY access
Consequences
Positive
- Full interactive shell in browser (zsh/bash with all plugins/aliases)
- Native performance: PTY is kernel-level, zero emulation overhead
- Agent tool integration: same PTY infrastructure for automated and manual terminal use
- xterm.js is battle-tested (VSCode, Theia, JupyterLab all use it)
- True 256-color, Unicode, mouse support via ANSI escape passthrough
Negative
- Unix-only: PTY (
openpty,fork,ioctl) requires POSIX - Windows would need ConPTY API (separate implementation)
- Each terminal session holds a PTY fd and child process (resource overhead)
- WebSocket per terminal tab adds connection count
- Raw binary frames are not debuggable without tooling
Security Considerations
- PTY runs as user process with user permissions (same as regular terminal)
- WebSocket auth required before PTY creation
- Session tokens scoped per-terminal to prevent hijacking
- No privilege escalation: sidecar runs as same user
- Terminal input is not logged by default (could contain passwords)
Performance Targets
| Metric | Target |
|---|---|
| Keystroke-to-display latency | < 50ms |
Throughput (e.g., cat large_file) | > 10MB/s |
| Terminal startup time | < 200ms |
| WebSocket frame overhead | < 10 bytes per frame |
| Max concurrent sessions | 10 |
Related
- ADR-165: WASM Split Architecture
- ADR-166: WebSocket Protocol
- TDD:
docs/architecture/browser-ide/TDD-CODITECT-BROWSER-IDE.md(Section 5) - Sidecar tool:
sidecar/src/agentic/tool/terminal/terminal.rs - Existing:
coditect-cloud-ide/theia-appuses @theia/terminal (xterm.js)