# Ticket 4.5 — Terminal Tabs

**Phase**: 4 — Frontend UI
**Priority**: Medium
**Depends on**: 2.2, 2.7

## Description

Embedded terminal emulator in the browser using xterm.js. Supports multiple terminal tabs, resize, and scrollback. Allows the user to run commands without leaving the relay UI.

### OpenCode PTY API

Unlike claude-relay which manages PTY processes directly via `node-pty`, OpenCode exposes PTY management through its REST API and WebSocket:

- **Create PTY**: `POST /pty/` → returns `Pty.Info` with `{ id, title, command, args, cwd, status, pid }`
- **List PTYs**: `GET /pty/` → returns `Pty.Info[]`
- **Delete PTY**: `DELETE /pty/:ptyID`
- **I/O via WebSocket**: `GET /pty/:ptyID/connect?cursor=` — bidirectional text communication
- **SSE events**: `pty.created`, `pty.updated`, `pty.exited`, `pty.deleted`

This means the relay can **proxy OpenCode's PTY WebSocket** instead of spawning its own processes, removing the `@lydell/node-pty` dependency entirely. The relay's WebSocket handler bridges between the browser's WebSocket and OpenCode's PTY WebSocket.

## Acceptance Criteria

### AC1: Create terminal tab

```gherkin
When the user clicks "New Terminal"
Then a new terminal tab opens
And a PTY session is created on the server via WebSocket
And the terminal renders with a shell prompt
And the terminal dimensions match the browser panel size
```

### AC2: Terminal input and output

```gherkin
Given a terminal tab is active
When the user types `ls -la` and presses Enter
Then the command is sent to the PTY via WebSocket
And the output is rendered in the terminal
And ANSI colour codes are rendered correctly
```

### AC3: Multiple terminal tabs

```gherkin
Given terminal tabs "Term 1" and "Term 2" exist
When the user switches between tabs
Then each tab preserves its own session and scrollback
And the inactive tab's PTY continues running in the background
```

### AC4: Terminal resize

```gherkin
Given a terminal tab is active
When the browser panel is resized
Then the terminal dimensions update to fill the new space
And the PTY is notified of the new dimensions (cols × rows)
And existing content reflows correctly
```

### AC5: Close terminal tab

```gherkin
Given a terminal tab is active
When the user clicks the close button on the tab
Then the PTY session is terminated on the server
And the tab is removed
And if it was the only tab, the terminal panel closes
```

### AC6: Maximum terminal limit

```gherkin
Given 10 terminal tabs are open
When the user tries to open an 11th
Then a message appears: "Maximum of 10 terminals reached"
And no new terminal is created
```

### AC7: Scrollback buffer

```gherkin
Given a terminal with extensive output
When the user scrolls up
Then they can view past output up to 50KB of scrollback
And scrolling to the bottom resumes live output
```

## Testable Components

| Component | Test Type | What to Assert |
|-----------|-----------|----------------|
| Max terminal limit | Unit | Rejects creation when at configured maximum |
| Terminal creation | E2E | New tab created, PTY requested, xterm.js renders |
| Terminal I/O | E2E | Keystrokes sent to PTY, output rendered with ANSI colours |
| Tab switching | E2E | Clicking tab shows correct terminal, sessions independent |
| Terminal resize | E2E | Resizing sends new dimensions, xterm fits container |
| Close tab | E2E | Close button removes tab, kills PTY |
| Scrollback | E2E | Can scroll up in terminal output |

## Reference

- Claude-relay terminal: `../claude-relay/lib/terminal-manager.js`, `../claude-relay/lib/terminal.js`
- Claude-relay terminal UI: `../claude-relay/lib/public/modules/terminal.js`
- OpenCode PTY routes: `../opencode/packages/opencode/src/server/routes/pty.ts`
- OpenCode PTY module: `../opencode/packages/opencode/src/pty/index.ts`
- Uses xterm.js in browser, OpenCode's PTY WebSocket on server (no node-pty dependency)
- PTY I/O: proxy browser WebSocket ↔ OpenCode PTY WebSocket at /pty/:id/connect
- SSE events for lifecycle: pty.created, pty.updated, pty.exited, pty.deleted
