TUI Runtime
Agent Air separates the agent logic from its presentation layer, making the TUI entirely optional. Agents can run in the terminal with a full interactive interface, or headlessly via channels for server deployments and integrations. This flexibility lets you choose the right approach for your use case without rewriting your agent logic.
The TUI runtime handles all the complexity of terminal interaction: raw mode, screen management, keyboard input, mouse events, and rendering. It provides a polished user experience out of the box while remaining fully customizable through layouts, widgets, and themes.
When to Use the TUI
The TUI runtime provides a complete terminal interface with conversation display, text input, permission prompts, and theme support. It handles the event loop, keyboard input, and screen rendering so you can focus on your agent’s functionality rather than terminal programming details.
Beyond basic functionality, the TUI offers features that users expect from modern terminal applications: smooth scrolling, streaming text display, syntax-highlighted code blocks, and responsive layouts that adapt to terminal size changes. All of this comes ready to use with minimal configuration.
Choose the TUI when:
- Building interactive command-line tools
- Creating developer assistants that run in terminals
- Prototyping agents before deploying to production
- Building agents where users directly interact via keyboard
For server deployments, background processes, or integration into larger applications, see the Server Quickstart guide for the channel-based approach.
Enabling the TUI
The TUI is enabled through the into_tui() extension method on AgentAir. This method is provided by the TuiRunner extension trait and transforms your agent into a TUI-enabled runner. The transformation is lightweight and simply wraps your agent with the necessary terminal infrastructure.
This pattern keeps the core agent logic separate from presentation concerns. The same AgentAir instance that runs in TUI mode could alternatively run headlessly, making it easy to support multiple deployment scenarios from a single codebase.
use agent_air::AgentAir;
use agent_air::tui::TuiRunner;
let config = MyConfig::default();
let agent = AgentAir::with_config(&config)?;
// Transform into TUI runner and start
agent.into_tui().run()?;
The into_tui() method returns a TuiRunner that wraps your agent and provides additional configuration options before starting. This intermediate step lets you customize the TUI before entering the main loop.
TuiRunner Configuration
Before calling run(), you can configure various aspects of the TUI through the runner. Each configuration method returns the runner for method chaining, allowing you to set up multiple options in a fluent style.
Configuration at this stage affects how the TUI initializes. Layout and theme choices made here become the initial state when the application starts, though users can change themes at runtime through the theme picker.
agent
.into_tui()
.with_layout(LayoutTemplate::minimal())
.with_theme("dark")
.run()?;
Available Configuration
The runner provides methods for the most common configuration needs. Each method is optional; the TUI will use sensible defaults if you don’t specify anything.
| Method | Purpose |
|---|---|
with_layout(template) | Set the layout template |
with_theme(name) | Set the initial theme |
with_welcome(screen) | Configure the welcome screen |
The App Lifecycle
When run() is called, the TUI initializes the terminal, enters the main event loop, and handles cleanup on exit. Understanding this lifecycle helps when debugging issues, implementing custom widgets, or extending the TUI’s behavior.
The lifecycle follows a predictable pattern: setup resources, process events until exit, then clean up. This ensures the terminal is always restored to a usable state, even if the agent encounters errors during operation.
Initialization
The TUI performs several setup steps before entering the main loop. Each step must complete successfully for the application to start. Failures during initialization result in an error returned from run().
- Terminal setup - Enables raw mode, alternate screen, and mouse capture
- Theme initialization - Loads the configured theme or the default
- Widget registration - Moves registered widgets into the App
- Layout computation - Calculates initial widget positions
Main Loop
The event loop is the heart of the TUI runtime. It continuously processes events and re-renders the screen, creating the responsive feel users expect. The loop runs until the user quits, an unrecoverable error occurs, or the agent signals it should exit.
The loop processes three types of events:
- Key events - Distributed to widgets by priority, then to the key handler
- Mouse events - Passed to widgets that support mouse interaction
- Tick events - Trigger re-renders and animation updates
Each iteration renders the current state, then waits for the next event. This approach minimizes CPU usage while keeping the interface responsive.
Cleanup
When the agent exits, whether from user action, error, or programmatic signal, the TUI ensures the terminal is properly restored. This cleanup happens even if a panic occurs, preventing the common problem of terminals left in an unusable raw mode state.
- Terminal restore - Disables raw mode and returns to normal screen
- State persistence - Saves session state if configured
- Resource cleanup - Drops widgets and releases resources
Terminal Requirements
The TUI works with most modern terminal emulators, but some features depend on terminal capabilities. Terminals that lack certain features will still work, but the experience may be degraded. For example, a terminal without true color support will display approximated colors.
Testing your agent in your target terminal environment is recommended, especially if you’re using custom themes with specific RGB colors. The built-in themes use colors that work well across a wide range of terminals.
For the best experience, use a terminal that supports:
- 256 colors or true color - Required for theme colors to display correctly
- Unicode - Used for borders, spinners, and icons
- Mouse input - Optional, enables scrolling and clicking
Common terminals with full support include iTerm2, Alacritty, Kitty, Windows Terminal, and most Linux terminal emulators.
Integrating with the TUI
Your agent logic runs alongside the TUI through the controller. Messages flow between the LLM, tools, and the UI automatically. You don’t need to manage the event loop or rendering directly; the TUI handles these concerns and keeps the interface updated as your agent processes requests.
This separation means your agent code remains focused on its core functionality. Tool implementations, slash commands, and other customizations don’t need to know whether they’re running in TUI mode or headlessly.
Sending Messages to the UI
Controller events are converted to UI updates automatically. When the LLM starts streaming a response, the chat view begins displaying text character by character. When a tool executes, its status appears with appropriate indicators. Error conditions display with styled messages that stand out from normal content.
// These happen internally when the LLM responds
// - Streaming text appears in the chat view
// - Tool executions show with status indicators
// - Errors display with appropriate styling
Receiving User Input
User messages from the text input are passed to your agent’s message handler. The TUI manages the text buffer, cursor, and editing commands, delivering complete messages when the user submits them.
Beyond text entry, the TUI handles several input patterns that would otherwise require significant code to implement correctly.
// The TUI handles:
// - Text entry and editing
// - Slash command detection
// - Permission prompt responses
// - Question panel responses
Running Without the TUI
For headless operation, skip into_tui() and use the channel-based interface instead. This runs the same agent logic without any terminal UI, making it suitable for server deployments, batch processing, or integration into other applications.
The channel interface provides the same events you’d see in the TUI, just delivered through a channel rather than rendered to a screen. This makes it straightforward to build alternative frontends or process agent interactions programmatically.
use agent_air::AgentAir;
let config = MyConfig::default();
let (agent, event_rx) = AgentAir::with_config(&config)?
.with_event_channel();
// Process events from event_rx
// Send messages via agent.send_message()
See Server Quickstart for complete examples of headless operation.
Error Handling
TUI errors are returned from run() as AgentError variants. The TUI attempts to recover from transient errors when possible, but some conditions require the application to exit. Understanding these error types helps you provide appropriate feedback to users and implement recovery strategies.
Error handling in terminal applications has the additional complexity of needing to restore the terminal state before displaying error messages. The TUI handles this automatically, ensuring error output appears on a properly configured terminal.
| Error | Cause |
|---|---|
TerminalError | Failed to initialize or restore terminal |
ConfigError | Invalid configuration detected at startup |
LlmError | LLM connection or API failure |
Handle these errors appropriately for your deployment:
match agent.into_tui().run() {
Ok(()) => println!("Agent exited normally"),
Err(e) => eprintln!("Agent error: {}", e),
}
Complete Example
A minimal TUI agent with custom configuration demonstrates how the pieces fit together. This example creates an agent with explicit layout and theme choices, though you could omit these for default behavior.
The pattern shown here—configure, transform to TUI, run—is the standard approach for TUI agents. Most customization happens through widget registration and layout configuration rather than modifying the core flow.
use agent_air::{AgentAir, AgentConfig};
use agent_air::tui::{TuiRunner, LayoutTemplate};
struct MyConfig;
impl AgentConfig for MyConfig {
fn name(&self) -> &str { "my-agent" }
fn version(&self) -> &str { "1.0.0" }
// ... other required methods
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let config = MyConfig;
AgentAir::with_config(&config)?
.into_tui()
.with_layout(LayoutTemplate::standard())
.with_theme("dark")
.run()?;
Ok(())
}
This creates an agent with the standard layout, dark theme, and all default widgets enabled.
