---
adr_number: 44
title: TUI Architecture and Framework
status_history:
- - '2026-03-10'
  - Proposed
  - Jeffrey Phillips Freeman
- - '2026-03-10'
  - Accepted
  - Jeffrey Phillips Freeman
tier: 2
authors:
- Jeffrey Phillips Freeman
superseded_by: null
related_adrs:
- number: 1
  title: Layered Architecture
  relationship: TUI is a Presentation-layer component in the layered architecture
- number: 5
  title: Technical Stack
  relationship: Adds Textual as an active dependency in the technical stack
- number: 21
  title: CLI and Output Rendering
  relationship: TUI integrates with the Output Rendering Framework via TuiMaterializer
- number: 26
  title: Agent-to-Agent Protocol (A2A)
  relationship: TUI communicates with the Application layer exclusively through A2A
- number: 20
  title: Session Model
  relationship: TUI sessions map to domain Session entities
- number: 45
  title: TUI Persona System
  relationship: Persona system is a TUI-specific concept defined in the companion ADR
- number: 46
  title: TUI Reference and Command System
  relationship: Reference and command systems are TUI-specific input mechanisms
acceptance:
  votes_for:
  - voter: Jeffrey Phillips Freeman <Jeffrey.Freeman@CleverThis.com>
    comment: 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
  votes_against: []
  abstentions: []
---
## Context

CleverAgents is architected as a multi-frontend system with five Presentation-layer surfaces — CLI, TUI, Web, IDE Plugin, and A2A Server Endpoint — all communicating through A2A (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 `OutputSession` → `ElementHandle` → `MaterializationStrategy` 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 A2A event bus to provide live visibility into plan phase transitions, tool invocations, validation results, and actor reasoning — the same events available via A2A `TaskStatusUpdateEvent` / `TaskArtifactUpdateEvent` notifications 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 A2A 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`: **hidden** → **visible** (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 A2A Task 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. **A2A event subscription** — The TUI subscribes to A2A events (local mode: RxPY observables; server mode: A2A `TaskStatusUpdateEvent` / `TaskArtifactUpdateEvent` via 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)
│   └── A2AClient           (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

### A2A Event Subscription Architecture

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

```
A2AClient (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: A2A TaskStatusUpdateEvent / TaskArtifactUpdateEvent via SSE
```

### 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 zone** — `ReferencePickerOverlay` 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. **PermissionQuestionWidget** — renders **inline in the conversation stream** for single-file permission requests. Displays the actor name, operation type, target file path, and four decision options with keyboard shortcuts (`a`/`A`/`r`/`R`). Press `v` to open the full `PermissionsScreen` for multi-file diff review. For multi-file operations, `PermissionsScreen` is pushed directly (no inline widget).
4. **QuestionWidget** — replaces the prompt during actor permission requests for non-file-specific choices (allow/reject)
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 A2A)
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 A2A — 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 A2A operations; all A2A calls are async
- The TUI must handle both local mode (in-process A2A) and server mode (HTTPS A2A) 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 A2A 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 A2A 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 A2A SSE notification 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 A2A 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 A2A 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
