Skip to main content

agent_core_tui/
runner.rs

1//! TUI Runner - Provides the run() method for AgentCore
2//!
3//! This module extends AgentCore with TUI-specific functionality,
4//! including widget registration, layout configuration, and the main
5//! TUI event loop.
6
7use std::io;
8
9use agent_core_runtime::agent::AgentCore;
10
11use crate::app::{App, AppConfig};
12use crate::commands::SlashCommand;
13use crate::keys::{DefaultKeyHandler, ExitHandler, KeyBindings, KeyHandler};
14use crate::layout::LayoutTemplate;
15use crate::widgets::{widget_ids, ConversationView, ConversationViewFactory, SessionInfo, Widget};
16
17/// TUI configuration that extends AgentCore with TUI-specific settings.
18///
19/// This struct holds all TUI-related configuration that would otherwise
20/// need to be stored on AgentCore. Use `TuiRunner::new()` to create one
21/// from an AgentCore, configure it, then call `run()`.
22pub struct TuiRunner {
23    /// The underlying agent core
24    agent: AgentCore,
25
26    /// Factory for creating conversation views
27    conversation_factory: Option<ConversationViewFactory>,
28
29    /// Widgets to register with the App
30    widgets_to_register: Vec<Box<dyn Widget>>,
31
32    /// Layout template for the TUI
33    layout_template: Option<LayoutTemplate>,
34
35    /// Key handler for customizable key bindings
36    key_handler: Option<Box<dyn KeyHandler>>,
37
38    /// Exit handler for cleanup before quitting
39    exit_handler: Option<Box<dyn ExitHandler>>,
40
41    /// Slash commands (None means use defaults)
42    commands: Option<Vec<Box<dyn SlashCommand>>>,
43
44    /// Extension data available to commands
45    command_extension: Option<Box<dyn std::any::Any + Send>>,
46
47    /// Custom status bar widget (replaces default if provided)
48    custom_status_bar: Option<Box<dyn Widget>>,
49
50    /// Whether to hide the default status bar
51    hide_status_bar: bool,
52}
53
54impl TuiRunner {
55    /// Create a new TuiRunner from an AgentCore.
56    pub fn new(agent: AgentCore) -> Self {
57        Self {
58            agent,
59            conversation_factory: None,
60            widgets_to_register: Vec::new(),
61            layout_template: None,
62            key_handler: None,
63            exit_handler: None,
64            commands: None,
65            command_extension: None,
66            custom_status_bar: None,
67            hide_status_bar: false,
68        }
69    }
70
71    /// Get a mutable reference to the underlying AgentCore.
72    pub fn agent_mut(&mut self) -> &mut AgentCore {
73        &mut self.agent
74    }
75
76    /// Get a reference to the underlying AgentCore.
77    pub fn agent(&self) -> &AgentCore {
78        &self.agent
79    }
80
81    /// Set the conversation view factory.
82    ///
83    /// The factory is called to create conversation views when sessions
84    /// are created or cleared. This allows customizing the chat view
85    /// with custom welcome screens, title renderers, etc.
86    pub fn set_conversation_factory<F>(&mut self, factory: F) -> &mut Self
87    where
88        F: Fn() -> Box<dyn ConversationView> + Send + Sync + 'static,
89    {
90        self.conversation_factory = Some(Box::new(factory));
91        self
92    }
93
94    /// Set the layout template for the TUI.
95    ///
96    /// This allows customizing how widgets are arranged in the terminal.
97    /// If not set, the default Standard layout with panels is used.
98    pub fn set_layout(&mut self, template: LayoutTemplate) -> &mut Self {
99        self.layout_template = Some(template);
100        self
101    }
102
103    /// Set a custom key handler for the TUI.
104    ///
105    /// This allows full control over key handling behavior.
106    pub fn set_key_handler<H: KeyHandler>(&mut self, handler: H) -> &mut Self {
107        self.key_handler = Some(Box::new(handler));
108        self
109    }
110
111    /// Set custom key bindings using the default handler.
112    ///
113    /// This is a simpler alternative to `set_key_handler` when you
114    /// only need to change which keys trigger which actions.
115    pub fn set_key_bindings(&mut self, bindings: KeyBindings) -> &mut Self {
116        self.key_handler = Some(Box::new(DefaultKeyHandler::new(bindings)));
117        self
118    }
119
120    /// Set an exit handler for cleanup before quitting.
121    ///
122    /// The exit handler's `on_exit()` method is called when the user
123    /// confirms exit. If it returns `false`, the exit is cancelled.
124    pub fn set_exit_handler<H: ExitHandler>(&mut self, handler: H) -> &mut Self {
125        self.exit_handler = Some(Box::new(handler));
126        self
127    }
128
129    /// Set the slash commands for this agent.
130    ///
131    /// If not called, uses the default command set.
132    pub fn set_commands(&mut self, commands: Vec<Box<dyn SlashCommand>>) -> &mut Self {
133        self.commands = Some(commands);
134        self
135    }
136
137    /// Set extension data available to custom commands.
138    ///
139    /// Commands can access this via `ctx.extension::<T>()`.
140    pub fn set_command_extension<T: std::any::Any + Send + 'static>(&mut self, ext: T) -> &mut Self {
141        self.command_extension = Some(Box::new(ext));
142        self
143    }
144
145    /// Register a widget with the agent.
146    ///
147    /// Widgets are registered before calling `run()` and will be available
148    /// in the TUI application.
149    pub fn register_widget<W: Widget>(&mut self, widget: W) -> &mut Self {
150        self.widgets_to_register.push(Box::new(widget));
151        self
152    }
153
154    /// Set a custom status bar widget to replace the default.
155    pub fn set_status_bar<W: Widget>(&mut self, status_bar: W) -> &mut Self {
156        self.custom_status_bar = Some(Box::new(status_bar));
157        self
158    }
159
160    /// Hide the default status bar.
161    pub fn hide_status_bar(&mut self) -> &mut Self {
162        self.hide_status_bar = true;
163        self
164    }
165
166    /// Run the agent with the TUI.
167    ///
168    /// This is the main entry point for running an agent with TUI. It:
169    /// 1. Starts background tasks (controller, input router)
170    /// 2. Creates an App with the configured settings
171    /// 3. Wires up all channels and registries
172    /// 4. Creates an initial session if LLM providers are configured
173    /// 5. Runs the TUI event loop
174    /// 6. Shuts down cleanly when the user quits
175    pub fn run(mut self) -> io::Result<()> {
176        let name = self.agent.name().to_string();
177        tracing::info!("{} starting", name);
178
179        // Start background tasks (controller, input router)
180        self.agent.start_background_tasks();
181
182        // Create App with our configuration
183        let app_config = AppConfig {
184            agent_name: name.clone(),
185            version: self.agent.version().to_string(),
186            commands: self.commands.take(),
187            command_extension: self.command_extension.take(),
188            error_no_session: self.agent.error_no_session().map(|s| s.to_string()),
189            ..Default::default()
190        };
191        let mut app = App::with_config(app_config);
192
193        // Set conversation factory if provided
194        if let Some(factory) = self.conversation_factory.take() {
195            app.set_conversation_factory(move || factory());
196        }
197
198        // Handle status bar customization
199        if self.hide_status_bar {
200            // Remove the default status bar
201            app.widgets.remove(widget_ids::STATUS_BAR);
202        } else if let Some(custom_status_bar) = self.custom_status_bar.take() {
203            // Replace default status bar with custom one
204            app.widgets.insert(widget_ids::STATUS_BAR, custom_status_bar);
205        }
206
207        // Register widgets with the App
208        for widget in self.widgets_to_register.drain(..) {
209            let id = widget.id();
210            app.widgets.insert(id, widget);
211        }
212        app.rebuild_priority_order();
213
214        // Wire up channels, controller, and registries to the App
215        app.set_to_controller(self.agent.to_controller_tx());
216        if let Some(rx) = self.agent.take_from_controller_rx() {
217            app.set_from_controller(rx);
218        }
219        app.set_controller(self.agent.controller().clone());
220        app.set_runtime_handle(self.agent.runtime_handle());
221        app.set_user_interaction_registry(self.agent.user_interaction_registry().clone());
222        app.set_permission_registry(self.agent.permission_registry().clone());
223
224        // Set layout template if specified
225        if let Some(layout) = self.layout_template.take() {
226            app.set_layout(layout);
227        }
228
229        // Set key handler if specified
230        if let Some(handler) = self.key_handler.take() {
231            app.set_key_handler_boxed(handler);
232        }
233
234        // Set exit handler if specified
235        if let Some(handler) = self.exit_handler.take() {
236            app.set_exit_handler_boxed(handler);
237        }
238
239        // Auto-create session if we have a configured LLM provider
240        match self.agent.create_initial_session() {
241            Ok((session_id, model, context_limit)) => {
242                let session_info = SessionInfo::new(session_id, model.clone(), context_limit);
243                app.add_session(session_info);
244                app.set_session_id(session_id);
245                app.set_model_name(&model);
246                app.set_context_limit(context_limit);
247                tracing::info!(
248                    session_id = session_id,
249                    model = %model,
250                    "Auto-created session on startup"
251                );
252            }
253            Err(e) => {
254                tracing::warn!(error = %e, "No initial session created");
255            }
256        }
257
258        // Pass LLM registry to app for creating new sessions
259        if let Some(registry) = self.agent.take_llm_registry() {
260            app.set_llm_registry(registry);
261        }
262
263        // Run the TUI (blocking)
264        let result = app.run();
265
266        // Shutdown the agent
267        self.agent.shutdown();
268
269        tracing::info!("{} stopped", name);
270        result
271    }
272}
273
274/// Extension trait to add TUI functionality to AgentCore.
275///
276/// This trait is implemented for AgentCore when the `tui` feature is enabled,
277/// providing a convenient `into_tui()` method.
278pub trait AgentCoreExt {
279    /// Convert this AgentCore into a TuiRunner for TUI operation.
280    fn into_tui(self) -> TuiRunner;
281}
282
283impl AgentCoreExt for AgentCore {
284    fn into_tui(self) -> TuiRunner {
285        TuiRunner::new(self)
286    }
287}