Files
headquarter/openspec/changes/responsive-terminal/spec.md
T
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

154 lines
7.0 KiB
Markdown

# 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)
```typescript
// 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.