forked from HAL9000/cleveragents-core
dd17d0f8e6
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.
247 lines
7.5 KiB
Markdown
247 lines
7.5 KiB
Markdown
# 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
|