Files
cleveragents-core/docs/api/tui.md
freemo d66887766f docs(tui): document PermissionQuestionWidget and add CHANGELOG entry
Add PermissionQuestionWidget API documentation to docs/api/tui.md including
method table, key bindings, PermissionDecisionEvent dataclass, and
render_permission_question() helper reference. Add corresponding CHANGELOG
entry under [Unreleased] Added section.

Refs: #997
2026-04-03 06:39:51 +00:00

281 lines
7.2 KiB
Markdown

# `cleveragents.tui` — Interactive Terminal UI
The `tui` package provides the full-screen Textual-based terminal user interface.
It requires the optional `cleveragents[tui]` extra.
See [ADR-044](../adr/ADR-044-tui-architecture-and-framework.md) (TUI framework),
[ADR-045](../adr/ADR-045-tui-persona-system.md) (persona system), and
[ADR-046](../adr/ADR-046-tui-reference-and-command-system.md) (reference and command system)
for design rationale.
---
## Launching the TUI
```bash
pip install "cleveragents[tui]"
agents tui
```
---
## First-Run Experience
### `is_first_run(registry: PersonaRegistry) → bool`
Returns `True` when no personas are configured — used by `app.on_mount()` to
decide whether to show the actor selection overlay.
### `create_default_persona_for_actor(registry: PersonaRegistry, actor: str) → None`
Creates and persists a `"default"` persona bound to `actor` after the user
completes the first-run selection flow.
### `ActorSelectionOverlay`
```python
from cleveragents.tui.widgets import ActorSelectionOverlay
```
A centred Textual widget displayed on first launch.
| Method | Description |
|--------|-------------|
| `show()` | Make the overlay visible |
| `hide()` | Dismiss the overlay |
| `move_up()` | Move selection cursor up (wraps) |
| `move_down()` | Move selection cursor down (wraps) |
| `set_search(query: str)` | Apply fuzzy filter to actor list |
| `confirm() → str` | Confirm selection; returns actor name and hides overlay |
| `render_actor_selection() → str` | Pure rendering function (testable without Textual) |
Default actor list (in display order):
| Actor | Notes |
|-------|-------|
| `anthropic/claude-4-sonnet` | Recommended |
| `anthropic/claude-4-opus` | |
| `openai/gpt-4o` | |
| `openai/o3` | |
| `google/gemini-2` | |
**Key bindings inside the overlay:**
| Key | Action |
|-----|--------|
| `j` / `↓` | Move down |
| `k` / `↑` | Move up |
| `/` | Enter fuzzy search |
| `Enter` | Confirm selection |
---
## Persona System
Personas are YAML files stored in `~/.config/cleveragents/personas/`. Each
persona binds an actor, optional argument presets, and scope references to a
named identity.
### `PersonaRegistry`
```python
from cleveragents.tui.persona import PersonaRegistry
registry = PersonaRegistry()
registry.load() # load all YAML files from config dir
persona = registry.get("default") # retrieve by name
registry.save(persona) # persist changes
registry.ensure_default(actor="openai/gpt-4o") # create default if absent
```
### `Persona`
```python
@dataclass
class Persona:
name: str
actor: str
presets: list[ArgumentPreset]
scope_refs: list[str]
```
---
## Input Mode Routing
The prompt auto-detects three input modes from the first character:
| First character | Mode | Handler |
|-----------------|------|---------|
| (none / letter) | Normal | Message + `@reference` expansion |
| `/` | Command | Slash command overlay |
| `!` | Shell | Subprocess passthrough |
### `InputModeRouter`
```python
from cleveragents.tui.routing import InputModeRouter
router = InputModeRouter(container)
await router.dispatch(input_text, session_id)
```
---
## Slash Commands
67 slash commands across 14 groups are exposed via `SlashCommandOverlay`.
```python
from cleveragents.tui.commands import SLASH_COMMAND_SPECS
# SLASH_COMMAND_SPECS: dict[str, list[SlashCommandSpec]]
# Keys are group names; values are lists of command specs.
```
**Groups:** Session, Persona, Scope, Plan, Project, Actor, Resource, Config,
Tool, Skill, Invariant, Profile, Context, Utility.
### Session commands (via `TuiCommandRouter`)
| Command | Description |
|---------|-------------|
| `/session:export [--format json\|md] [path]` | Export session to JSON or Markdown |
| `/session:import <path>` | Import a session from a JSON file |
```python
# Export as Markdown transcript
/session:export --format md ~/my-session.md
# Export as canonical JSON (default)
/session:export ~/my-session.json
# Import a previously exported session
/session:import ~/my-session.json
```
---
## Session Export / Import
### `Session.as_export_markdown() → str`
Domain method on `Session` that renders a human-readable Markdown transcript.
The output is **lossy** (for sharing/documentation) and cannot be re-imported.
```python
from cleveragents.domain.models.core.session import Session
session: Session = ...
md = session.as_export_markdown()
# Returns a Markdown string with:
# - Header block: session ID, actor, created_at, message count
# - Message history: role | timestamp | content
# - Linked plan references
```
### CLI
```bash
# Export as canonical JSON (importable)
agents session export --session-id <ID> --output session.json
# Export as Markdown transcript (human-readable, not importable)
agents session export --session-id <ID> --output session.md --format md
# Import from JSON
agents session import --input session.json
```
---
## Widgets
### `ThoughtBlockWidget`
Renders actor reasoning traces inline in the conversation stream.
```python
from cleveragents.tui.widgets import ThoughtBlockWidget
```
- Collapsed by default; press `Space` to expand/collapse
- Muted styling distinguishes thought blocks from regular messages
- Backed by `ThoughtBlock` domain model with configurable `max_lines` (default: 10)
### `PermissionQuestionWidget`
Inline permission question widget rendered directly in the conversation stream
for single-file permission requests. For multi-file operations the full
`PermissionsScreen` is pushed instead.
```python
from cleveragents.tui.widgets import PermissionQuestionWidget
from cleveragents.domain.models.core.inline_permission_question import (
InlinePermissionQuestion,
PermissionDecision,
)
widget = PermissionQuestionWidget(question)
event = widget.handle_key("a") # returns PermissionDecisionEvent or None
```
| Method | Description |
|--------|-------------|
| `move_up()` | Move selection cursor up (wraps) |
| `move_down()` | Move selection cursor down (wraps) |
| `handle_key(key: str) → PermissionDecisionEvent \| None` | Process a key press; returns a decision event when resolved |
**Key bindings:**
| Key | Action |
|-----|--------|
| `a` | Allow once |
| `A` | Allow always (this session) |
| `r` | Reject once |
| `R` | Reject always (this session) |
| `↑` / `↓` | Navigate options |
| `Enter` | Confirm highlighted option |
| `v` | Open full `PermissionsScreen` with diff view |
**`PermissionDecisionEvent`** — emitted when the user makes a decision:
```python
@dataclass
class PermissionDecisionEvent:
question: InlinePermissionQuestion
decision: PermissionDecision
```
**`render_permission_question(question, selected_index=0, *, show_diff=False) → str`** — pure rendering helper (testable without Textual).
---
### `PermissionsScreen`
Full-screen overlay for tool permission requests.
```python
from cleveragents.tui.screens import PermissionsScreen
```
| Key | Action |
|-----|--------|
| `a` | Allow once |
| `A` | Allow always |
| `r` | Reject once |
| `R` | Reject always |
| `d` | Cycle diff display mode (unified → side-by-side → context) |
---
## TUI State Persistence
The TUI persists minimal state to `~/.config/cleveragents/tui-state.yaml`:
```yaml
last_persona: "default"
```
This is loaded on startup to restore the last active persona.