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.
7.5 KiB
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 is used instead.
Introduced in the [Unreleased] milestone (issue #997).
See also 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.
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
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
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
vto request a detailed diff view,
the host application should push the full
PermissionsScreen overlay instead.
Related Documentation
- TUI Reference — overall TUI architecture
- TUI Permissions Screen — full-screen permission overlay for multi-file operations
- TUI Shell Safety — shell command danger detection
- Permissions — permission system and role bindings