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

60 lines
3.2 KiB
Markdown

# Explore: Responsive Web Terminal
## Problem Statement
The current web terminal feels sluggish and fragile compared to a local terminal session. Key pain points:
1. **No reconnection** — A brief network hiccup kills the terminal. Users must navigate away and back.
2. **No heartbeat** — Half-open connections stall silently. No way to know if the terminal is alive.
3. **High input latency** — Every keystroke round-trips to the server before appearing on screen. No local echo.
4. **Inefficient I/O path** — Backend `select` polling with 0.1s timeout, 4096-byte reads, busy-wait sleep(0.01). Frontend receives Blob and converts to ArrayBuffer asynchronously.
5. **No scrollback persistence** — Reconnect starts with a blank terminal. Session history is lost.
6. **Rudimentary resize** — Fires on every window resize event with no debouncing.
7. **No connection quality feedback** — Binary status (connected/disconnected). No latency or health indicator.
8. **No graceful container exit handling** — Process death closes WebSocket with a generic error.
## Current Architecture
### Frontend
- `apps/web/src/components/terminal.tsx` — xterm.js v5.3.0 with FitAddon and WebLinksAddon
- WebSocket to `/ws/tool-instances/{instance_id}/terminal`
- Receives Blob (binary) and string (JSON control) messages
- Sends raw bytes for input, JSON for resize
- Basic status: connecting | connected | disconnected | error
### Backend
- `apps/api/src/api/terminal.py` — FastAPI WebSocket endpoint, auth, session lifecycle
- `apps/api/src/services/terminal_manager.py` — Manages TerminalSession, read/write loops
- `apps/api/src/services/terminal_session.py` — PTY-based `docker exec` with `select` I/O
- Protocol: raw bytes for terminal I/O, JSON for resize control messages
### Gaps vs. Local Terminal Feel
| Aspect | Local Terminal | Current Web Terminal |
|--------|---------------|----------------------|
| Keystroke feedback | Immediate (kernel TTY) | Round-trip (~50-200ms) |
| Network resilience | N/A (local) | Dies on any disconnect |
| Scrollback | Persistent | Lost on reconnect |
| Resize | Instant | Undebounced, may spam |
| Health visibility | Always local | Binary connected/disconnected |
| Large output | Buffered by kernel | Select polling, 4KB chunks |
## Opportunities
- **WebSocket reconnection with exponential backoff** and session token for continuity
- **Heartbeat/ping-pong** to detect half-open connections within seconds
- **Local echo optimization** for printable characters (with server-side authoritative sync)
- **Message batching** on backend to reduce WebSocket frame overhead
- **Scrollback serialization** via xterm-addon-serialize to restore on reconnect
- **Resize debouncing** to avoid flooding the server
- **Connection quality indicator** (latency, jitter) in the terminal chrome
- **Graceful handling** of container exit with clear user messaging
## Risks
- Adding heartbeat may increase server load with many concurrent terminals
- Local echo requires careful handling of password prompts and special modes
- Reconnecting to a docker exec PTY is not natively resumable — new `docker exec` on reconnect
- xterm-addon-serialize may be large for very long sessions
- Changes touch both frontend and backend — cross-stack coordination needed