# TUI — Permission Question Widget

The Permission Question Widget renders inline permission requests directly in
the conversation stream for **single-file operations**. For multi-file
operations, the full [`PermissionsScreen`](tui_permissions.md) is used instead.

Introduced in the [Unreleased] milestone (issue #997).
See also [`tui.md`](tui.md) for the overall TUI architecture.

**Module**: `cleveragents.tui.widgets.permission_question`

---

## Overview

When an actor requests a single-file operation (write, delete, read, shell
execution, or network access), the TUI renders a `PermissionQuestionWidget`
inline in the conversation stream. The user can respond with single-key
shortcuts or navigate with arrow keys.

```
Permission Required

The actor wants to write to:
  src/api/main.py

  ❯ a  Allow once
    A  Allow always (this session)
    r  Reject once
    R  Reject always (this session)

Press v to open full PermissionsScreen with diff view
```

---

## Domain Models

### `PermissionRequestType`

**Module**: `cleveragents.domain.models.core.inline_permission_question`

`StrEnum` classifying the type of operation being requested.

| Value | Description |
|-------|-------------|
| `file_write` | Actor wants to write to a file |
| `file_delete` | Actor wants to delete a file |
| `file_read` | Actor wants to read a file |
| `shell_exec` | Actor wants to execute a shell command |
| `network` | Actor wants to access the network |

### `PermissionDecision`

**Module**: `cleveragents.domain.models.core.inline_permission_question`

`StrEnum` for the user's decision on a permission request.

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

### `InlinePermissionQuestion`

**Module**: `cleveragents.domain.models.core.inline_permission_question`

Pydantic model representing a single-file permission request.

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `file_path` | `str` | — | Path of the file the actor wants to operate on (non-empty) |
| `request_type` | `PermissionRequestType` | — | Type of operation being requested |
| `diff_content` | `str` | `""` | Unified diff content showing proposed changes (may be empty) |
| `actor_name` | `str` | `"actor"` | Name of the actor requesting permission |

**Properties:**

| Property | Type | Description |
|----------|------|-------------|
| `has_diff` | `bool` | `True` when `diff_content` is non-empty after stripping whitespace |

**Methods:**

| Method | Returns | Description |
|--------|---------|-------------|
| `description_line()` | `str` | Human-readable one-line description (e.g. `"The actor wants to write to:"`) |

---

## `render_permission_question`

**Module**: `cleveragents.tui.widgets.permission_question`

Pure rendering helper that produces the widget's text content.

```python
def render_permission_question(
    question: InlinePermissionQuestion,
    selected_index: int = 0,
    *,
    show_diff: bool = False,
) -> str:
```

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `question` | `InlinePermissionQuestion` | — | The permission question to render |
| `selected_index` | `int` | `0` | Index of the currently highlighted option (0–3) |
| `show_diff` | `bool` | `False` | When `True`, appends the diff content below the options |

Returns a multi-line string suitable for display in a Textual `Static` widget.

---

## `PermissionDecisionEvent`

**Module**: `cleveragents.tui.widgets.permission_question`

Emitted when the user makes a permission decision. The host application
listens for this event to act on the user's choice.

| Attribute | Type | Description |
|-----------|------|-------------|
| `question` | `InlinePermissionQuestion` | The original permission question |
| `decision` | `PermissionDecision` | The decision made by the user |

---

## `PermissionQuestionWidget`

**Module**: `cleveragents.tui.widgets.permission_question`

Textual `Static` subclass that renders an inline permission question in the
conversation stream.

### Constructor

```python
PermissionQuestionWidget(question: InlinePermissionQuestion, *args, **kwargs)
```

### Properties

| Property | Type | Description |
|----------|------|-------------|
| `question` | `InlinePermissionQuestion` | The associated permission question |
| `selected_index` | `int` | Index of the currently highlighted option |
| `decision` | `PermissionDecision \| None` | The decision if made, else `None` |
| `open_full_screen` | `bool` | `True` if the user pressed `v` to open `PermissionsScreen` |

### Navigation

| Method | Returns | Description |
|--------|---------|-------------|
| `move_up()` | `None` | Move the selection cursor up by one option (wraps) |
| `move_down()` | `None` | Move the selection cursor down by one option (wraps) |

### Key Handling

| Method | Returns | Description |
|--------|---------|-------------|
| `handle_key(key)` | `PermissionDecisionEvent \| None` | Process a key press; returns a decision event when resolved |

**Supported keys:**

| Key | Action |
|-----|--------|
| `↑` / `up` | Move selection up |
| `↓` / `down` | Move selection down |
| `Enter` | Confirm the currently highlighted option |
| `a` | Allow once |
| `A` | Allow always (this session) |
| `r` | Reject once |
| `R` | Reject always (this session) |
| `v` | Set `open_full_screen = True` (host opens `PermissionsScreen`) |

---

## Usage Example

```python
from cleveragents.domain.models.core.inline_permission_question import (
    InlinePermissionQuestion,
    PermissionDecision,
    PermissionRequestType,
)
from cleveragents.tui.widgets.permission_question import (
    PermissionDecisionEvent,
    PermissionQuestionWidget,
    render_permission_question,
)

# Build the domain model
question = InlinePermissionQuestion(
    file_path="src/api/main.py",
    request_type=PermissionRequestType.FILE_WRITE,
    diff_content="--- a/src/api/main.py\n+++ b/src/api/main.py\n@@ -1 +1,2 @@\n+import logging\n",
    actor_name="openai/gpt-4o",
)

# Pure rendering (no Textual dependency)
text = render_permission_question(question, selected_index=0, show_diff=True)
print(text)

# Widget usage (requires Textual)
widget = PermissionQuestionWidget(question)

# Simulate key presses
event = widget.handle_key("down")   # moves cursor, returns None
event = widget.handle_key("enter")  # confirms selection, returns PermissionDecisionEvent
assert event is not None
assert event.decision == PermissionDecision.ALLOW_ALWAYS  # second option

# Single-key shortcut
event = widget.handle_key("r")
assert event.decision == PermissionDecision.REJECT_ONCE

# Open full screen
widget.handle_key("v")
assert widget.open_full_screen is True
```

---

## Relationship to PermissionsScreen

The `PermissionQuestionWidget` is designed for **single-file** operations
where a compact inline display is sufficient. When:

- The operation involves **multiple files**, or
- The user presses **`v`** to request a detailed diff view,

the host application should push the full
[`PermissionsScreen`](tui_permissions.md) overlay instead.

---

## Related Documentation

- [TUI Reference](tui.md) — overall TUI architecture
- [TUI Permissions Screen](tui_permissions.md) — full-screen permission overlay for multi-file operations
- [TUI Shell Safety](tui_shell_safety.md) — shell command danger detection
- [Permissions](permissions.md) — permission system and role bindings
