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

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:

  1. PR-1 (Backend foundation): T1 protocol types + T2 heartbeat/batching + T3 echo/exit + T7 backend tests (~590 lines)
  2. PR-2 (Frontend connection): T4 useTerminalConnection hook + T6 frontend hook tests (~460 lines)
  3. 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 (add xterm-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
  • encodeControlMessage and decodeControlMessage functions handle JSON serialization
  • isControlMessage helper correctly identifies text vs binary frames
  • npm install completes without lockfile conflicts

Depends on: None Estimated: 2 hours


Task T2: Backend Heartbeat and Message Batching

Files:

  • apps/api/src/services/terminal_manager.py
  • apps/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 10000 produces fewer WebSocket frames than before
  • Server responds to ping with matching pong within 100ms
  • Server closes idle connections after 60s of no client messages
  • Backend accepts both binary and text WebSocket frames for input
  • make test passes (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.py
  • apps/api/src/services/terminal_manager.py
  • apps/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 -echo in terminal triggers set_echo_state: false message
  • Running stty echo triggers set_echo_state: true message
  • Running exit in shell sends session_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+R triggers immediate reconnect when disconnected
  • Scrollback serialized to sessionStorage on 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.