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/
9.0 KiB
Tasks: Responsive Web Terminal
Review Workload Forecast
| Task | Estimated Lines | Stack | Risk |
|---|---|---|---|
| T1: Protocol types + utilities | ~120 | Frontend | Low |
| T2: Backend heartbeat + batching | ~200 | Backend | Medium |
| T3: Backend echo detection + graceful exit | ~150 | Backend | Medium |
| T4: useTerminalConnection hook | ~280 | Frontend | High |
| T5: TerminalComponent rewrite | ~250 | Frontend | High |
| T6: Frontend tests | ~180 | Frontend | Low |
| T7: Backend tests | ~120 | Backend | Low |
| Total | ~1,300 |
Review recommendation: This exceeds the 400-line budget. Split into 3 chained PRs:
- PR-1 (Backend foundation): T1 protocol types + T2 heartbeat/batching + T3 echo/exit + T7 backend tests (~590 lines)
- PR-2 (Frontend connection): T4 useTerminalConnection hook + T6 frontend hook tests (~460 lines)
- PR-3 (Terminal UI + integration): T5 TerminalComponent rewrite + page integration + remaining tests (~250 lines)
Task T1: Protocol Types and Utilities
Files:
apps/web/src/types/terminal.ts(new)apps/web/src/utils/terminal-protocol.ts(new)apps/web/package.json(addxterm-addon-serialize)
Description:
Define TypeScript types for all WebSocket control messages. Implement encode/decode helpers that distinguish binary frames (raw terminal I/O) from JSON text frames (control messages). Add base64 encoding for the input control message type. Install xterm-addon-serialize dependency.
Acceptance:
- All message types from the design spec are represented as TypeScript types
encodeControlMessageanddecodeControlMessagefunctions handle JSON serializationisControlMessagehelper correctly identifies text vs binary framesnpm installcompletes without lockfile conflicts
Depends on: None Estimated: 2 hours
Task T2: Backend Heartbeat and Message Batching
Files:
apps/api/src/services/terminal_manager.pyapps/api/src/api/terminal.py
Description:
Rewrite TerminalManager read loop to batch small reads into single WebSocket frames (max 16ms buffering). Add heartbeat tracking: server records last_client_message_at timestamp, and a background task closes WebSockets idle for 60s. Update terminal.py endpoint to accept ping control messages and respond with pong. Handle binary input frames (not just text JSON).
Acceptance:
- Backend sends batched binary frames;
yes | head -n 10000produces fewer WebSocket frames than before - Server responds to
pingwith matchingpongwithin 100ms - Server closes idle connections after 60s of no client messages
- Backend accepts both binary and text WebSocket frames for input
make testpasses (existing backend tests still green)
Depends on: None Estimated: 3 hours
Task T3: Backend Echo Detection and Graceful Exit
Files:
apps/api/src/services/terminal_session.pyapps/api/src/services/terminal_manager.pyapps/api/src/api/terminal.py
Description:
Add termios PTY inspection to detect ECHO flag state changes. Send set_echo_state control messages to client when echo toggles. Detect container process exit (returncode set) and send session_ended JSON message before closing WebSocket with code 1000. Distinguish between normal process exit, container stop, and unexpected errors.
Acceptance:
- Running
stty -echoin terminal triggersset_echo_state: falsemessage - Running
stty echotriggersset_echo_state: truemessage - Running
exitin shell sendssession_ended: { reason: "process_exit" }then closes with code 1000 - Stopping container sends
session_ended: { reason: "container_stop" } - Unexpected errors still close with code 4000 and error message
Depends on: T2 Estimated: 2.5 hours
Task T4: useTerminalConnection Hook
Files:
apps/web/src/hooks/use-terminal-connection.ts(new)
Description: Implement the core connection hook with: WebSocket lifecycle (open/close/reconnect with exponential backoff), heartbeat (send ping every 15s, timeout after 5s), local echo (write printable ASCII to xterm immediately, deduplicate server echo), resize debouncing (200ms, max 1/500ms), scrollback serialization on disconnect, scrollback restoration on reconnect, connection quality tracking (latency, jitter), manual reconnect bypass.
Acceptance:
- Hook exposes
state,sendInput,sendResize,reconnect,onData,onControl - Reconnect backoff: 1s, 2s, 4s, 8s, then max 30s
- Max 10 reconnection attempts before giving up
- Local echo works for printable ASCII; disabled when echo state is false
- Pending echo buffer deduplicates server echo correctly
- Pending echo buffer flushes to terminal if it grows > 100 chars
- Resize sends at most 1 message per 500ms
Ctrl+Shift+Rtriggers immediate reconnect when disconnected- Scrollback serialized to
sessionStorageon disconnect, restored on reconnect with divider
Depends on: T1 Estimated: 4 hours
Task T5: TerminalComponent Rewrite
Files:
apps/web/src/components/terminal.tsx(rewrite)apps/web/src/pages/terminal.tsx(minor)apps/web/src/styles.css(add terminal status styles)
Description:
Rewrite TerminalComponent to use useTerminalConnection. Integrate xterm.js with the hook's onData and onControl callbacks. Add status bar with connection quality indicator (green/yellow/red/gray dot, latency tooltip, attempt counter). Add reconnect overlay when disconnected. Wire xterm onData to hook's sendInput. Use ResizeObserver for container-level resize detection. Apply xterm-addon-serialize for scrollback. Update page to pass instance ID and handle close.
Acceptance:
- Terminal renders and connects on mount
- Status bar shows correct dot color based on connection state
- Hovering dot shows latency tooltip
- Reconnect overlay appears when max retries exceeded
- ResizeObserver triggers fit + resize message (debounced)
- Theme colors adapt to dark/light mode
- Close button works
Depends on: T4 Estimated: 3 hours
Task T6: Frontend Tests
Files:
apps/web/src/utils/terminal-protocol.test.ts(new)apps/web/src/hooks/use-terminal-connection.test.ts(new)
Description: Write Vitest tests for protocol utilities (encode/decode all message types, base64 round-trip, frame type detection). Write tests for the connection hook using a mock WebSocket server (or manual mock). Test: reconnect backoff timing, heartbeat timeout detection, local echo deduplication, resize throttling, scrollback serialization round-trip.
Acceptance:
- Protocol tests cover all message types and edge cases
- Hook tests cover connection lifecycle without real WebSocket
- All tests pass:
cd apps/web && npm test - Coverage for new code > 80%
Depends on: T1, T4 Estimated: 3 hours
Task T7: Backend Tests
Files:
apps/api/tests/unit/test_terminal_session.py(new)apps/api/tests/unit/test_terminal_manager.py(new)
Description:
Write pytest unit tests for TerminalSession (PTY creation, resize, echo detection, process exit detection). Write tests for TerminalManager (session creation, batching logic, heartbeat tracking). Use mocks for os, pty, termios, and asyncio where appropriate.
Acceptance:
- TerminalSession tests: start, resize, write, read, echo detection, close
- TerminalManager tests: create session, read loop batching, heartbeat timeout
- All tests pass:
make test
Depends on: T2, T3 Estimated: 2.5 hours
Task Order and Dependencies
T1 ──► T4 ──► T5 ──► PR-3 (Frontend UI)
│
└──► T6 (Frontend tests)
T2 ──► T3 ──► PR-1 (Backend foundation)
│
└──► T7 (Backend tests)
Parallel work possible:
- T1 and T2 can be done in parallel (no dependencies)
- T3 and T4 can be done in parallel (T3 depends on T2, T4 depends on T1)
- T5 depends on T4
- T6 depends on T4
- T7 depends on T3
Chained PR Plan
PR-1: Backend Foundation
Scope: T1 (protocol types only) + T2 + T3 + T7
Files touched: apps/api/src/services/terminal_manager.py, apps/api/src/services/terminal_session.py, apps/api/src/api/terminal.py, new test files, apps/web/src/types/terminal.ts, apps/web/src/utils/terminal-protocol.ts
Estimated diff: ~590 lines
Review focus: Protocol correctness, heartbeat logic, batching efficiency
PR-2: Frontend Connection Hook
Scope: T4 + T6
Files touched: apps/web/src/hooks/use-terminal-connection.ts, new test files
Estimated diff: ~460 lines
Review focus: State machine correctness, local echo algorithm, reconnection logic
PR-3: Terminal UI Integration
Scope: T5
Files touched: apps/web/src/components/terminal.tsx, apps/web/src/pages/terminal.tsx, apps/web/src/styles.css
Estimated diff: ~250 lines
Review focus: UX, accessibility, visual polish, integration with hook
Note: PR-2 and PR-3 can be developed in parallel if PR-1's protocol types are stable. The hook can be tested against mock protocol types before the backend is merged.