- 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.
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:
actormust be namespaced (namespace/nameformat).namemust not contain/,\,.., null bytes, or control characters.argument_presetsalways 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
Spaceto expand/collapse - Muted styling distinguishes thought blocks from regular messages
- Backed by
ThoughtBlockdomain model with configurablemax_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.