# Design: High-Performance Web Terminal

## Component Diagram

```
┌─────────────────────────────────────────────────────────────┐
│                        Browser                              │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────────────┐ │
│  │ Terminal    │  │ WebSocket   │  │ xterm.js            │ │
│  │ Component   │──│ Client      │──│ + WebGL addon       │ │
│  │             │  │ (binary)    │  │ + DOM fallback      │ │
│  └─────────────┘  └─────────────┘  └─────────────────────┘ │
│         │                                              │     │
│         │ Flow control ack                             │     │
│         └──────────────────────────────────────────────┘     │
└─────────────────────────────────────────────────────────────┘
                              │ WebSocket
                              │ (binary frames)
┌─────────────────────────────────────────────────────────────┐
│                      API Container                          │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────────────┐ │
│  │ Terminal    │  │ WebSocket   │  │ TerminalSession     │ │
│  │ Manager     │──│ Endpoint    │──│ (new)               │ │
│  │ (lifecycle) │  │ (router)    │  │ - asyncio fd reader │ │
│  └─────────────┘  └─────────────┘  │ - output batcher    │ │
│                                     │ - flow control      │ │
│                                     └─────────────────────┘ │
│                                              │               │
│                                     ┌────────┴────────┐      │
│                                     │ docker exec -it │      │
│                                     │ (subprocess)    │      │
│                                     └────────┬────────┘      │
│                                              │               │
│                                     ┌────────┴────────┐      │
│                                     │ Tool Container  │      │
│                                     │ (bash shell)    │      │
│                                     └─────────────────┘      │
└─────────────────────────────────────────────────────────────┘
```

## File Changes

### New Files
- `apps/api/src/services/terminal_session_v2.py` — New TerminalSession implementation

### Modified Files
- `apps/api/src/services/terminal_session.py` — Delete (or keep as legacy, user said no legacy needed)
- `apps/api/src/services/terminal_manager.py` — Update to use new TerminalSession
- `apps/api/src/api/terminal.py` — Update WebSocket handler for binary frames + flow control
- `apps/web/src/components/terminal.tsx` — Binary mode, WebGL, flow control ack
- `apps/web/package.json` — Add `xterm-addon-webgl`

## TerminalSession Implementation

```python
class TerminalSession:
    """High-performance terminal session with asyncio-native I/O."""
    
    BUFFER_SIZE = 10 * 1024
    IDLE_TIMEOUT = 30 * 60
    BATCH_WINDOW_MS = 2
    FLOW_CONTROL_THRESHOLD = 64 * 1024
    FLOW_CONTROL_RESUME = 32 * 1024
    
    def __init__(self, session_id, instance_id, container_id, ...):
        self._master_fd: int | None = None
        self._process: asyncio.subprocess.Process | None = None
        self._output_buffer: deque[bytes] = deque(maxlen=self.BUFFER_SIZE)
        self._websockets: set[WebSocket] = set()
        self._batch_buffer = bytearray()
        self._batch_timer: asyncio.TimerHandle | None = None
        self._unacknowledged_bytes = 0
        self._paused = False
        self._read_handler_set = False
        
    async def start(self):
        self._master_fd, slave_fd = pty.openpty()
        self._set_terminal_size(80, 24)
        
        self._process = await asyncio.create_subprocess_exec(
            "docker", "exec", "-it", "-e", "TERM=xterm-256color",
            self.container_id, "bash", "-il",
            stdin=slave_fd, stdout=slave_fd, stderr=slave_fd,
        )
        os.close(slave_fd)
        
    def _start_reading(self):
        """Register fd with asyncio event loop for event-driven reading."""
        if self._read_handler_set or self._master_fd is None:
            return
        loop = asyncio.get_event_loop()
        loop.add_reader(self._master_fd, self._on_fd_readable)
        self._read_handler_set = True
        
    def _on_fd_readable(self):
        """Callback when PTY fd has data available."""
        if self._master_fd is None or self._paused:
            return
        try:
            data = os.read(self._master_fd, 4096)
            if data:
                self._add_to_buffer(data)
                self._queue_output(data)
                self.last_activity = time.time()
        except (OSError, IOError):
            pass
            
    def _queue_output(self, data: bytes):
        """Add to batch buffer, schedule flush."""
        self._batch_buffer.extend(data)
        self._unacknowledged_bytes += len(data)
        
        if self._unacknowledged_bytes > self.FLOW_CONTROL_THRESHOLD:
            self._pause_output()
            
        if self._batch_timer is None:
            loop = asyncio.get_event_loop()
            self._batch_timer = loop.call_later(
                self.BATCH_WINDOW_MS / 1000, self._flush_batch
            )
            
    def _flush_batch(self):
        """Flush batched output to all WebSockets."""
        self._batch_timer = None
        if not self._batch_buffer:
            return
            
        payload = bytes(self._batch_buffer)
        self._batch_buffer.clear()
        
        dead = set()
        for ws in self._websockets:
            try:
                asyncio.create_task(ws.send_bytes(payload))
            except Exception:
                dead.add(ws)
        
        self._websockets -= dead
        
    def acknowledge_data(self, char_count: int):
        """Client acknowledges processed bytes."""
        self._unacknowledged_bytes = max(0, self._unacknowledged_bytes - char_count)
        if self._paused and self._unacknowledged_bytes < self.FLOW_CONTROL_RESUME:
            self._resume_output()
            
    def _pause_output(self):
        """Pause reading from PTY."""
        if self._read_handler_set and self._master_fd is not None:
            loop = asyncio.get_event_loop()
            loop.remove_reader(self._master_fd)
            self._read_handler_set = False
        self._paused = True
        
    def _resume_output(self):
        """Resume reading from PTY."""
        self._paused = False
        self._start_reading()
```

## Frontend Changes

### WebSocket Binary Mode
```typescript
const ws = new WebSocket(wsUrl);
ws.binaryType = "arraybuffer";  // Receive ArrayBuffer directly

ws.onmessage = (event) => {
    if (event.data instanceof ArrayBuffer) {
        const data = new Uint8Array(event.data);
        termRef.current?.write(data);
        
        // Flow control: acknowledge processed bytes
        ackAccumulator += data.length;
        if (ackAccumulator >= 4096) {
            ws.send(JSON.stringify({ type: "ack", chars: ackAccumulator }));
            ackAccumulator = 0;
        }
    }
};
```

### WebGL Renderer
```typescript
import { WebglAddon } from "xterm-addon-webgl";

const webglAddon = new WebglAddon();
try {
    term.loadAddon(webglAddon);
} catch (e) {
    console.warn("WebGL failed, using DOM renderer", e);
}
```

## WebSocket Protocol

### Message Types

**Client → Server:**
- `{"type": "input", "data": "base64_encoded"}` — Keystrokes
- `{"type": "resize", "cols": 80, "rows": 24}` — Resize
- `{"type": "ack", "chars": 4096}` — Flow control acknowledgment
- `{"type": "reset"}` — Reset session

**Server → Client:**
- Binary frame: raw terminal output bytes
- `{"type": "status", "status": "connected"}` — Connection status
- `{"type": "ping"}` — Heartbeat (server → client)

## Testing

### Unit Tests
- `test_terminal_session_v2.py` — Test batching, flow control, resize, reset
- `test_terminal_manager.py` — Test session lifecycle with new session class

### Integration Tests
- `test_terminal_websocket.py` — Full WebSocket round-trip

### Performance Tests
- `benchmark_terminal_latency.py` — Measure input/output latency
- `benchmark_terminal_throughput.py` — Measure max throughput

## Rollback Plan

Since this is a full rewrite with no legacy support:
- Keep a backup branch of the old terminal code
- Feature flag in frontend: `?terminal=v2` to test before full rollout
- Monitor error rates after deployment
