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/
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 serializationxterm-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
- Should we add a "full screen" button to the terminal chrome? (Nice-to-have, out of scope for this change)
- Should scrollback be persisted across full page reloads (via
localStorage) or only during session (sessionStorage)? — Decision: UsesessionStorageto avoid leaking sensitive data. - 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_stateto client.