Files
cleveragents-core/docs/api/tui.md
T
freemo 66f2ed97e2 docs: add shell safety API docs and update README highlights
Document the new ShellSafetyService subsystem (DangerousPattern,
ShellDangerLevel, DangerousPatternDetector, SafetyCheckResult) in
docs/api/tui.md with the full built-in pattern table and usage examples.

Update README.md Highlights to surface inline PermissionQuestionWidget,
shell danger detection, and plan workflow isolation (legacy/v3 mixing
guard) introduced in the unreleased milestone.
2026-04-28 09:25:02 +00:00

12 KiB

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 (TUI framework), ADR-045 (persona system), and ADR-046 (reference and command system) for design rationale.


Launching the TUI

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

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

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

@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

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.

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
# 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.

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

# 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.

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.

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:

@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.

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 Safety

The TUI shell mode (! prefix) passes commands through a configurable danger-detection layer before execution. A warning overlay is surfaced to the user when a dangerous pattern is matched; the user can choose to proceed or abort.

Module: cleveragents.tui.shell_safety

ShellDangerLevel

from cleveragents.tui.shell_safety import ShellDangerLevel

class ShellDangerLevel(IntEnum):
    LOW      = 1   # minor risk, generally recoverable
    MEDIUM   = 2   # moderate risk — data loss or security exposure possible
    HIGH     = 3   # significant, hard-to-reverse damage likely
    CRITICAL = 4   # system destruction or fork bomb

Levels are ordered so numeric comparisons work naturally: level >= ShellDangerLevel.HIGH.


DangerousPattern

from cleveragents.tui.shell_safety import DangerousPattern

pattern = DangerousPattern(
    name="rm_rf_root",
    pattern=r"rm\s+(-\w*r\w*f|-\w*f\w*r)\s+/\s*$",
    level=ShellDangerLevel.CRITICAL,
    description="rm -rf / recursively deletes the entire filesystem root.",
)

A frozen dataclass describing a single dangerous shell pattern.

Field Type Description
name str Unique identifier for the pattern
pattern str Regular expression matched against the command
level ShellDangerLevel Severity classification
description str Human-readable explanation shown in the warning overlay

Built-in Patterns (DEFAULT_PATTERNS)

The default pattern registry covers the following categories:

Name Level Description
rm_rf_root CRITICAL rm -rf / — destroys filesystem root
rm_rf_wildcard CRITICAL rm -rf /* or similar wildcard paths
fork_bomb CRITICAL `:(){ :
dd_if_device HIGH dd if= — can overwrite disk devices
mkfs HIGH Formats a filesystem, erasing all data
shred_device HIGH shred on a device or with --remove
chmod_777 MEDIUM World-writable permissions
sudo_rm MEDIUM sudo rm — elevated-privilege deletion
wget_pipe_sh MEDIUM Remote code execution via wget | sh
curl_pipe_sh MEDIUM Remote code execution via curl | sh
wget_pipe_bash MEDIUM Remote code execution via wget | bash
curl_pipe_bash MEDIUM Remote code execution via curl | bash
git_push_force LOW git push --force — overwrites remote history
chmod_recursive_permissive LOW Recursive chmod with permissive modes

ShellSafetyService

from cleveragents.tui.shell_safety import ShellSafetyService

service = ShellSafetyService()
result = service.check_command("rm -rf /")
if not result.allowed:
    print(result.warning.message)

Application service that wraps DangerousPatternDetector with a higher-level API.

ShellSafetyService(
    *,
    detector: DangerousPatternDetector | None = None,
    block_level: ShellDangerLevel = ShellDangerLevel.MEDIUM,
    warn_callback: Callable[[DangerousCommandWarning], bool] | None = None,
    extra_patterns: list[DangerousPattern] | None = None,
)
Parameter Description
detector Custom detector; defaults to DangerousPatternDetector() with DEFAULT_PATTERNS
block_level Minimum level that triggers automatic blocking when no warn_callback is provided (default: MEDIUM)
warn_callback Optional (warning) -> bool; return True to allow, False to block
extra_patterns Additional patterns registered on top of the defaults

Methods:

Method Returns Description
check_command(command) SafetyCheckResult Full safety check with warning and allow/block decision
is_safe(command) bool Convenience wrapper — True if command is allowed

Properties:

Property Type Description
detector DangerousPatternDetector The underlying detector instance
block_level ShellDangerLevel The configured automatic-block threshold

SafetyCheckResult

result = service.check_command("sudo rm -rf /var/log")
result.command   # "sudo rm -rf /var/log"
result.allowed   # False (MEDIUM >= block_level MEDIUM)
result.warning   # DangerousCommandWarning(...)
Attribute Type Description
command str The command that was checked
warning DangerousCommandWarning | None Warning if a pattern matched, else None
allowed bool True if the command is permitted to proceed

DangerousPatternDetector

from cleveragents.tui.shell_safety import DangerousPatternDetector

detector = DangerousPatternDetector()
detector.add_pattern(my_pattern)
warning = detector.check_first("rm -rf /")

Low-level detector that scans a command string against all registered patterns and returns the first (highest-severity) match as a DangerousCommandWarning, or None if no pattern matches.


TUI State Persistence

The TUI persists minimal state to ~/.config/cleveragents/tui-state.yaml:

last_persona: "default"

This is loaded on startup to restore the last active persona.