6c8cfe9157
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/
154 lines
7.0 KiB
Markdown
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.
|