# `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 ` | 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 --output session.json # Export as Markdown transcript (human-readable, not importable) agents session export --session-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) | --- ## Shell Danger Detection **Module:** `cleveragents.tui.shell_safety` The TUI shell mode (`!` prefix) checks commands against a configurable pattern registry before execution. Commands that match a dangerous pattern surface a warning overlay; commands at or above the configured `block_level` are blocked automatically unless a `warn_callback` is provided. ### `ShellDangerLevel` ```python from cleveragents.tui.shell_safety import ShellDangerLevel ``` `IntEnum` with four severity levels (ordered least to most severe): | Level | Value | Description | |-------|-------|-------------| | `LOW` | 1 | Minor risk — generally recoverable (e.g. `chmod 777` on a single file) | | `MEDIUM` | 2 | Moderate risk — can cause data loss or security exposure (e.g. `sudo rm`, `wget \| sh`) | | `HIGH` | 3 | High risk — significant, hard-to-reverse damage (e.g. `dd if=`, `mkfs`) | | `CRITICAL` | 4 | Critical risk — can destroy the system or create a fork bomb (e.g. `rm -rf /`, `:(){ :\|:& };:`) | Numeric comparisons work naturally: `level >= ShellDangerLevel.HIGH`. --- ### `DangerousPattern` ```python from cleveragents.tui.shell_safety import DangerousPattern pattern = DangerousPattern( name="rm_rf_root", pattern=r"rm\s+-[rRfF]*\s+/", level=ShellDangerLevel.CRITICAL, description="Recursively deletes the root filesystem.", ) ``` Immutable frozen dataclass describing a single dangerous shell pattern. | Attribute | Type | Description | |-----------|------|-------------| | `name` | `str` | Short human-readable identifier | | `pattern` | `str` | Regex matched against the command text | | `level` | `ShellDangerLevel` | Severity classification | | `description` | `str` | Human-readable explanation of the risk | | `case_sensitive` | `bool` | When `True`, regex is compiled without `re.IGNORECASE` (default: `False`) | **`matches(command: str) → bool`** — returns `True` if `command` matches the compiled pattern. --- ### `ShellSafetyService` ```python from cleveragents.tui.shell_safety import ShellSafetyService, ShellDangerLevel service = ShellSafetyService(block_level=ShellDangerLevel.MEDIUM) result = service.check_command("rm -rf /") if not result.allowed: print(result.warning.message) ``` Application service that checks shell commands before execution. **Constructor parameters:** | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `detector` | `DangerousPatternDetector \| None` | `None` | Custom detector; a default one is created when `None` | | `block_level` | `ShellDangerLevel` | `MEDIUM` | Minimum level that triggers automatic blocking when no callback is provided | | `warn_callback` | `Callable[[DangerousCommandWarning], bool] \| None` | `None` | Optional callback invoked for dangerous commands; return `True` to allow, `False` to block | | `extra_patterns` | `list[DangerousPattern] \| None` | `None` | Additional patterns registered on top of the defaults | **Methods:** | Method | Returns | Description | |--------|---------|-------------| | `check_command(command)` | `SafetyCheckResult` | Check a command and return a structured result | | `is_safe(command)` | `bool` | Convenience wrapper — returns `True` if the command passes | **`SafetyCheckResult`** attributes: | Attribute | Type | Description | |-----------|------|-------------| | `command` | `str` | The command that was checked | | `warning` | `DangerousCommandWarning \| None` | Warning if a dangerous pattern matched, otherwise `None` | | `allowed` | `bool` | `True` if the command is allowed to proceed | **Built-in pattern categories:** - Destructive filesystem operations (`rm -rf`, `shred`, `wipe`) - Privilege escalation (`sudo`, `su`, `chmod 777 /`) - Network exfiltration (`curl \| sh`, `wget \| bash`, `nc` reverse shells) - Fork bombs and resource exhaustion - Disk-level writes (`dd if=`, `mkfs`) --- ## 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.