Skip to content

TUI Dashboard

Terminal window
mix annihilation

The TUI enters raw terminal mode using ANSI escape codes and Erlang’s :io module. It uses the alternate screen buffer and restores the terminal on exit.

The TUI is built from scratch — no TUI framework:

  • Terminal — Raw mode management, ANSI escape code primitives, screen buffer rendering, terminal dimension queries
  • Input — Pure function parser: transforms raw byte streams into structured key events (:char, :key, :ctrl, :fn, arrows, etc.)
  • Layout — Flexible panel layout system with row/column calculations
  • Mode — Behaviour for TUI modes (init, handle_key, handle_events, render, shortcuts)
  • ModeManager — Mode switching, state preservation, key dispatch, notification badges
  • EventBridge — Subscribes to system event topics, maps events to affected TUI panels, batches re-renders at 30fps (33ms intervals)
  1. System events arrive via Annihilation.Event
  2. EventBridge maps events to affected panels (e.g., "agent:streaming" -> [:overview, :focus])
  3. Dirty panels are batched and a render timer fires every 33ms
  4. ModeManager delegates rendering to the current mode
  5. Mode returns a list of terminal cells: {row, col, text, attrs_map}
  6. Terminal.render/1 converts cells to ANSI escape code iodata
  7. Single IO.write/1 call for flicker-free output

Six modes, switchable via Tab (cycle) or number keys 1-6 (direct jump):

Split-panel view showing all psychonauts and their activity.

  • Left panel: Psychonaut list with status indicators (phase, turn count)
  • Right panel: Real-time activity log from all psychonauts
  • Bottom: Live streaming preview of the selected psychonaut

Select a psychonaut with Up/Down, press Enter to switch to Focus mode.

Full conversation view for a single psychonaut:

  • Complete message history (user, assistant, tool calls, tool results)
  • Live streaming output with delta updates
  • Agent stats: turn count, model, phase, bead ID
  • Scrollable with Up/Down, PageUp/PageDown

Pending questions and drifts from psychonauts:

  • Question list: Priority indicators (critical/high/normal/low), agent ID, age
  • Question detail: Full text + context
  • Answer input: Type response, press Enter to send beacon
  • Drifted questions: Status badge showing what the psychonaut assumed
  • Drift actions: Ground (confirm), Reject (with reason), Note (comment)

Review generated code and pipeline mutations before they’re applied:

  • Grounding queue: Type indicators (NEW AGENT, NEW TOOL, PIPELINE)
  • Source code view: Full source with line numbers
  • Security scan results: CommandGuard classification of any shell commands
  • Actions: Approve (A), Edit (E), Reject (R)
  • Pipeline mutations: Shows proposed stages and matching rules

Bead management view:

  • Bead list with status, priority, type, and labels
  • Dependency visualization
  • Comment thread view
  • Create/update beads

Embedded command runner:

  • Command input with history
  • Output display with scrolling
  • Commands run through CommandGuard classification
  • Results visible to other modes for cross-referencing
KeyAction
TabCycle to next mode
1-6Jump to specific mode (when not in text input)
Ctrl+C / Ctrl+QQuit
Ctrl+LForce re-render
KeyModeAction
Up/DownAllNavigate lists
EnterOverviewFocus on selected psychonaut
EnterTetherSubmit answer to question
EscFocusReturn to Overview
AGroundingApprove (ground)
EGroundingEdit before approving
RGroundingReject
PageUp/PageDownFocusScroll conversation

EventBridge maintains a mapping of event topics to affected TUI panels:

# Topic -> affected panels
"agent:started" -> [:overview, :focus]
"agent:streaming" -> [:overview, :focus]
"agent:done" -> [:overview, :focus]
"burst:started" -> [:overview, :status_bar]
"burst:completed" -> [:overview, :status_bar]
"tether:reaching" -> [:tether, :status_bar]
"tether:drifts" -> [:tether, :grounding]
"keeper:bead_created" -> [:beads]
"system" -> [:overview, :status_bar]

When events arrive for a mode that is not currently active, the EventBridge increments a notification counter. The status bar shows badge counts next to mode names:

[1:Overview] [2:Focus] [3:Tether(2)] [4:Grounding(1)] [5:Beads] [6:Shell]

Switching to a mode clears its notification count.

Events are buffered and rendered at most every 33ms (30fps). This prevents flicker from rapid event bursts during active streaming. The buffer is capped at 100 events.

The bottom row displays:

  • Current mode name (highlighted)
  • All mode tabs with notification badges
  • Contextual keyboard shortcuts for the current mode
  • System status (burst phase, active agent count)

Custom modes implement Annihilation.TUI.Mode:

@callback init(term_size()) :: state()
@callback init(term_size(), term()) :: state() # Optional, for data from mode switch
@callback handle_key(key_event(), state()) ::
{:ok, state()} | {:switch_mode, atom(), term()}
@callback handle_events([map()], state()) :: state()
@callback render(state()) :: [Terminal.cell()]
@callback input_active?(state()) :: boolean() # Block number-key mode switching
@callback shortcuts() :: [{String.t(), String.t()}] # Status bar help text

Helper functions in the Mode module: truncate/2, word_wrap/2, pad_right/2, relative_time/1.