# 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