Component Architecture
The TUI uses a composable component architecture built on the Oil renderer — a React-like immediate-mode UI system with flexbox layout (via taffy).
Core Concepts
Section titled “Core Concepts”Oil Renderer
Section titled “Oil Renderer”Oil is Crucible’s terminal rendering library, providing:
- Immediate-mode rendering — UI rebuilt each frame from state
- Node-based composition —
Nodetree defines UI structure - Flexbox layout — Powered by taffy for CSS-like layouts
- Streaming graduation — Content transitions from viewport to stdout
// Basic Oil node constructionlet view = Node::col(vec![ Node::text("Hello").bold(), Node::row(vec![ Node::badge("OK").fg(Color::Green), Node::spacer(), Node::text("Status"), ]),]);Rendering Pipeline
Section titled “Rendering Pipeline”State Change → Node Tree → Layout (taffy) → Render (buffer) → Terminal Output- State Change: User input or agent event modifies
InkChatAppstate - Node Tree:
view()method builds node tree from current state - Layout: Taffy calculates flex positions and sizes
- Render: Nodes render to terminal buffer with styles
- Output: Diff algorithm writes minimal changes to terminal
Components
Section titled “Components”Components are modules in crates/crucible-cli/src/tui/oil/components/:
MessageList
Section titled “MessageList”Renders conversation history with markdown formatting.
Location: components/message_list.rs
Features:
- Markdown rendering with syntax highlighting
- Tool call display with collapsible results
- Streaming content with spinner
- Thinking block display (toggle with
Alt+T)
InputArea
Section titled “InputArea”Text input with readline-style editing.
Location: components/input_area.rs
Features:
- Multi-line input with cursor
- Emacs keybindings (Ctrl+A/E/W/U/K)
- Command prefix detection (
/,@,[[,:,!)
StatusBar
Section titled “StatusBar”Mode indicator and status display.
Location: components/status_bar.rs
Features:
- Mode display (Normal, Plan, Auto)
- Model name and token count
- Notification display
- Thinking token count (when visible)
PopupOverlay
Section titled “PopupOverlay”Autocomplete popup for commands and references.
Location: components/popup_overlay.rs
Features:
- Fuzzy filtering
- Keyboard navigation
- Multiple popup kinds (Command, File, Agent, Note, Model)
Graduation System
Section titled “Graduation System”The TUI uses a two-layer “graduation” system for streaming content:
Viewport Graduation
Section titled “Viewport Graduation”Static content graduates from the viewport to terminal stdout (scrollback):
┌─────────────────────────┐│ STDOUT (graduated) │ ← Historical content, no longer re-rendered├─────────────────────────┤│ ││ VIEWPORT (active) │ ← Live content, re-rendered each frame│ │└─────────────────────────┘Key files:
graduation.rs—GraduationState,plan_graduation(),commit_graduation()planning.rs—FramePlannerorchestrates graduation-first rendering
Streaming Graduation
Section titled “Streaming Graduation”Within the viewport, streaming content graduates block-by-block:
pub struct StreamingBuffer { graduated_blocks: Vec<String>, // Completed paragraphs in_progress: String, // Active streaming text}Paragraphs (split at \n\n) graduate to graduated_blocks while new content streams into in_progress.
Key files:
viewport_cache.rs—ViewportCache,StreamingBuffer
Invariants
Section titled “Invariants”- XOR Placement — Content in stdout OR viewport, never both
- Monotonic — Graduated count never decreases
- Atomic — Graduation commits before viewport filtering (no flash)
ViewportCache
Section titled “ViewportCache”The ViewportCache manages conversation state for efficient rendering:
pub struct ViewportCache { items: VecDeque<CachedChatItem>, graduated_ids: HashSet<String>, streaming: Option<StreamingBuffer>,}Key methods:
push_message()— Add user/assistant messagepush_tool_call()/push_tool_result()— Tool call lifecyclemark_graduated()— Track graduated contentungraduated_items()— Iterator for viewport rendering
Event Flow
Section titled “Event Flow”Events propagate through the system:
Terminal Event ↓ChatRunner::handle_event() ↓InkChatApp::update() → ChatAppMsg ↓State mutation + view rebuild ↓Terminal::draw()ChatAppMsg
Section titled “ChatAppMsg”High-level messages that modify app state:
pub enum ChatAppMsg { SendMessage(String), StreamChunk(String), ToolCallStart { id, name }, ToolCallComplete { id, result }, SetModel(String), SetThinkingBudget(Option<u32>), ToggleThinking, // ...}Theming
Section titled “Theming”The theme.rs module defines consistent styling:
pub struct Theme { pub user_prefix: Style, pub assistant_prefix: Style, pub thinking_border: Style, pub tool_name: Style, // ...}Access via Theme::default() or configure via :set commands.
Testing
Section titled “Testing”Components are tested via:
- Unit tests — State transitions, event handling
- Snapshot tests — Visual output with insta
- Property tests — Graduation invariants with proptest
Example snapshot test:
#[test]fn test_status_bar_normal_mode() { let app = InkChatApp::default(); let node = app.view(); insta::assert_snapshot!(render_to_string(&node, 80, 24));}See Also
Section titled “See Also”- Index — TUI overview
- Keybindings — Keyboard shortcuts
- Commands — REPL commands (
:set,:model, etc.) - Scripted UI — Lua/Fennel UI building with
cru.oil