# Ticket 2.2 — WebSocket Handler

**Phase**: 2 — Server & WebSocket Layer
**Priority**: Critical (blocking)
**Depends on**: 2.1

## Description

Handle WebSocket connections from browsers. Each project has its own WebSocket endpoint (`/p/{slug}/ws`). The handler routes incoming messages to the correct bridge/handler and broadcasts outgoing events from the OpenCode bridge to all connected clients.

## Acceptance Criteria

### AC1: WebSocket upgrade on project path

```gherkin
Given the HTTP server is running with project slug "my-app"
When a browser requests WebSocket upgrade on /p/my-app/ws
Then the server upgrades the connection
And it associates the connection with the "my-app" project
And it increments the client count for that project
```

### AC2: Broadcasts events to all connected clients

```gherkin
Given 3 browsers are connected to project "my-app"
When the OpenCode bridge emits a delta event
Then all 3 browsers receive the { type: "delta", text: "..." } message
And the messages are sent in order
```

### AC3: Routes incoming messages to correct handler

```gherkin
Given a browser is connected
When it sends { type: "message", text: "Hello" }
Then the handler forwards it to sendMessage on the REST client

When it sends { type: "permission_response", requestId: "abc", decision: "allow" }
Then the handler forwards it to the permission bridge

When it sends { type: "ask_user_response", toolId: "q1", answers: {...} }
Then the handler forwards it to the question bridge

When it sends { type: "new_session" }
Then the handler creates a new session via the REST client

When it sends { type: "switch_session", id: "sess123" }
Then the handler switches the active session and replays history
```

### AC4: Client count tracking and broadcast

```gherkin
Given 2 browsers are connected to a project
When a 3rd browser connects
Then all 3 browsers receive { type: "client_count", count: 3 }

When 1 browser disconnects
Then the remaining 2 receive { type: "client_count", count: 2 }
```

### AC5: New client receives current state

```gherkin
Given a session is active with pending permissions and recent history
When a new browser connects
Then it receives the current session state:
  - Session ID and title
  - Model/agent info
  - Any pending permission requests
  - Any pending questions
  - Recent message history (last N messages)
  - Client count
  - Processing status
```

### AC6: Handles client disconnection gracefully

```gherkin
Given a browser is connected
When the connection drops (network issue, tab close)
Then the server removes the client from the project's client list
And it updates the client count broadcast
And it does NOT affect other clients or the OpenCode bridge
And it cleans up any client-specific state (e.g., terminal sessions)
```

### AC7: Handles invalid messages

```gherkin
Given a browser sends a message with unknown type
When the handler receives it
Then it logs a warning with the message type
And it sends { type: "error", message: "Unknown message type: ..." } to that client
And it does NOT disconnect the client
```

## Testable Components

| Component | Test Type | What to Assert |
|-----------|-----------|----------------|
| WebSocket upgrade | Unit | Upgrades on correct path, rejects wrong slug |
| `broadcast(message)` | Unit | Sends to all connected clients, skips closed connections |
| `routeMessage(msg)` | Unit | Routes by type to correct handler |
| `onConnect(ws)` | Unit | Increments count, sends state snapshot, broadcasts count |
| `onDisconnect(ws)` | Unit | Decrements count, cleans up, broadcasts count |
| `onMessage(ws, data)` | Unit | Parses JSON, routes, handles errors |
| Invalid message handling | Unit | Logs warning, sends error, doesn't disconnect |
| State replay on connect | Fixture | New client receives full state snapshot |
| Multi-client broadcast | Fixture | All clients receive same events |

## API Shape

```ts
// src/lib/server.ts (WebSocket section) or src/lib/websocket-handler.ts
export class WebSocketHandler {
  constructor(project)            // Associated with a specific project

  handleUpgrade(request, socket, head)
  broadcast(message)              // Send to all clients
  sendTo(ws, message)             // Send to specific client
  getClientCount()                // Number of connected clients
  getClients()                    // Iterate connected clients

  // Incoming message routing
  onUserMessage(ws, data)
  onPermissionResponse(ws, data)
  onQuestionResponse(ws, data)
  onSessionCommand(ws, data)
  onFileCommand(ws, data)
  onTerminalCommand(ws, data)
}
```

## Reference

- Claude-relay WebSocket: `../claude-relay/lib/server.js` (WebSocket upgrade)
- Claude-relay message routing: `../claude-relay/lib/project.js` (handleWSMessage)
- Message types: see ticket 1.3 for the relay protocol
