Files
temp/docs/reference/tui_permission_question.md
freemo dd17d0f8e6 docs(tui): add shell safety, permission question widget, and first-run docs
Add reference documentation for three new TUI features:

- docs/reference/tui_shell_safety.md: Full reference for the shell danger
  detection subsystem (DangerousPatternDetector, ShellDangerLevel,
  DangerousPattern, DangerousCommandWarning, DEFAULT_PATTERNS registry).
  Covers all built-in patterns across CRITICAL/HIGH/MEDIUM/LOW levels,
  usage examples, and custom pattern extension.

- docs/reference/tui_permission_question.md: Full reference for the inline
  PermissionQuestionWidget (issue #997). Documents InlinePermissionQuestion,
  PermissionRequestType, PermissionDecision, render_permission_question(),
  PermissionDecisionEvent, and PermissionQuestionWidget with key bindings
  and usage examples.

- docs/reference/tui.md: Extended with First-Run Experience section
  (ActorSelectionOverlay, first_run helpers), Inline Permission Questions
  section, shell danger detection note in Shell Mode, updated module table,
  and links to new reference pages.
2026-04-03 18:01:26 +00:00

247 lines
7.5 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 (03) |
| `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