# TUI — Permissions Screen

The Permissions Screen is a full-screen TUI overlay that presents tool
permission requests to the user before a tool is allowed to modify resources.
It displays a file list on the left and a diff view on the right, and records
the user's decision for the current session.

Introduced in v3.7.0 (issue #996). See also [`tui.md`](tui.md) for the
overall TUI architecture.

**Module**: `cleveragents.tui.permissions`

---

## Overview

When an actor requests a write operation on a resource, the TUI raises the
`PermissionsScreen` overlay. The user reviews the proposed changes and
decides whether to allow or reject the operation.

```
┌─────────────────────────────────────────────────────────────────┐
│  Tool: local/file-write   Resource: local/api-service           │
├──────────────────────┬──────────────────────────────────────────┤
│  Files               │  Diff view                               │
│  ─────               │  ─────────                               │
│  M src/api/main.py   │  --- a/src/api/main.py                   │
│  A src/api/utils.py  │  +++ b/src/api/main.py                   │
│                      │  @@ -10,6 +10,8 @@                       │
│                      │   def run():                              │
│                      │  +    logger.info("starting")            │
│                      │  +    setup_tracing()                     │
│                      │       app.start()                         │
├──────────────────────┴──────────────────────────────────────────┤
│  [a] Allow once  [A] Allow always  [r] Reject once  [R] Reject  │
│  [d] Toggle diff mode (unified / side-by-side / context)        │
└─────────────────────────────────────────────────────────────────┘
```

---

## Keyboard Bindings

| Key | Action |
|-----|--------|
| `a` | Allow this request once |
| `A` | Allow all requests from this tool for the session |
| `r` | Reject this request once |
| `R` | Reject all requests from this tool for the session |
| `d` | Cycle diff display mode (unified → side-by-side → context) |
| `↑` / `↓` | Navigate the file list |

---

## Domain Models

### `DiffDisplayMode`

`StrEnum` controlling how diffs are rendered.

| Value | Description |
|-------|-------------|
| `unified` | Standard unified diff with `+`/`-` lines (default) |
| `side_by_side` | Two-column view — old content left, new content right |
| `context` | Shows only changed lines with 3 lines of surrounding context |

### `FileChangeType`

`StrEnum` for the type of file change.

| Value | Description |
|-------|-------------|
| `M` | File was modified |
| `A` | File was added |
| `D` | File was deleted |

### `PermissionDecision`

`StrEnum` for the user's decision.

| Value | Description |
|-------|-------------|
| `allow_once` | Allow this operation once |
| `allow_always` | Allow all operations from this tool for the session |
| `reject_once` | Reject this operation once |
| `reject_always` | Reject all operations from this tool for the session |

### `PermissionRequest`

Pydantic model representing a single file change within a tool permission
request.

| Field | Type | Description |
|-------|------|-------------|
| `path` | `str` | File path being changed (non-empty) |
| `change_type` | `FileChangeType` | Type of change (M/A/D) |
| `before_content` | `str \| None` | Content before the change (`None` for new files) |
| `after_content` | `str \| None` | Content after the change (`None` for deleted files) |

**Diff methods:**

| Method | Returns | Description |
|--------|---------|-------------|
| `unified_diff(*, context_lines=3)` | `str` | Standard unified diff |
| `side_by_side_diff()` | `tuple[list[str], list[str]]` | `(left_lines, right_lines)` |
| `context_diff(*, context_lines=3)` | `str` | Context diff |
| `render_diff(mode, *, context_lines=3)` | `str` | Render in the specified mode |

### `ToolPermissionRequest`

Pydantic model for a complete tool permission request with multiple file
changes.

| Field | Type | Description |
|-------|------|-------------|
| `request_id` | `str` | Unique identifier for this request |
| `tool_name` | `str` | Name of the tool requesting permission |
| `resource_name` | `str` | Name of the resource being modified |
| `changes` | `list[PermissionRequest]` | File changes in this request |
| `decision` | `PermissionDecision \| None` | User decision (`None` if pending) |

**Properties:**

| Property | Type | Description |
|----------|------|-------------|
| `is_pending` | `bool` | `True` if no decision has been made |
| `is_allowed` | `bool` | `True` if the request was allowed |
| `is_rejected` | `bool` | `True` if the request was rejected |

**Methods:**

| Method | Returns | Description |
|--------|---------|-------------|
| `apply_decision(decision)` | `ToolPermissionRequest` | Returns a new request with the decision applied |

---

## PermissionRequestService

**Module**: `cleveragents.tui.permissions.service`

In-memory service that manages the permission request queue and persists
session-scoped decisions (`allow_always` / `reject_always`).

### Methods

| Method | Returns | Description |
|--------|---------|-------------|
| `enqueue(request)` | `None` | Add a request to the queue; auto-resolves if a session decision exists |
| `get(request_id)` | `ToolPermissionRequest \| None` | Return a request by ID |
| `pending_requests()` | `list[ToolPermissionRequest]` | All undecided requests |
| `all_requests()` | `list[ToolPermissionRequest]` | All requests (pending and decided) |
| `record_decision(request_id, decision)` | `ToolPermissionRequest \| None` | Apply a decision; records session-scoped decisions for `allow_always` / `reject_always` |
| `get_session_decision(tool_name)` | `PermissionDecision \| None` | Return the session-scoped decision for a tool |
| `clear_session_decision(tool_name)` | `bool` | Remove a session-scoped decision |
| `clear_all()` | `None` | Clear all requests and session decisions |

### Session-Scoped Decisions

When the user presses `A` (allow-always) or `R` (reject-always), the
`PermissionRequestService` records the decision against the tool name.
Subsequent requests from the same tool are auto-resolved immediately when
`enqueue()` is called, without showing the `PermissionsScreen`.

---

## PermissionsScreen Widget

**Module**: `cleveragents.tui.permissions.screen`

The `PermissionsScreen` class is a Textual `Screen` subclass that renders
the split-pane layout. It is instantiated by the main TUI app when a
`ToolPermissionRequest` arrives.

### Diff Mode Cycling

The diff display mode cycles through `unified → side_by_side → context`
each time the user presses `d`. The current mode is displayed in the
status bar.

---

## Usage Example

```python
from cleveragents.tui.permissions.models import (
    DiffDisplayMode,
    FileChangeType,
    PermissionDecision,
    PermissionRequest,
    ToolPermissionRequest,
)
from cleveragents.tui.permissions.service import PermissionRequestService

service = PermissionRequestService()

# Build a permission request
request = ToolPermissionRequest(
    request_id="req-001",
    tool_name="local/file-write",
    resource_name="local/api-service",
    changes=[
        PermissionRequest(
            path="src/api/main.py",
            change_type=FileChangeType.MODIFIED,
            before_content="def run():\n    app.start()\n",
            after_content="def run():\n    logger.info('starting')\n    app.start()\n",
        )
    ],
)

# Enqueue the request
service.enqueue(request)

# Render the diff
change = request.changes[0]
print(change.render_diff(DiffDisplayMode.UNIFIED))

# Record a decision
service.record_decision("req-001", PermissionDecision.ALLOW_ONCE)
```

---

## Related Documentation

- [TUI Reference](tui.md) — overall TUI architecture and key bindings
- [Permissions](permissions.md) — permission system and role bindings
- [Safety Profiles](safety_profiles.md) — automation profile safety levels
