Files
cleveragents-core/docs/adr/ADR-044-tui-architecture-and-framework.md
T
freemo 56f3b49725 docs(tui): add comprehensive TUI specification with 18 aligned ASCII mockup diagrams
Add the complete TUI (Terminal User Interface) specification section to
docs/specification.md, covering architecture, layout, navigation, persona
system, reference picker, slash commands, shell mode, session management,
and keyboard shortcuts. The section includes 18 HTML-styled ASCII mockup
diagrams using the Dracula color palette that render with actual colors
in MkDocs.

Key specification areas:
- MainScreen layout with right-side collapsible sidebar (3 states:
  hidden, visible, fullscreen cycled by shift+tab)
- Direct-to-chat launch with first-run actor selection overlay
- Persona system bundling actor + arguments + project/plan scope,
  cycled with tab; argument presets cycled with ctrl+tab
- @ reference picker with fuzzy search for projects/plans/resources
- / slash commands mirroring CLI noun:verb patterns (session:new,
  plan:list, project:show, actor:switch, etc.)
- ! shell mode for direct command execution
- Multi-session tabs with independent personas and conversations
- Escape-cascading navigation back to main screen
- Plan detail view with decision tree navigation
- Project detail view with resource DAG
- Permission approval workflow with diff view
- Complete keyboard shortcut reference table

New ADR files:
- ADR-044: TUI Architecture and Framework (Textual >= 1.0)
- ADR-045: TUI Persona System
- ADR-046: TUI Reference and Command System (CLI-aligned commands)

Also adds TUI mockup CSS classes to docs/stylesheets/extra.css for
Dracula-palette diagram rendering (.tui-primary, .tui-secondary,
.tui-success, .tui-warning, .tui-error, .tui-dim, .tui-bold,
.tui-bold-primary, .tui-bold-warning, .tui-primary-u, .tui-rainbow).
2026-03-11 00:39:57 -04:00

27 KiB
Raw Blame History

adr_number, title, status_history, tier, authors, superseded_by, related_adrs, acceptance
adr_number title status_history tier authors superseded_by related_adrs acceptance
44 TUI Architecture and Framework
2026-03-10
Proposed
Jeffrey Phillips Freeman
2026-03-10
Accepted
Jeffrey Phillips Freeman
2
Jeffrey Phillips Freeman
null
number title relationship
1 Layered Architecture TUI is a Presentation-layer component in the layered architecture
number title relationship
5 Technical Stack Adds Textual as an active dependency in the technical stack
number title relationship
21 CLI and Output Rendering TUI integrates with the Output Rendering Framework via TuiMaterializer
number title relationship
26 Agent Client Protocol (ACP) TUI communicates with the Application layer exclusively through ACP
number title relationship
20 Session Model TUI sessions map to domain Session entities
number title relationship
45 TUI Persona System Persona system is a TUI-specific concept defined in the companion ADR
number title relationship
46 TUI Reference and Command System Reference and command systems are TUI-specific input mechanisms
votes_for votes_against abstentions
voter comment
Jeffrey Phillips Freeman <Jeffrey.Freeman@CleverThis.com> Textual provides the reactive widget framework needed for real-time plan monitoring, multi-session management, and rich conversation rendering while enabling future Web and IDE reuse through a single UI codebase

Context

CleverAgents is architected as a multi-frontend system with five Presentation-layer surfaces — CLI, TUI, Web, IDE Plugin, and REST API — all communicating through ACP (ADR-026). The CLI is fully specified and partially implemented. The TUI is the second Presentation-layer surface to be designed and represents a significant advancement in user experience: it enables real-time plan monitoring, multi-session management, interactive decision tree navigation, and rich conversation with actors — capabilities that are impractical in a stateless CLI.

The existing Output Rendering Framework (ADR-021) was explicitly designed to support the TUI. The OutputSessionElementHandleMaterializationStrategy pipeline is format-agnostic; producer code writes to handles without knowing whether the active strategy renders to a Rich terminal, a Textual widget tree, or a JSON serializer. The TUI needs a TuiMaterializer that maps handle events to Textual widget operations.

The system operates on multiple codebases simultaneously. Unlike single-project tools, CleverAgents manages multiple projects and plans concurrently, with plans potentially spanning multiple projects. The TUI must surface this multi-project, multi-plan reality as a first-class concern — not as an afterthought bolted onto a single-directory file browser.

Additionally, the TUI must support real-time event streaming from the ACP event bus to provide live visibility into plan phase transitions, tool invocations, validation results, and actor reasoning — the same events available via SSE in server mode and RxPY observables in local mode.

Decision Drivers

  • The Output Rendering Framework (ADR-021) was designed with TUI integration as an explicit goal; the TuiMaterializer must fulfill this architectural promise without requiring changes to existing producer code
  • Real-time plan monitoring requires event-driven UI updates from the ACP event bus, not polling — the TUI must subscribe to domain events and reflect state changes immediately
  • The multi-project, multi-plan nature of CleverAgents demands a UI that shows project and plan state simultaneously alongside the conversation, not hidden behind navigation menus
  • Multiple concurrent sessions with different actors and scopes are a natural workflow for users managing several projects; the TUI must support session tabs with independent persona configurations
  • The Textual framework enables a "single UI codebase" strategy: the same widget tree can serve the standalone TUI, a Textual Web deployment, and an embedded IDE plugin — all three Presentation-layer surfaces from one implementation
  • Keyboard-driven workflows must be first-class; every operation must be achievable without a mouse, with discoverable hotkeys and consistent escape-to-return navigation

Decision

CleverAgents adopts Textual (>= 1.0) as the TUI framework, with the following architectural decisions:

  1. Direct-to-chat launch model — The TUI opens directly to the main chat screen (MainScreen). On first run, an actor selection overlay guides the user through initial setup. Subsequent launches restore the last active persona. There is no separate launcher/store screen.

  2. Right-side collapsible sidebar with three states — The sidebar is docked to the right edge (not left, to keep the conversation — the primary interaction surface — aligned with the terminal's natural reading position). It cycles through three states via shift+tab: hiddenvisible (partial, ~32 chars wide) → fullscreen (covers the conversation and takes input focus for plan/project/persona management). The fullscreen state is a dedicated management interface, not merely a wider sidebar.

  3. Multi-session tab bar — Multiple concurrent sessions are supported, each with an independent persona, conversation history, and ACP session binding. Session tabs appear at the top of the screen and are navigable with ctrl+[ / ctrl+]. Tabs auto-show when two or more sessions exist.

  4. TuiMaterializer integration — A new TuiMaterializer implementation of MaterializationStrategy maps ElementHandle events to Textual widget operations. Each handle type maps to a specific Textual widget (see Design section). This enables all existing CLI command producers to render in the TUI without modification.

  5. ACP event subscription — The TUI subscribes to ACP events (local mode: RxPY observables; server mode: SSE) for real-time updates to the sidebar plan/project panels, conversation stream, and notification system.

  6. Escape-cascading navigation — Pressing escape always moves toward the main screen: closing modals, dismissing overlays, exiting fullscreen sidebar, and eventually returning to the prompt. Multiple escape presses from any state eventually reach the main chat screen.

  7. Window-specific hotkey contexts — Each screen and focused widget defines its own hotkey bindings. The help panel (F1) dynamically reflects the current context's available hotkeys.

Design

Application Class and Screen Architecture

The root CleverAgentsApp(App) manages screen modes and global state:

CleverAgentsApp(App)
│
├── CSS_PATH = "cleveragents.tcss"  (global styles)
│
├── Screens:
│   ├── MainScreen          (per-session, dynamically created modes)
│   ├── SidebarFullScreen   (pushed when sidebar enters fullscreen)
│   ├── PlanDetailModal     (pushed from sidebar or conversation)
│   ├── ProjectDetailModal  (pushed from sidebar)
│   ├── PersonaEditorModal  (pushed from fullscreen sidebar)
│   ├── SettingsScreen      (pushed via F2 / ctrl+,)
│   ├── SessionsScreen      (pushed via ctrl+s)
│   └── PermissionsScreen   (pushed for tool permission requests)
│
├── Global State:
│   ├── SessionTracker      (active sessions, current session)
│   ├── PersonaRegistry     (loaded from ~/.config/cleveragents/personas/)
│   ├── Settings            (TUI configuration, reactive)
│   └── ACPClient           (local facade or HTTP transport)
│
└── Theme:
    └── Default: "dracula" (17+ Textual built-in themes supported)

MainScreen Layout

The MainScreen is the primary interface. It uses a horizontal layout with the conversation taking available space and the sidebar docked to the right:

┌─────────────────────────────────────────────────────┬──────────────────────┐
│ Throbber (height: 1, full width, visible when busy) │                      │
│ SessionTabs (height: auto, visible when ≥2 sessions)│                      │
├─────────────────────────────────────────────────────┤  SideBar             │
│                                                     │  (dock: right)       │
│  Conversation                                       │  (width: 32-40)      │
│  (flex: 1fr)                                        │  (max-width: 45%)    │
│                                                     │  ┌────────────────┐  │
│  ┌─ ContentsGrid ─────────────────────────────────┐ │  │ PlansPanel     │  │
│  │ Cursor │ Contents stream                       │ │  │ (collapsible)  │  │
│  │  (1ch) │  - Welcome / UserInput / ActorResponse│ │  │                │  │
│  │        │  - ToolCall / PlanProgress / DiffView │ │  ├────────────────┤  │
│  │        │  - TerminalEmbed / Note / Warning     │ │  │ ProjectsPanel  │  │
│  └────────┴───────────────────────────────────────┘ │  │ (collapsible)  │  │
│                                                     │  │                │  │
│  Flash (notification bar, height: 1)                │  └────────────────┘  │
│                                                     │                      │
│  ┌─ Prompt ───────────────────────────────────────┐ │                      │
│  │ ReferencePickerOverlay (overlay, triggered by @)│ │                      │
│  │ SlashCommandOverlay (overlay, triggered by /)   │ │                      │
│  │ ┌─ PromptContainer ─────────────────────────┐  │ │                      │
│  │ │ [] PromptTextArea                        │  │ │                      │
│  │ └───────────────────────────────────────────┘  │ │                      │
│  │ PersonaBar: name │ actor │ preset │ cost       │ │                      │
│  └─────────────────────────────────────────────────┘ │                      │
├─────────────────────────────────────────────────────┴──────────────────────┤
│ Footer: F1 Help │ shift+tab Sidebar │ tab Persona │ ctrl+q Quit           │
└────────────────────────────────────────────────────────────────────────────┘

Sidebar Three-State Behavior

The sidebar cycles through three states via shift+tab:

State Layout Input Focus Content
Hidden Sidebar has display: none; conversation takes full width Prompt retains focus No sidebar content visible
Visible Sidebar docked right, 32-40 chars wide (configurable) Prompt retains focus; ctrl+b focuses sidebar Plans and Projects panels in collapsible containers; read-only status display
Fullscreen Sidebar covers entire screen; MainScreen conversation hidden Sidebar takes input focus; typing goes to sidebar search/navigation, not the prompt Extended plan/project details, selection mode for persona building, persona cycle list management

State transitions:

Hidden ──shift+tab──► Visible ──shift+tab──► Fullscreen
  ▲                                              │
  └──────────────── escape (×1-2) ───────────────┘

In fullscreen mode, escape returns to visible; another escape returns to hidden. This cascading escape behavior is consistent across all TUI states.

TuiMaterializer — Output Rendering Framework Integration

The TuiMaterializer bridges the existing OutputSession / ElementHandle pipeline (ADR-021) to Textual widgets. Producer code is completely unaware of the TUI — the same command logic that renders rich tables and panels in the CLI renders interactive Textual widgets in the TUI.

ElementHandle Type Textual Widget Mapping Behavior
PanelHandle Static container with key-value Label pairs inside a Collapsible set_entry() → update/add label; remove_entry() → remove label; border styled by border_style parameter
TableHandle DataTable widget add_row()add_row() on DataTable; set_sort_key() → re-sort; set_summary() → footer row
TreeHandle Tree widget add_child()add_leaf() or add() on tree node; set_node_style() → update node classes
ProgressHandle ProgressBar (determinate) or custom Throbber (indeterminate) set_progress() → update bar; set_step_status() → update step indicator; increment() → advance bar
StatusHandle Label with semantic CSS class (-info, -warning, -error, -success) set_message() → update label text; set_level() → swap CSS class
CodeHandle Read-only TextArea with syntax highlighting via tree-sitter set_content() → replace text; set_language() → change grammar; set_highlight_lines() → update gutter markers
DiffHandle Custom DiffView widget supporting unified and side-by-side modes add_hunk() → append hunk to diff display; set_stats() → update stats header
SeparatorHandle Rule widget Auto-closed on creation; renders as horizontal rule
ActionHintHandle Static with muted text and command references Auto-closed; renders suggested next commands

The TuiMaterializer observes ElementEvent notifications from the OutputSession:

  • ElementCreated → instantiate the corresponding Textual widget and mount it in the conversation's Contents container
  • ElementUpdated → call the appropriate update method on the mounted widget (reactive binding ensures screen refresh)
  • ElementClosed → mark the widget as finalized (disable loading indicators, settle animations)
  • SessionEnd → flush any pending updates

ACP Event Subscription Architecture

The TUI subscribes to ACP events to provide real-time visibility into system activity:

ACPClient (local or server)
    │
    ├── events.subscribe(plan_id=*, event_type=*)
    │       │
    │       ├──► plan.phase_changed    ──► SideBar.PlansPanel: update phase indicator
    │       ├──► plan.state_changed    ──► SideBar.PlansPanel: update state badge
    │       ├──► plan.decision_made    ──► SideBar.PlansPanel: update depth counter
    │       │                              PlanDetailModal: add node to decision tree
    │       ├──► tool.invoked          ──► Conversation: mount ToolCall widget (pending)
    │       ├──► tool.completed        ──► Conversation: update ToolCall widget (done/fail)
    │       ├──► validation.completed  ──► Conversation: append validation result
    │       ├──► plan.apply_completed  ──► SideBar: update terminal state
    │       │                              Flash: show completion notification
    │       ├──► plan.error            ──► Flash: show error notification
    │       │                              SideBar: mark plan as errored
    │       └──► session.message       ──► Conversation: stream ActorResponse content
    │
    └── Local mode: RxPY Observable subscription
        Server mode: SSE connection to /api/v1/events/stream

Conversation Stream Architecture

The conversation displays a chronological stream of message blocks, each typed for its content:

Block Type Visual Treatment Source
Welcome ASCII art + first-run instructions in $text-success App startup (first message only)
UserInput Left border $secondary, 15% background tint, Markdown rendered User prompt submission
ActorResponse Streaming Markdown with syntax-highlighted code fences session.message events
ActorThought $primary-muted 20% background, max-height 10 lines (expandable), italic Actor thinking/reasoning (if exposed)
ToolCall Expandable: header with tool icon + status pill, collapsible content with DiffView/text/Markdown tool.invoked / tool.completed events
PlanProgress Grid layout with status icons per step Plan phase changes
DiffView Unified or side-by-side diff with syntax highlighting, line numbers Tool results containing diffs
TerminalEmbed Bordered terminal widget with $primary 50% border, green (success) or red (error) tint Shell commands or tool terminal output
ShellResult Left border $primary, 4% foreground background User shell command (!) output
Note Semantic styling: info ($primary), warning ($warning), error ($error) System notifications

The conversation uses a 2-column grid: a 1-character-wide cursor column (left) with a block cursor navigable via alt+up/alt+down, and the content stream (right). The block cursor provides keyboard-driven message navigation — pressing enter or space on a block expands/collapses it.

Prompt Architecture

The prompt area is docked to the bottom of the conversation and comprises:

  1. Overlay zoneReferencePickerOverlay and SlashCommandOverlay render as floating panels above the prompt, triggered by @ and / respectively
  2. PromptContainer — houses the PromptTextArea (syntax-highlighted, multi-line capable) with a mode-dependent prompt symbol:
    • — normal prompt mode (Markdown input)
    • $ — shell mode (activated by ! at position 0)
    • — multi-line mode (activated when input contains newlines)
  3. QuestionWidget — replaces the prompt during actor permission requests (allow/reject choices)
  4. PersonaBar — always visible below the prompt, showing: persona name, actor name/model, current argument preset name (from ctrl+tab cycling), scope indicator (number of scoped projects/plans), and cumulative session cost

Theme and Styling Architecture

The TUI uses Textual's CSS (TCSS) system with semantic color tokens:

Token Usage
$primary Active elements, sidebar borders, plan status indicators
$secondary User input highlights, prompt accent, info bar
$success Completed states, passed validations, welcome text
$error Failed states, error notifications, dangerous commands
$warning Caution states, cost warnings, non-rollbackable alerts
$text, $text-muted Primary and secondary text
$text-primary, $text-secondary Accent-colored text
$panel Panel/container backgrounds

Default theme is Dracula. All 17+ Textual built-in themes are supported and switchable via /theme <name> command or settings. The TCSS file hierarchy is:

src/cleveragents/tui/
├── cleveragents.tcss       (global app styles)
├── screens/
│   ├── main.tcss           (MainScreen layout and conversation)
│   ├── sidebar_full.tcss   (SidebarFullScreen)
│   ├── plan_detail.tcss    (PlanDetailModal)
│   ├── project_detail.tcss (ProjectDetailModal)
│   ├── persona_editor.tcss (PersonaEditorModal)
│   ├── settings.tcss       (SettingsScreen)
│   ├── sessions.tcss       (SessionsScreen)
│   └── permissions.tcss    (PermissionsScreen)
└── widgets/
    ├── conversation.tcss   (conversation stream widgets)
    ├── prompt.tcss         (prompt area styling)
    ├── sidebar.tcss        (sidebar panels)
    └── overlays.tcss       (reference picker, slash commands)

Throbber Design

A rainbow gradient animated bar spans the full width of the screen at the top, visible only when the actor is processing. The gradient cycles through:

#881177 → #aa3355 → #cc6666 → #ee9944 → #eedd00 → #99dd55 →
#44dd88 → #22ccbb → #00bbcc → #0099cc → #3366bb → #663399

Animation rate: 15fps. Height: 1 row. When idle, the throbber row collapses to zero height.

First-Run Experience

On first launch (no personas configured), the TUI opens to the MainScreen with a centered modal overlay for actor selection:

  1. The overlay lists all registered actors (discovered from the Actor Registry via ACP)
  2. The user selects an actor with arrow keys + enter, or types / to search
  3. Selection creates a default persona named "default" with the chosen actor and no scoped projects/plans
  4. The overlay dismisses and the user can begin chatting

On subsequent launches, the TUI restores the last active persona and session.

Multi-Session Management

Each session tab represents an independent domain Session (ADR-020):

  • Sessions have independent conversation histories
  • Each session binds to a persona (which can differ per session)
  • Session tabs show state indicators: (actor working), (awaiting user input), plain (idle)
  • ctrl+n creates a new session tab with the current persona
  • ctrl+w closes the current session tab (with confirmation if unsaved)
  • Session state is persisted; ctrl+r resumes a previous session from the sessions screen

Constraints

  • The TUI must communicate with the Application layer exclusively through ACP — no direct imports from Domain or Infrastructure layers (enforced by import-linter architecture tests)
  • All TUI functionality must be accessible via keyboard alone; mouse support is supplementary
  • The TUI must not block the main thread during ACP operations; all ACP calls are async
  • The TUI must handle both local mode (in-process ACP) and server mode (HTTPS ACP) transparently — the transport is selected at startup based on configuration
  • The TuiMaterializer must not require changes to existing OutputSession or ElementHandle interfaces — it is a drop-in MaterializationStrategy implementation
  • Persona definitions are stored locally in user configuration space (~/.config/cleveragents/personas/) and are not part of the domain model — they do not round-trip through ACP or persist in the database
  • The escape key must always navigate toward the main screen from any state; no modal or screen may trap the user without an escape path

Consequences

Positive

  • The Output Rendering Framework's TuiMaterializer validates the framework's design promise: existing CLI command producers render in the TUI with zero code changes
  • Real-time ACP event subscription provides immediate visibility into plan progress, eliminating the need to poll plan status from the CLI
  • The multi-session tab model enables parallel workflows — a user can monitor a long-running plan in one tab while chatting about a different project in another
  • The right-side sidebar keeps the conversation (primary interaction surface) left-aligned, matching natural reading direction and terminal cursor behavior
  • Textual's "single UI codebase" strategy means the TUI implementation also serves as the Web frontend (via Textual Web) and IDE plugin (embedded Textual) with minimal additional work
  • The three-state sidebar provides progressive disclosure: hidden for focused chat, visible for at-a-glance status, fullscreen for deep management operations

Negative

  • Adding Textual as a dependency increases the installation footprint; CLI-only users who never launch the TUI still carry the dependency (mitigated by making it an optional extra: pip install cleveragents[tui])
  • The TuiMaterializer must handle all ElementHandle types from day one — partial implementation would cause silent failures when CLI commands run in TUI context
  • Real-time event subscription creates a persistent connection (RxPY observable or SSE stream) that must be managed across session tab switches and app lifecycle

Risks

  • Textual's API is still evolving; pinning to >= 1.0 reduces but does not eliminate the risk of breaking changes in minor versions
  • Complex conversation streams (many tool calls, large diffs) may cause performance issues; the conversation should implement pruning (collapse old messages) when line count exceeds configurable thresholds
  • The three-state sidebar transition must be smooth and not cause layout thrashing — CSS transitions and careful height/width management are required

Alternatives Considered

Urwid — A mature terminal UI library with lower-level widget primitives. Rejected because it lacks Textual's reactive data binding, CSS-based styling, and the Textual Web pathway for future Web/IDE deployment. Would require significantly more custom widget development.

Prompt Toolkit — Excellent for input handling but not designed for full-screen multi-widget layouts. Would require combining with another library (e.g., Rich Live) for the conversation stream, resulting in a fragmented architecture. Rejected in favor of Textual's integrated approach.

Blessed / Blessed-Contrib (Python port) — Node.js-inspired terminal UI. Less Pythonic, smaller community, no web deployment path. Rejected.

Custom Rich Live application — Build the TUI using Rich's Live display and manual layout management. Feasible for simple displays but does not scale to the complexity needed (multi-screen, overlays, focus management, event handling). Rejected as insufficiently structured.

Compliance

  • Architecture tests: Import-linter rules verify that src/cleveragents/tui/ imports only from src/cleveragents/cli/output/ (for TuiMaterializer integration) and ACP client interfaces — never from Domain or Infrastructure layers
  • Widget tests: Each custom Textual widget has unit tests using Textual's pilot testing framework, verifying rendering, event handling, and reactive state updates
  • Integration tests: End-to-end tests launch the TUI in headless mode (Textual's run_test() context), simulate user input via pilot, and verify ACP operations are invoked correctly
  • Hotkey tests: Every documented hotkey binding is covered by a pilot test that simulates the keypress and verifies the expected action
  • Materialization tests: The TuiMaterializer is tested against the same OutputSession test fixtures used for RichMaterializer, PlainMaterializer, etc., verifying that all element handle types produce the expected Textual widgets
  • Accessibility tests: Automated checks verify that all interactive elements are keyboard-reachable and that focus order follows logical navigation paths
  • Performance tests: Conversation stream benchmarks verify that rendering remains responsive with 500+ message blocks (the pruning threshold)
  • Theme tests: Visual regression tests capture screenshots of key screens in multiple themes and compare against baselines