App Structure
This page documents the App struct, which is the central coordinator for the terminal user interface. The App manages widgets, handles events, coordinates with the controller, and orchestrates rendering.
Architecture Overview
┌─────────────────────────────────────────────────────────────────┐
│ App │
├─────────────────────────────────────────────────────────────────┤
│ Controller Communication │
│ ├── to_controller: Sender<ControllerInputPayload> │
│ └── from_controller: Receiver<UiMessage> │
├─────────────────────────────────────────────────────────────────┤
│ Widget System │
│ ├── widgets: HashMap<ID, Box<dyn Widget>> │
│ ├── widget_priority_order: Vec<ID> │
│ └── conversation_view: Box<dyn ConversationView> │
├─────────────────────────────────────────────────────────────────┤
│ Session State │
│ ├── session_id, model_name │
│ ├── context_used, context_limit │
│ └── user_turn_counter │
├─────────────────────────────────────────────────────────────────┤
│ Key Handling │
│ ├── key_handler: Box<dyn KeyHandler> │
│ └── exit_handler: Option<Box<dyn ExitHandler>> │
├─────────────────────────────────────────────────────────────────┤
│ Layout │
│ └── layout_template: LayoutTemplate │
└─────────────────────────────────────────────────────────────────┘
App Struct
The main application struct contains all TUI state:
pub struct App {
// Identification
agent_name: String,
version: String,
// Command system
commands: Vec<Box<dyn SlashCommand>>,
command_extension: Option<Box<dyn std::any::Any + Send>>,
// Controller communication
to_controller: Option<ToControllerTx>,
from_controller: Option<FromControllerRx>,
controller: Option<Arc<LLMController>>,
// Session state
session_id: i64,
user_turn_counter: i64,
model_name: String,
context_used: i64,
context_limit: i32,
// Widget management
pub widgets: HashMap<&'static str, Box<dyn Widget>>,
pub widget_priority_order: Vec<&'static str>,
// Conversation display
conversation_view: Box<dyn ConversationView>,
conversation_factory: ConversationViewFactory,
// Animation state
throbber_state: ThrobberState,
waiting_for_response: bool,
waiting_started: Option<Instant>,
animation_frame_counter: u8,
// Turn tracking
current_turn_id: Option<TurnId>,
executing_tools: HashSet<String>,
// UI configuration
layout_template: LayoutTemplate,
key_handler: Box<dyn KeyHandler>,
exit_handler: Option<Box<dyn ExitHandler>>,
}
Field Categories
Identification
| Field | Type | Purpose |
|---|---|---|
agent_name | String | Display name for the agent |
version | String | Version string for status bar |
Controller Communication
| Field | Type | Purpose |
|---|---|---|
to_controller | Sender<ControllerInputPayload> | Send user input to controller |
from_controller | Receiver<UiMessage> | Receive updates from controller |
controller | Arc<LLMController> | Direct controller reference |
Session State
| Field | Type | Purpose |
|---|---|---|
session_id | i64 | Current session identifier |
user_turn_counter | i64 | Tracks user message count |
model_name | String | Current LLM model name |
context_used | i64 | Tokens used in context |
context_limit | i32 | Maximum context tokens |
Widget Management
| Field | Type | Purpose |
|---|---|---|
widgets | HashMap<&'static str, Box<dyn Widget>> | Registered widgets by ID |
widget_priority_order | Vec<&'static str> | Key handling order |
conversation_view | Box<dyn ConversationView> | Chat display widget |
conversation_factory | ConversationViewFactory | Creates new views |
Processing State
| Field | Type | Purpose |
|---|---|---|
waiting_for_response | bool | Awaiting LLM response |
waiting_started | Option<Instant> | When waiting began |
executing_tools | HashSet<String> | Tools currently running |
current_turn_id | Option<TurnId> | Current conversation turn |
throbber_state | ThrobberState | Animation state |
animation_frame_counter | u8 | Frame counter for animations |
Initialization
The App uses a builder-style initialization:
pub fn with_config(config: AppConfig) -> Self {
// Initialize default theme
let theme_name = default_theme_name();
if let Some(theme) = get_theme(theme_name) {
init_theme(theme_name, theme);
}
// Use provided commands or defaults
let commands = config.commands.unwrap_or_else(default_commands);
// Create conversation factory
let default_factory: ConversationViewFactory = Box::new(|| {
Box::new(ChatView::new())
});
let mut app = Self {
agent_name: config.agent_name,
version: config.version,
commands,
widgets: HashMap::new(),
widget_priority_order: Vec::new(),
// ... other fields
};
// Register default widgets
app.register_widget(StatusBar::new());
app
}
AppConfig
Configuration options for App creation:
pub struct AppConfig {
pub agent_name: String,
pub version: String,
pub commands: Option<Vec<Box<dyn SlashCommand>>>,
pub key_handler: Option<Box<dyn KeyHandler>>,
pub exit_handler: Option<Box<dyn ExitHandler>>,
pub layout_template: Option<LayoutTemplate>,
pub conversation_factory: Option<ConversationViewFactory>,
pub status_bar_config: Option<StatusBarConfig>,
pub command_extension: Option<Box<dyn std::any::Any + Send>>,
}
Widget Registration
Widgets are registered and managed by ID:
impl App {
pub fn register_widget<W: Widget>(&mut self, widget: W) {
let id = widget.id();
self.widgets.insert(id, Box::new(widget));
self.rebuild_priority_order();
}
fn rebuild_priority_order(&mut self) {
let mut ordered: Vec<_> = self.widgets.iter()
.map(|(id, w)| (*id, w.priority()))
.collect();
ordered.sort_by(|a, b| b.1.cmp(&a.1)); // Descending priority
self.widget_priority_order = ordered.into_iter().map(|(id, _)| id).collect();
}
}
Widget Access
Access widgets by ID with type casting:
impl App {
pub fn widget<W: Widget + 'static>(&self, id: &str) -> Option<&W> {
self.widgets.get(id)
.and_then(|w| w.as_any().downcast_ref::<W>())
}
pub fn widget_mut<W: Widget + 'static>(&mut self, id: &str) -> Option<&mut W> {
self.widgets.get_mut(id)
.and_then(|w| w.as_any_mut().downcast_mut::<W>())
}
}
Controller Connection
Connect App to the LLM controller:
impl App {
pub fn connect_controller(
&mut self,
to_controller: ToControllerTx,
from_controller: FromControllerRx,
controller: Arc<LLMController>,
) {
self.to_controller = Some(to_controller);
self.from_controller = Some(from_controller);
self.controller = Some(controller);
}
}
Session Management
Update session state:
impl App {
pub fn set_session(
&mut self,
session_id: i64,
model_name: String,
context_limit: i32,
) {
self.session_id = session_id;
self.model_name = model_name;
self.context_limit = context_limit;
self.context_used = 0;
}
pub fn update_context(&mut self, used: i64, limit: i32) {
self.context_used = used;
self.context_limit = limit;
}
}
State Layers
The App maintains multiple state layers:
App State
├── Global State
│ ├── Theme (static RwLock)
│ ├── Agent name, version
│ └── Slash commands
│
├── Session State
│ ├── Current session ID
│ ├── Model name
│ ├── Context usage
│ └── Turn counter
│
├── Conversation State (per session)
│ ├── Message history
│ ├── Scroll position
│ └── Streaming buffer
│
├── Processing State
│ ├── Waiting for response
│ ├── Executing tools
│ └── Current turn ID
│
└── Widget State
├── Active/inactive status
├── Widget-specific data
└── Focus state
Built-in Widget IDs
pub mod widget_ids {
pub const CHAT_VIEW: &str = "chat_view";
pub const TEXT_INPUT: &str = "text_input";
pub const STATUS_BAR: &str = "status_bar";
pub const PERMISSION_PANEL: &str = "permission_panel";
pub const QUESTION_PANEL: &str = "question_panel";
pub const SLASH_POPUP: &str = "slash_popup";
pub const SESSION_PICKER: &str = "session_picker";
pub const THEME_PICKER: &str = "theme_picker";
}
Next Steps
- Event Loop - Main event processing loop
- Widget System - Widget management details
- Rendering Pipeline - Frame rendering
