Files
cleveragents-core/docs/api/tui.md
T
HAL9000 c0e9d59b79 docs: update TUI persona schema, add UKO vocabulary extensions module guide, add ACMS to API index
- docs/api/tui.md: Fix Persona dataclass to reflect actual Pydantic model with
  all fields (icon, greeting, color, cycle_order, base_arguments, scoped_projects,
  scoped_plans). Add PersonaPreset docs and effective_arguments() method.
  Add txt export format to /session:export and CLI session export commands.
- docs/api/index.md: Add cleveragents.acms entry linking to reference/acms.md.
- docs/modules/uko-vocabulary-extensions.md: New module guide covering Layer 2
  paradigm vocabularies (uko-oo, uko-func, uko-proc) and Layer 3 technology
  vocabularies (uko-py, uko-ts, uko-rs, uko-java) with usage examples,
  DetailLevelMap inheritance, and VocabularyRegistry API.
- mkdocs.yml: Add Module Guides nav section with shell-safety, uko-provenance,
  and uko-vocabulary-extensions pages.
2026-04-28 09:25:08 +00:00

15 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

Pydantic model persisted as a YAML file in ~/.config/cleveragents/personas/.

from cleveragents.tui.persona import Persona, PersonaPreset

class Persona(BaseModel):
    name: str                          # unique identifier; no path/control chars
    description: str = ""             # human-readable description
    icon: str = ""                     # emoji or single char shown in tab labels
    actor: str                         # namespaced actor ref, e.g. "openai/gpt-4o"
    color: str | None = None           # optional accent colour for the persona bar
    base_arguments: dict = {}          # default argument overrides applied to all presets
    scoped_projects: list[str] = []    # project names this persona is scoped to
    scoped_plans: list[str] = []       # plan IDs this persona is scoped to
    argument_presets: list[PersonaPreset] = []  # named argument override presets
    cycle_order: int = 0               # position in Ctrl+T cycling order
    greeting: str = ""                 # message shown when persona is activated

Invariants enforced by validators:

  • actor must be namespaced (namespace/name format).
  • name must not contain /, \, .., null bytes, or control characters.
  • argument_presets always contains exactly one preset named "default" with empty overrides. If none is provided, a default preset is created automatically.

PersonaPreset

class PersonaPreset(BaseModel):
    name: str      # preset identifier (e.g. "default", "verbose")
    display: str   # human-readable label shown in the UI
    overrides: dict = {}  # argument overrides merged on top of base_arguments

Persona.effective_arguments(preset_name=None) → dict

Computes the effective argument dict for a given preset using shallow merge:

persona = registry.get("my-persona")

# Base arguments only (no preset)
args = persona.effective_arguments()

# Base arguments merged with "verbose" preset overrides
args = persona.effective_arguments("verbose")

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|txt] [path] Export session to JSON, Markdown, or plain text
/session:import <path> Import a session from a JSON file
# Export as canonical JSON (default; importable)
/session:export ~/my-session.json

# Export as Markdown transcript (human-readable, not importable)
/session:export --format md ~/my-session.md

# Export as plain-text transcript (for piping into other tools)
/session:export --format txt ~/my-session.txt

# Import a previously exported session
/session:import ~/my-session.json

Format comparison:

Format Importable Use case
json (default) Yes Backup, migration, round-trip
md No Sharing, documentation, human review
txt No Piping into other tools, plain-text processing

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

# Export as plain-text transcript (for piping into other tools)
agents session export --session-id <ID> --output session.txt --format txt

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