Files
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

214 lines
9.0 KiB
Markdown

# 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.