Skip to main content

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:

  1. Running build commands (cargo build, npm install)
  2. Executing Claude Code CLI (claude --print --message)
  3. Running tests (pytest, cargo test)
  4. Git operations (git commit, git push)
  5. 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)

  1. Session creation: fork() + openpty() + exec(shell) per terminal tab
  2. I/O bridge: tokio::io::AsyncRead on PTY master fd -> WebSocket binary frame
  3. Resize: ioctl(TIOCSWINSZ) when browser window resizes
  4. Lifecycle: Track child PID, detect exit via waitpid(), cleanup
  5. Limit: Max 10 concurrent terminal sessions per sidecar instance

Client-Side: xterm.js

  1. Rendering: xterm.js v5 with WebGL renderer for performance
  2. Addons: xterm-addon-fit (auto-resize), xterm-addon-web-links (clickable URLs)
  3. Font: JetBrains Mono / Fira Code monospace
  4. Connection: Dedicated WebSocket per terminal (/ws/terminal?session_id=X)

WebSocket Frame Protocol

DirectionFrame TypeContent
Client -> ServerBinaryRaw terminal input (keystrokes)
Client -> ServerTextJSON control: {"type":"resize","cols":120,"rows":40}
Server -> ClientBinaryRaw terminal output (ANSI escape sequences)
Server -> ClientTextJSON 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

MetricTarget
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 sessions10
  • 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-app uses @theia/terminal (xterm.js)