# Ticket 2.7 — PTY WebSocket Proxy

**Phase**: 2 — Server & WebSocket Layer
**Priority**: Medium
**Depends on**: 2.1, 1.4

## Description

Server-side module that proxies browser WebSocket connections to OpenCode's PTY WebSocket endpoint (`GET /pty/:ptyID/connect`). This enables the browser terminal (ticket 4.5) to communicate with OpenCode-managed PTY sessions without the relay needing to manage its own PTY processes or depend on `node-pty`.

The proxy bridges bidirectional text I/O: keystrokes from the browser are forwarded to OpenCode's PTY, and PTY output is forwarded back to the browser.

## Acceptance Criteria

### AC1: Proxy WebSocket connection

```gherkin
Given a browser connects to the relay's PTY WebSocket endpoint
When the relay receives the connection for a specific ptyID
Then it opens a WebSocket connection to OpenCode at GET /pty/{ptyID}/connect
And it begins proxying messages bidirectionally
And the browser receives PTY output in real-time
```

### AC2: Browser-to-PTY forwarding

```gherkin
Given a proxied PTY connection is established
When the browser sends a text message (keystrokes)
Then the relay forwards it to OpenCode's PTY WebSocket
And the forwarding adds minimal latency (< 5ms)
```

### AC3: PTY-to-browser forwarding

```gherkin
Given a proxied PTY connection is established
When the OpenCode PTY WebSocket sends output data
Then the relay forwards it to the browser WebSocket
And ANSI escape codes are preserved unchanged
And binary data is forwarded correctly
```

### AC4: PTY lifecycle management via REST

```gherkin
Given the browser wants to create a new terminal
When the relay receives a "create PTY" request
Then it calls POST /pty/ on the OpenCode API (via REST client from 1.4)
And it returns the PTY info (id, title, command, cwd, status, pid)

When the browser wants to close a terminal
Then the relay calls DELETE /pty/{ptyID} on the OpenCode API
And it closes the proxy WebSocket connection
```

### AC5: Resize support

```gherkin
Given a proxied PTY connection is active
When the browser sends a resize message with { cols, rows }
Then the relay forwards the resize to OpenCode's PTY WebSocket
And the PTY adjusts its dimensions
And existing content reflows correctly
```

### AC6: Connection cleanup

```gherkin
Given a proxied PTY connection exists
When the browser WebSocket disconnects
Then the relay closes the upstream OpenCode PTY WebSocket
And resources are cleaned up

When the OpenCode PTY WebSocket disconnects unexpectedly
Then the relay notifies the browser of the disconnection
And the terminal tab shows an appropriate error state
```

### AC7: Cursor-based reconnection

```gherkin
Given a PTY connection was previously active
When the browser reconnects to the same ptyID
Then it can pass a cursor query parameter to GET /pty/{ptyID}/connect?cursor=
And OpenCode replays output from that cursor position
And the terminal resumes from where it left off
```

### AC8: Multiple concurrent PTY proxies

```gherkin
Given 5 terminal tabs are open in the browser
Then 5 independent proxy connections exist
And each proxy routes messages to the correct PTY
And closing one proxy does not affect others
```

## Testable Components

| Component | Test Type | What to Assert |
|-----------|-----------|----------------|
| `createProxy(browserWs, ptyID)` | Unit | Opens upstream WS to correct URL, begins forwarding |
| Browser→PTY forwarding | Unit | Messages forwarded with correct content |
| PTY→browser forwarding | Unit | Messages forwarded, ANSI preserved |
| Resize forwarding | Unit | Resize message format correct |
| Connection cleanup | Unit | Both sides closed, resources freed |
| Multiple proxies | Fixture | Independent routing, no cross-talk |
| Reconnection with cursor | Fixture | Output replayed from cursor position |

## API Shape

```ts
// src/lib/pty-proxy.ts
export class PtyProxy {
  constructor(options: { openCodeBaseUrl: string; auth?: { username: string; password: string } })

  // Create a proxy for a specific PTY
  async proxy(browserWs: WebSocket, ptyID: string, cursor?: string): Promise<void>

  // Close a specific proxy
  async close(ptyID: string): Promise<void>

  // Close all proxies (shutdown)
  async closeAll(): Promise<void>

  // Get count of active proxies
  getActiveCount(): number
}
```

## Reference

- OpenCode PTY routes: `../opencode/packages/opencode/src/server/routes/pty.ts`
- OpenCode PTY module: `../opencode/packages/opencode/src/pty/index.ts`
- OpenCode PTY WebSocket: `GET /pty/:ptyID/connect?cursor=` — bidirectional text I/O
- OpenCode PTY REST: `POST /pty/` (create), `GET /pty/` (list), `DELETE /pty/:ptyID` (delete)
- SSE events: `pty.created`, `pty.updated`, `pty.exited`, `pty.deleted`
- Claude-relay terminal: `../claude-relay/lib/terminal-manager.js` (uses node-pty — we replace this with proxy)
