Conversation Customization
The conversation view is the central UI element that displays the chat history between the user and the agent. Agent Air provides extensive customization options through the conversation factory pattern, which lets you configure how new conversations are created, styled, and initialized. Welcome screens, custom titles, and branded interfaces are all possible through these APIs.
The factory pattern decouples conversation creation from the rest of the application. When sessions are created or cleared, the factory produces a fresh ConversationView instance with your configured settings. This ensures consistent branding and behavior across all conversation instances.
ConversationViewFactory
The factory is a function that creates new ConversationView instances. It’s called when sessions are created or cleared.
pub type ConversationViewFactory = Box<dyn Fn() -> Box<dyn ConversationView> + Send + Sync>;
A factory is a boxed closure that:
- Takes no arguments
- Returns a boxed
ConversationView - Is
Send + Syncfor thread safety
Setting the Factory
Use set_conversation_factory() on AgentAir:
impl AgentAir {
pub fn set_conversation_factory<F>(&mut self, factory: F) -> &mut Self
where
F: Fn() -> Box<dyn ConversationView> + Send + Sync + 'static;
}
Example:
use agent_air::AgentAir;
use agent_air::tui::widgets::ChatView;
let mut agent = AgentAir::new(&MyConfig)?;
agent.set_conversation_factory(|| {
Box::new(ChatView::new().with_title("My Agent"))
});
Default Factory
If no factory is set, the App creates a default ChatView:
// Equivalent to the default behavior
agent.set_conversation_factory(|| {
Box::new(ChatView::new())
});
ConversationView Trait
The factory returns types implementing ConversationView. This trait defines the interface for displaying and managing conversation content.
pub trait ConversationView: Send + 'static {
// Message operations
fn add_user_message(&mut self, content: String);
fn add_assistant_message(&mut self, content: String);
fn add_system_message(&mut self, content: String);
// Streaming
fn append_streaming(&mut self, text: &str);
fn complete_streaming(&mut self);
fn discard_streaming(&mut self);
fn is_streaming(&self) -> bool;
// Tool messages
fn add_tool_message(&mut self, tool_use_id: &str, display_name: &str, display_title: &str);
fn update_tool_status(&mut self, tool_use_id: &str, status: ToolStatus);
// Scrolling
fn scroll_up(&mut self);
fn scroll_down(&mut self);
fn enable_auto_scroll(&mut self);
// Rendering
fn render(&mut self, frame: &mut Frame, area: Rect, theme: &Theme, pending_status: Option<&str>);
// Animation
fn step_spinner(&mut self);
// Session state
fn save_state(&self) -> Box<dyn Any + Send>;
fn restore_state(&mut self, state: Box<dyn Any + Send>);
fn clear(&mut self);
}
Customizing ChatView
The factory is typically used to configure ChatView, the built-in conversation implementation. ChatView supports titles, custom prefixes, and initial content.
use agent_air::tui::widgets::{ChatView, ChatViewConfig};
agent.set_conversation_factory(|| {
Box::new(
ChatView::with_config(ChatViewConfig::new()
.with_user_prefix("You: ")
.with_empty_message("Start a conversation...")
)
.with_title("Assistant")
)
});
ChatViewConfig Options
pub struct ChatViewConfig {
pub user_prefix: String,
pub assistant_prefix: String,
pub system_prefix: String,
pub default_title: String,
pub empty_message: String,
}
impl ChatViewConfig {
pub fn new() -> Self;
pub fn with_user_prefix(self, prefix: &str) -> Self;
pub fn with_assistant_prefix(self, prefix: &str) -> Self;
pub fn with_system_prefix(self, prefix: &str) -> Self;
pub fn with_default_title(self, title: &str) -> Self;
pub fn with_empty_message(self, msg: &str) -> Self;
}
Welcome Screens
Welcome screens are custom content displayed when the chat is empty. They provide branding, instructions, or getting-started information before the user sends their first message. Welcome screens are implemented as render functions that draw content when there are no messages.
RenderFn Type
Welcome screens use the RenderFn type:
pub type RenderFn = Box<dyn Fn(&mut Frame, Rect, &Theme) + Send + Sync>;
The function receives:
&mut Frame- The ratatui frame for renderingRect- The area to render within&Theme- The current theme for styling
with_initial_content Method
Set a welcome screen using with_initial_content() on ChatView:
impl ChatView {
pub fn with_initial_content(mut self, render: RenderFn) -> Self;
}
Basic Welcome Screen
A simple centered welcome message:
use agent_air::tui::widgets::{ChatView, RenderFn};
use ratatui::{
layout::Alignment,
text::{Line, Span},
widgets::Paragraph,
};
let welcome: RenderFn = Box::new(|frame, area, theme| {
let lines = vec![
Line::from(""),
Line::from("Welcome to MyAgent"),
Line::from(""),
Line::from("Type a message to get started."),
];
let paragraph = Paragraph::new(lines)
.alignment(Alignment::Center);
frame.render_widget(paragraph, area);
});
let chat = ChatView::new()
.with_initial_content(welcome);
Styled Welcome Screen
Apply theme colors and text styles:
use ratatui::style::{Color, Modifier, Style};
let welcome: RenderFn = Box::new(|frame, area, theme| {
let title_style = Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD);
let subtitle_style = Style::default()
.fg(Color::DarkGray);
let lines = vec![
Line::from(""),
Line::from(""),
Line::from(Span::styled("MyAgent", title_style)),
Line::from(Span::styled("v1.0.0", subtitle_style)),
Line::from(""),
Line::from("An AI-powered assistant"),
Line::from(""),
Line::from(Span::styled(
"Type /help for available commands",
subtitle_style
)),
];
let paragraph = Paragraph::new(lines)
.alignment(Alignment::Center);
frame.render_widget(paragraph, area);
});
Using Theme Colors
Access theme colors for consistent styling:
let welcome: RenderFn = Box::new(|frame, area, theme| {
let lines = vec![
Line::from(""),
Line::from(vec![
Span::styled("My", theme.user_prefix),
Span::styled("Agent", theme.assistant_text),
]),
Line::from(""),
Line::from(Span::styled(
"Ready to help.",
theme.system_prefix,
)),
];
let paragraph = Paragraph::new(lines)
.alignment(Alignment::Center);
frame.render_widget(paragraph, area);
});
ASCII Art Welcome
Display ASCII art or logos:
let welcome: RenderFn = Box::new(|frame, area, theme| {
let logo = r#"
__ __ _ _
| \/ |_ _ / \ __ _ ___ _ __ | |_
| |\/| | | | |/ _ \ / _` |/ _ \ '_ \| __|
| | | | |_| / ___ \ (_| | __/ | | | |_
|_| |_|\__, /_/ \_\__, |\___|_| |_|\__|
|___/ |___/
"#;
let lines: Vec<Line> = logo
.lines()
.map(|l| Line::from(Span::styled(l, Style::default().fg(Color::Cyan))))
.collect();
let paragraph = Paragraph::new(lines)
.alignment(Alignment::Center);
frame.render_widget(paragraph, area);
});
Dynamic Content
Include dynamic information like time-based greetings:
use chrono::Local;
let welcome: RenderFn = Box::new(|frame, area, theme| {
let now = Local::now();
let greeting = match now.hour() {
5..=11 => "Good morning",
12..=17 => "Good afternoon",
_ => "Good evening",
};
let lines = vec![
Line::from(""),
Line::from(format!("{}, welcome to MyAgent!", greeting)),
Line::from(""),
Line::from(format!("Today is {}", now.format("%A, %B %d"))),
];
let paragraph = Paragraph::new(lines)
.alignment(Alignment::Center);
frame.render_widget(paragraph, area);
});
Custom Title Rendering
Customize the title bar with with_title_renderer():
pub type TitleRenderFn = Box<dyn Fn(&str, &Theme) -> (Line<'static>, Line<'static>) + Send + Sync>;
impl ChatView {
pub fn with_title_renderer<F>(mut self, render: F) -> Self
where
F: Fn(&str, &Theme) -> (Line<'static>, Line<'static>) + Send + Sync + 'static;
}
The renderer returns a tuple of (left_line, right_line) for the title bar:
let chat = ChatView::new()
.with_title("MyAgent")
.with_title_renderer(|title, theme| {
let left = Line::from(vec![
Span::styled("// ", Style::default().fg(Color::Red)),
Span::styled(title, Style::default().bold()),
]);
let right = Line::from(Span::styled(
"v1.0.0",
Style::default().fg(Color::DarkGray),
));
(left, right)
});
Session State Preservation
The ConversationView trait includes methods for session switching. The factory is called when clear() needs a fresh view or when the App needs a new conversation instance.
// Save current state before switching
let state = conversation.save_state();
// Restore when switching back
conversation.restore_state(state);
// Clear for new session
conversation.clear();
Custom ConversationView
For complete control, implement ConversationView directly:
use agent_air::tui::widgets::{ConversationView, ToolStatus};
use ratatui::{layout::Rect, Frame};
use std::any::Any;
struct CustomChatView {
messages: Vec<(String, String)>, // (role, content)
streaming: Option<String>,
}
impl ConversationView for CustomChatView {
fn add_user_message(&mut self, content: String) {
self.messages.push(("user".into(), content));
}
fn add_assistant_message(&mut self, content: String) {
self.messages.push(("assistant".into(), content));
}
fn add_system_message(&mut self, content: String) {
self.messages.push(("system".into(), content));
}
fn append_streaming(&mut self, text: &str) {
if let Some(ref mut buffer) = self.streaming {
buffer.push_str(text);
} else {
self.streaming = Some(text.to_string());
}
}
fn complete_streaming(&mut self) {
if let Some(content) = self.streaming.take() {
self.add_assistant_message(content);
}
}
fn discard_streaming(&mut self) {
self.streaming = None;
}
fn is_streaming(&self) -> bool {
self.streaming.is_some()
}
fn add_tool_message(&mut self, _id: &str, name: &str, title: &str) {
self.messages.push(("tool".into(), format!("{}: {}", name, title)));
}
fn update_tool_status(&mut self, _id: &str, _status: ToolStatus) {}
fn scroll_up(&mut self) {}
fn scroll_down(&mut self) {}
fn enable_auto_scroll(&mut self) {}
fn render(&mut self, frame: &mut Frame, area: Rect, theme: &Theme, _pending: Option<&str>) {
// Custom rendering
}
fn step_spinner(&mut self) {}
fn save_state(&self) -> Box<dyn Any + Send> {
Box::new(self.messages.clone())
}
fn restore_state(&mut self, state: Box<dyn Any + Send>) {
if let Ok(messages) = state.downcast::<Vec<(String, String)>>() {
self.messages = *messages;
}
}
fn clear(&mut self) {
self.messages.clear();
self.streaming = None;
}
}
// Use in factory
agent.set_conversation_factory(|| {
Box::new(CustomChatView {
messages: Vec::new(),
streaming: None,
})
});
Complete Example
A branded agent with custom welcome screen and title rendering:
use agent_air::AgentAir;
use agent_air::tui::widgets::{ChatView, ChatViewConfig, RenderFn};
use agent_air::tui::themes::Theme;
use ratatui::{
layout::{Alignment, Constraint, Direction, Layout, Rect},
style::{Color, Modifier, Style},
text::{Line, Span},
widgets::Paragraph,
Frame,
};
fn create_branded_welcome() -> RenderFn {
Box::new(|frame: &mut Frame, area: Rect, theme: &Theme| {
// Center the content vertically
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Percentage(30),
Constraint::Min(10),
Constraint::Percentage(30),
])
.split(area);
let center = chunks[1];
let title_style = Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD);
let version_style = Style::default()
.fg(Color::DarkGray);
let command_style = Style::default()
.fg(Color::Yellow);
let lines = vec![
Line::from(vec![
Span::styled("Dev", title_style),
Span::styled("Agent", Style::default().fg(Color::Green).bold()),
]),
Line::from(Span::styled("v0.1.0", version_style)),
Line::from(""),
Line::from("Your AI-powered development assistant"),
Line::from(""),
Line::from(vec![
Span::raw("Commands: "),
Span::styled("/help", command_style),
Span::raw(" | "),
Span::styled("/clear", command_style),
Span::raw(" | "),
Span::styled("/themes", command_style),
]),
Line::from(""),
Line::from(Span::styled(
"Press Enter to send a message",
version_style,
)),
];
let paragraph = Paragraph::new(lines)
.alignment(Alignment::Center);
frame.render_widget(paragraph, center);
})
}
fn main() -> std::io::Result<()> {
let mut agent = AgentAir::new(&MyConfig)?;
agent.set_conversation_factory(|| {
Box::new(
ChatView::with_config(
ChatViewConfig::new()
.with_default_title("DevAgent")
.with_empty_message("") // Use custom welcome instead
)
.with_title("DevAgent")
.with_initial_content(create_branded_welcome())
.with_title_renderer(|title, _theme| {
let left = Line::from(vec![
Span::styled("// ", Style::default().fg(Color::Red)),
Span::styled(title, Style::default().bold()),
]);
let right = Line::from("");
(left, right)
})
)
});
agent.run()
} 