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

7.0 KiB

Spec: Responsive Web Terminal

Overview

Upgrade the web terminal from a fragile single-shot WebSocket into a resilient, responsive terminal that survives network blips, provides instant typing feedback, restores scrollback on reconnect, and gives users clear visibility into connection health.

Acceptance Criteria

AC-1: WebSocket Auto-Reconnection

GIVEN a terminal is connected to a running instance
WHEN the WebSocket disconnects (network hiccup, server restart, proxy timeout)
THEN the client automatically reconnects with exponential backoff (1s, 2s, 4s, 8s, max 30s)
AND the user sees a reconnection indicator showing attempt count and next retry time
AND after successful reconnection, the terminal scrollback is restored
AND a new docker exec session is spawned transparently

Test: Disconnect WiFi for 5s, verify reconnect and scrollback intact.

AC-2: Heartbeat / Ping-Pong Protocol

GIVEN a terminal connection is established
WHEN 15 seconds pass with no data exchanged
THEN the client sends a ping control message
AND the server responds with a pong within 5 seconds
AND if no pong is received within 5 seconds, the client treats the connection as dead and begins reconnection
AND the server closes WebSockets that have not sent any message (including ping) for 60 seconds

Test: Block server responses with firewall rule, verify connection declared dead within 20s and reconnection starts.

AC-3: Local Echo for Reduced Typing Latency

GIVEN the terminal is in a normal interactive shell
WHEN the user types printable ASCII characters
THEN they appear on screen immediately (local echo) without waiting for the server round-trip
AND when the server sends the authoritative echo back, the client reconciles (deduplicates)
AND when the server sends a set_echo_state control message with enabled: false (e.g., for password prompts), local echo is disabled
AND when set_echo_state with enabled: true is received, local echo is re-enabled

Test: Type echo hello — characters appear instantly. Run sudo — local echo stops during password prompt.

AC-4: Resize Debouncing

GIVEN the user is resizing the browser window
WHEN the terminal dimensions change
THEN resize events are debounced by 200ms
AND only the final dimensions after the user stops resizing are sent to the server
AND at most one resize message is sent per 500ms

Test: Rapidly resize window 10 times in 1s — verify only 1-2 resize messages sent.

AC-5: Scrollback Serialization and Restoration

GIVEN a terminal has been in use with output history
WHEN a disconnect occurs
THEN the client serializes the terminal buffer (via xterm-addon-serialize, capped at 10,000 lines)
AND stores it in sessionStorage under key hq-terminal-{instance_id}
AND on successful reconnection, the serialized content is written back into the terminal before new output
AND a visual divider line indicates "--- Reconnected ---" between old and new output

Test: Run ls -la 50 times, disconnect, reconnect — verify all output visible with divider.

AC-6: Connection Quality Indicator

GIVEN the terminal is connected
THEN the status bar shows:

  • Green dot + "Connected" when healthy (latency < 100ms)
  • Yellow dot + "Slow" when latency is 100-500ms
  • Red dot + "Reconnecting (N)" during reconnection attempts
  • Gray dot + "Disconnected" when permanently disconnected (max retries exceeded)
    AND hovering the status dot shows a tooltip with round-trip latency (ms) and jitter
    AND the indicator updates every 5 seconds

Test: Use network throttling in dev tools to simulate slow connection, verify indicator changes.

AC-7: Graceful Container Exit

GIVEN a terminal session is active
WHEN the container process exits (shell terminates, container stops)
THEN the terminal shows a clear message: "Session ended. The container process has exited."
AND a "Reconnect" button is shown to spawn a new session
AND a "Go Back" button navigates to the previous page
AND the WebSocket closes with code 1000 (normal) instead of an error code

Test: Run exit in the terminal, verify friendly message and buttons appear.

AC-8: Backend Message Batching

GIVEN a container process is producing output rapidly
WHEN the backend PTY produces multiple small reads within a single event loop tick
THEN the backend batches them into a single WebSocket binary frame
AND batching does not add more than 16ms of latency
AND the batch is flushed immediately when no new data is available

Test: Run yes | head -n 10000 and measure WebSocket frame count vs. current implementation.

AC-9: Keyboard Shortcut for Reconnect

GIVEN the terminal is disconnected
WHEN the user presses Ctrl+Shift+R
THEN an immediate reconnection attempt is triggered (bypassing backoff)

Test: Disconnect terminal, press Ctrl+Shift+R, verify immediate reconnect attempt.

API / Protocol Changes

WebSocket Control Messages (JSON)

// Client → Server
type ClientMessage =
  | { type: "ping"; id: number }
  | { type: "pong"; id: number }
  | { type: "resize"; cols: number; rows: number }
  | { type: "input"; data: string }  // base64-encoded bytes

// Server → Client
type ServerMessage =
  | { type: "pong"; id: number }
  | { type: "status"; status: "connected" | "reconnected" }
  | { type: "set_echo_state"; enabled: boolean }
  | { type: "session_ended"; reason: "process_exit" | "container_stop" | "timeout" }

Binary Frames

  • Raw terminal output from server → client: binary WebSocket frame (no wrapping)
  • Raw terminal input from client → server: binary WebSocket frame (no wrapping)
  • Control messages (resize, ping, etc.): text JSON frames

Dependencies

Frontend

  • xterm-addon-serialize — scrollback serialization
  • xterm-addon-webgl (optional) — GPU rendering for smoother feel

Backend

  • No new Python dependencies required
  • Uses existing asyncio, fastapi, websockets

Non-Functional Requirements

  • Latency: Perceived typing latency < 50ms for local echo characters
  • Reconnection time: < 5 seconds for transient disconnects
  • Memory: Scrollback serialization capped at 10,000 lines (~2-5MB worst case)
  • Server load: Heartbeat interval 15s; max 4 pings/minute per terminal
  • Browser support: Chrome 90+, Firefox 88+, Safari 14+ (all support required WebSocket features)

Open Questions

  1. Should we add a "full screen" button to the terminal chrome? (Nice-to-have, out of scope for this change)
  2. Should scrollback be persisted across full page reloads (via localStorage) or only during session (sessionStorage)? — Decision: Use sessionStorage to avoid leaking sensitive data.
  3. Should the server echo-state detection be automatic (TIOCGWINSZ / stty inspection) or manual (client tells server)? — Decision: Server detects via PTY state inspection; sends set_echo_state to client.