Files
alex 6c8cfe9157 feat: responsive web terminal with auto-reconnect, heartbeat, and local echo
Implements a resilient, responsive web terminal that survives network blips,
provides instant typing feedback, and restores scrollback on reconnect.

Backend changes:
- Add heartbeat tracking (15s ping interval, 60s idle timeout)
- Add message batching (16ms flush window) for efficient I/O
- Add termios echo detection and set_echo_state control messages
- Add graceful session_ended notification before close
- Add ping/pong protocol support

Frontend changes:
- Rewrite TerminalComponent with status bar, connection indicator,
  session-ended overlay, reconnect banner, and ResizeObserver
- Add useTerminalConnection hook with:
  - Exponential backoff auto-reconnect (1s → 30s max, 10 attempts)
  - Heartbeat/ping-pong with latency tracking
  - Local echo for printable ASCII with server deduplication
  - Resize debounce (200ms) + throttle (500ms)
  - Scrollback serialization via xterm-addon-serialize
  - Ctrl+Shift+R manual reconnect shortcut
- Add WebSocket protocol types and encoding utilities
- Add xterm-addon-serialize dependency

Tests:
- 16 backend unit tests (TerminalSession + TerminalManager)
- 13 frontend hook tests (connection lifecycle, reconnect, resize,
  scrollback, callbacks)

Quality gates:
- Frontend typecheck: clean
- Frontend lint: clean
- Frontend tests: 48 passed
- Backend unit tests: 101 passed
- Backend ruff: clean

SDD artifacts: openspec/changes/responsive-terminal/
2026-05-27 21:27:49 +02:00

3.2 KiB

Explore: Responsive Web Terminal

Problem Statement

The current web terminal feels sluggish and fragile compared to a local terminal session. Key pain points:

  1. No reconnection — A brief network hiccup kills the terminal. Users must navigate away and back.
  2. No heartbeat — Half-open connections stall silently. No way to know if the terminal is alive.
  3. High input latency — Every keystroke round-trips to the server before appearing on screen. No local echo.
  4. Inefficient I/O path — Backend select polling with 0.1s timeout, 4096-byte reads, busy-wait sleep(0.01). Frontend receives Blob and converts to ArrayBuffer asynchronously.
  5. No scrollback persistence — Reconnect starts with a blank terminal. Session history is lost.
  6. Rudimentary resize — Fires on every window resize event with no debouncing.
  7. No connection quality feedback — Binary status (connected/disconnected). No latency or health indicator.
  8. No graceful container exit handling — Process death closes WebSocket with a generic error.

Current Architecture

Frontend

  • apps/web/src/components/terminal.tsx — xterm.js v5.3.0 with FitAddon and WebLinksAddon
  • WebSocket to /ws/tool-instances/{instance_id}/terminal
  • Receives Blob (binary) and string (JSON control) messages
  • Sends raw bytes for input, JSON for resize
  • Basic status: connecting | connected | disconnected | error

Backend

  • apps/api/src/api/terminal.py — FastAPI WebSocket endpoint, auth, session lifecycle
  • apps/api/src/services/terminal_manager.py — Manages TerminalSession, read/write loops
  • apps/api/src/services/terminal_session.py — PTY-based docker exec with select I/O
  • Protocol: raw bytes for terminal I/O, JSON for resize control messages

Gaps vs. Local Terminal Feel

Aspect Local Terminal Current Web Terminal
Keystroke feedback Immediate (kernel TTY) Round-trip (~50-200ms)
Network resilience N/A (local) Dies on any disconnect
Scrollback Persistent Lost on reconnect
Resize Instant Undebounced, may spam
Health visibility Always local Binary connected/disconnected
Large output Buffered by kernel Select polling, 4KB chunks

Opportunities

  • WebSocket reconnection with exponential backoff and session token for continuity
  • Heartbeat/ping-pong to detect half-open connections within seconds
  • Local echo optimization for printable characters (with server-side authoritative sync)
  • Message batching on backend to reduce WebSocket frame overhead
  • Scrollback serialization via xterm-addon-serialize to restore on reconnect
  • Resize debouncing to avoid flooding the server
  • Connection quality indicator (latency, jitter) in the terminal chrome
  • Graceful handling of container exit with clear user messaging

Risks

  • Adding heartbeat may increase server load with many concurrent terminals
  • Local echo requires careful handling of password prompts and special modes
  • Reconnecting to a docker exec PTY is not natively resumable — new docker exec on reconnect
  • xterm-addon-serialize may be large for very long sessions
  • Changes touch both frontend and backend — cross-stack coordination needed