# 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