# Ticket 4.2 — Permission Approval UI

**Phase**: 4 — Frontend UI
**Priority**: Critical (blocking)
**Depends on**: 4.1, 1.5

## Description

Display permission requests from the agent with large, mobile-friendly approve/reject buttons. When the agent needs to run a bash command, edit a file, or access an external directory, the user must approve or deny from the browser.

Note: OpenCode's permission model differs from Claude Code's. Permissions are categorized by type (e.g., `bash`, `edit`, `external_directory`, `doom_loop`) with glob patterns for file paths, rather than by tool name + input. The permission bridge (ticket 1.5) translates between models, so the frontend can use the same decision vocabulary (`allow`/`deny`/`allow_always`) — the bridge maps these to OpenCode's `once`/`always`/`reject`.

## Acceptance Criteria

### AC1: Displays permission request inline in chat

```gherkin
Given the agent requests permission to run `npm install`
When the permission_request arrives via WebSocket
Then a permission card appears in the chat showing:
  - Tool name ("bash")
  - Tool input summary ("npm install")
  - Large green "Approve" button
  - Large red "Reject" button
  - "Always Allow" option (checkbox or third button)
```

### AC2: Approve sends response and updates UI

```gherkin
Given a permission request is displayed
When the user taps "Approve"
Then the browser sends { type: "permission_response", requestId, decision: "allow" }
And the permission card updates to show "Approved ✓"
And the buttons are replaced with the approval status
And the card is not removable (stays in chat history)
```

### AC3: Reject sends response and updates UI

```gherkin
Given a permission request is displayed
When the user taps "Reject"
Then the browser sends { type: "permission_response", requestId, decision: "deny" }
And the permission card updates to show "Rejected ✗"
```

### AC4: Always Allow caches the decision

```gherkin
Given a permission request for tool "read"
When the user selects "Always Allow"
Then the browser sends decision: "allow_always"
And future permission requests for "read" show as auto-approved
```

### AC5: Multiple pending permissions stack

```gherkin
Given 3 permission requests arrive before the first is resolved
When they are displayed
Then all 3 appear as separate cards in the chat
And each can be resolved independently
And resolving one does not affect the others
```

### AC6: Mobile-optimised buttons

```gherkin
Given the browser is on a mobile device
When a permission request is displayed
Then the approve/reject buttons are at least 48px tall
And they are full-width on narrow screens
And they have sufficient padding for touch targets
```

### AC7: Favicon/title blinks on pending permission

```gherkin
Given a permission request is pending
When the browser tab is not focused
Then the page title alternates between the normal title and "⚠️ Approval needed"
And the favicon changes to indicate attention is needed
```

### AC8: Tool input display

```gherkin
Given a permission request with complex tool input
When it is displayed
Then bash commands show the full command in a code block
And file edits show the file path and a snippet of the change
And long inputs are truncated with an "expand" toggle
```

## Testable Components

| Component | Test Type | What to Assert |
|-----------|-----------|----------------|
| Permission card data mapping | Unit | OpenCode permission fields map to correct card fields |
| Input truncation logic | Unit | Long inputs truncated at limit with expand flag |
| Permission card rendering | E2E | Card shows tool name, input summary, Approve/Reject/Always buttons |
| Approve flow | E2E | Tapping Approve sends correct response, card updates to resolved |
| Reject flow | E2E | Tapping Reject sends correct response, card updates to rejected |
| Always Allow | E2E | Tapping Always sends allow_always, subsequent requests auto-approved |
| Multiple pending | E2E | Multiple permission cards visible simultaneously, independently resolvable |
| Mobile button sizing | E2E | Buttons are at least 48px tall, full-width on narrow viewport |
| Title blinking | E2E | Browser title alternates when permission pending and tab unfocused |
| Tool input display | E2E | Bash commands shown in code block; file paths shown with snippet |

## Reference

- Claude-relay permission UI: `../claude-relay/lib/public/modules/tools.js`
- Claude-relay title blink: `../claude-relay/lib/public/app.js`
