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/
214 lines
9.0 KiB
Markdown
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.
|