Skip to main content

agent_core/agent/
core.rs

1// AgentCore - Complete working agent out of the box
2//
3// Users call `AgentCore::new(config)` then `core.run()` and get a working chat agent.
4
5use std::io;
6use std::sync::Arc;
7
8use tokio::runtime::Runtime;
9use tokio::sync::mpsc;
10use tokio_util::sync::CancellationToken;
11
12use crate::controller::{
13    ControllerEvent, ControllerInputPayload, LLMController, LLMSessionConfig, LLMTool,
14    PermissionRegistry, ToolRegistry, UserInteractionRegistry,
15};
16
17use super::config::{load_config, AgentConfig, LLMRegistry};
18use super::error::AgentError;
19use super::logger::Logger;
20use super::messages::channels::DEFAULT_CHANNEL_SIZE;
21use super::messages::UiMessage;
22use super::router::InputRouter;
23
24use crate::tui::{App, AppConfig, DefaultKeyHandler, ExitHandler, KeyBindings, KeyHandler, LayoutTemplate, SessionInfo};
25use crate::tui::widgets::{Widget, ConversationView, ConversationViewFactory};
26
27/// Sender for messages from TUI to controller
28pub type ToControllerTx = mpsc::Sender<ControllerInputPayload>;
29/// Receiver for messages from TUI to controller
30pub type ToControllerRx = mpsc::Receiver<ControllerInputPayload>;
31/// Sender for messages from controller to TUI
32pub type FromControllerTx = mpsc::Sender<UiMessage>;
33/// Receiver for messages from controller to TUI
34pub type FromControllerRx = mpsc::Receiver<UiMessage>;
35
36/// AgentCore - A complete, working agent infrastructure.
37///
38/// AgentCore provides all the infrastructure needed for an LLM-powered agent:
39/// - Logging with tracing
40/// - LLM configuration loading
41/// - Tokio async runtime
42/// - LLMController for session management
43/// - Communication channels
44/// - User interaction and permission registries
45///
46/// # Basic Usage
47///
48/// ```ignore
49/// struct MyConfig;
50/// impl AgentConfig for MyConfig {
51///     fn config_path(&self) -> &str { ".myagent/config.yaml" }
52///     fn default_system_prompt(&self) -> &str { "You are helpful." }
53///     fn log_prefix(&self) -> &str { "myagent" }
54///     fn name(&self) -> &str { "MyAgent" }
55/// }
56///
57/// fn main() -> io::Result<()> {
58///     let mut core = AgentCore::new(&MyConfig)?;
59///     // Access channels and controller to wire up your TUI
60///     // then run your TUI loop
61///     Ok(())
62/// }
63/// ```
64pub struct AgentCore {
65    /// Logger instance (must be kept alive)
66    #[allow(dead_code)]
67    logger: Logger,
68
69    /// Agent name for display
70    name: String,
71
72    /// Agent version for display
73    version: String,
74
75    /// Factory for creating conversation views
76    conversation_factory: Option<ConversationViewFactory>,
77
78    /// Tokio runtime for async operations
79    runtime: Runtime,
80
81    /// The LLM controller
82    controller: Arc<LLMController>,
83
84    /// LLM provider registry (loaded from config)
85    llm_registry: Option<LLMRegistry>,
86
87    /// Sender for messages from TUI to controller
88    to_controller_tx: ToControllerTx,
89
90    /// Receiver for messages from TUI to controller (consumed by InputRouter)
91    to_controller_rx: Option<ToControllerRx>,
92
93    /// Sender for messages from controller to TUI (held by event handler)
94    #[allow(dead_code)]
95    from_controller_tx: FromControllerTx,
96
97    /// Receiver for messages from controller to TUI
98    from_controller_rx: Option<FromControllerRx>,
99
100    /// Cancellation token for graceful shutdown
101    cancel_token: CancellationToken,
102
103    /// User interaction registry for AskUserQuestions tool
104    user_interaction_registry: Arc<UserInteractionRegistry>,
105
106    /// Permission registry for AskForPermissions tool
107    permission_registry: Arc<PermissionRegistry>,
108
109    /// Tool definitions to register on sessions
110    tool_definitions: Vec<LLMTool>,
111
112    /// Widgets to register with the App
113    widgets_to_register: Vec<Box<dyn Widget>>,
114
115    /// Layout template for the TUI
116    layout_template: Option<LayoutTemplate>,
117
118    /// Key handler for customizable key bindings
119    key_handler: Option<Box<dyn KeyHandler>>,
120
121    /// Exit handler for cleanup before quitting
122    exit_handler: Option<Box<dyn ExitHandler>>,
123
124    /// Slash commands (None means use defaults)
125    commands: Option<Vec<Box<dyn crate::tui::commands::SlashCommand>>>,
126
127    /// Extension data available to commands
128    command_extension: Option<Box<dyn std::any::Any + Send>>,
129
130    /// Custom status bar widget (replaces default if provided)
131    custom_status_bar: Option<Box<dyn Widget>>,
132
133    /// Whether to hide the default status bar
134    hide_status_bar: bool,
135
136    /// Error message shown when user submits but no session exists
137    error_no_session: Option<String>,
138}
139
140impl AgentCore {
141    /// Create a new AgentCore with the given configuration.
142    ///
143    /// This initializes:
144    /// - Logging infrastructure
145    /// - LLM configuration from config file or environment
146    /// - Tokio runtime
147    /// - Communication channels
148    /// - LLMController
149    /// - User interaction and permission registries
150    pub fn new<C: AgentConfig>(config: &C) -> io::Result<Self> {
151        let logger = Logger::new(config.log_prefix())?;
152        tracing::info!("{} agent initialized", config.name());
153
154        // Load LLM configuration
155        let llm_registry = load_config(config);
156        if llm_registry.is_empty() {
157            tracing::warn!(
158                "No LLM providers configured. Set ANTHROPIC_API_KEY or create ~/{}",
159                config.config_path()
160            );
161        } else {
162            tracing::info!(
163                "Loaded {} LLM provider(s): {:?}",
164                llm_registry.providers().len(),
165                llm_registry.providers()
166            );
167        }
168
169        // Create tokio runtime for async operations
170        let runtime = Runtime::new().map_err(|e| {
171            io::Error::new(
172                io::ErrorKind::Other,
173                format!("Failed to create runtime: {}", e),
174            )
175        })?;
176
177        // Get channel buffer size from config (or use default)
178        let channel_size = config.channel_buffer_size().unwrap_or(DEFAULT_CHANNEL_SIZE);
179        tracing::debug!("Using channel buffer size: {}", channel_size);
180
181        // Create communication channels
182        let (to_controller_tx, to_controller_rx) =
183            mpsc::channel::<ControllerInputPayload>(channel_size);
184        let (from_controller_tx, from_controller_rx) =
185            mpsc::channel::<UiMessage>(channel_size);
186
187        // Create the controller with UI channel for direct event forwarding
188        // The controller will use backpressure: when UI channel is full, it stops
189        // reading from LLM, which backs up the from_llm channel, which blocks the
190        // session, which slows down network consumption.
191        let controller = Arc::new(LLMController::new(
192            Some(from_controller_tx.clone()),
193            Some(channel_size),
194        ));
195        let cancel_token = CancellationToken::new();
196
197        // Create channel for user interaction events
198        let (interaction_event_tx, mut interaction_event_rx) =
199            mpsc::channel::<ControllerEvent>(channel_size);
200
201        // Create the user interaction registry
202        let user_interaction_registry =
203            Arc::new(UserInteractionRegistry::new(interaction_event_tx));
204
205        // Spawn a task to forward user interaction events to the UI channel
206        // Uses blocking send for backpressure
207        let ui_tx_for_interactions = from_controller_tx.clone();
208        runtime.spawn(async move {
209            while let Some(event) = interaction_event_rx.recv().await {
210                let msg = convert_controller_event_to_ui_message(event);
211                if let Err(e) = ui_tx_for_interactions.send(msg).await {
212                    tracing::warn!("Failed to send user interaction event to UI: {}", e);
213                }
214            }
215        });
216
217        // Create channel for permission events
218        let (permission_event_tx, mut permission_event_rx) =
219            mpsc::channel::<ControllerEvent>(channel_size);
220
221        // Create the permission registry
222        let permission_registry = Arc::new(PermissionRegistry::new(permission_event_tx));
223
224        // Spawn a task to forward permission events to the UI channel
225        // Uses blocking send for backpressure
226        let ui_tx_for_permissions = from_controller_tx.clone();
227        runtime.spawn(async move {
228            while let Some(event) = permission_event_rx.recv().await {
229                let msg = convert_controller_event_to_ui_message(event);
230                if let Err(e) = ui_tx_for_permissions.send(msg).await {
231                    tracing::warn!("Failed to send permission event to UI: {}", e);
232                }
233            }
234        });
235
236        Ok(Self {
237            logger,
238            name: config.name().to_string(),
239            version: "0.1.0".to_string(),
240            conversation_factory: None,
241            runtime,
242            controller,
243            llm_registry: Some(llm_registry),
244            to_controller_tx,
245            to_controller_rx: Some(to_controller_rx),
246            from_controller_tx,
247            from_controller_rx: Some(from_controller_rx),
248            cancel_token,
249            user_interaction_registry,
250            permission_registry,
251            tool_definitions: Vec::new(),
252            widgets_to_register: Vec::new(),
253            layout_template: None,
254            key_handler: None,
255            exit_handler: None,
256            commands: None,
257            command_extension: None,
258            custom_status_bar: None,
259            hide_status_bar: false,
260            error_no_session: None,
261        })
262    }
263
264    /// Set the error message shown when user submits but no session exists.
265    ///
266    /// This overrides the default message "No active session. Use /new-session to create one."
267    ///
268    /// # Example
269    ///
270    /// ```ignore
271    /// agent.set_error_no_session("No configuration found in ~/.myagent/config.yaml");
272    /// ```
273    pub fn set_error_no_session(&mut self, message: impl Into<String>) -> &mut Self {
274        self.error_no_session = Some(message.into());
275        self
276    }
277
278    /// Set the agent version for display.
279    pub fn set_version(&mut self, version: impl Into<String>) {
280        self.version = version.into();
281    }
282
283    /// Load environment context into the system prompt.
284    ///
285    /// This adds information about the current execution environment to
286    /// all LLM session prompts:
287    /// - Current working directory
288    /// - Platform (darwin, linux, windows)
289    /// - OS version
290    /// - Today's date
291    ///
292    /// The context is wrapped in `<env>` tags and appended to the system prompt.
293    ///
294    /// # Example
295    ///
296    /// ```ignore
297    /// let mut core = AgentCore::new(&config)?;
298    /// core.load_environment_context();  // One line!
299    /// core.run()?;
300    /// ```
301    pub fn load_environment_context(&mut self) -> &mut Self {
302        if let Some(registry) = self.llm_registry.take() {
303            self.llm_registry = Some(registry.with_environment_context());
304            tracing::info!("Environment context loaded into system prompt");
305        }
306        self
307    }
308
309    /// Set the conversation view factory.
310    ///
311    /// The factory is called to create conversation views when sessions
312    /// are created or cleared. This allows customizing the chat view
313    /// with custom welcome screens, title renderers, etc.
314    ///
315    /// # Example
316    ///
317    /// ```ignore
318    /// agent.set_conversation_factory(|| {
319    ///     Box::new(ChatView::new()
320    ///         .with_title("My Agent")
321    ///         .with_initial_content(welcome_renderer))
322    /// });
323    /// ```
324    pub fn set_conversation_factory<F>(&mut self, factory: F) -> &mut Self
325    where
326        F: Fn() -> Box<dyn ConversationView> + Send + Sync + 'static,
327    {
328        self.conversation_factory = Some(Box::new(factory));
329        self
330    }
331
332    /// Set the layout template for the TUI.
333    ///
334    /// This allows customizing how widgets are arranged in the terminal.
335    /// If not set, the default Standard layout with panels is used.
336    ///
337    /// # Example
338    ///
339    /// ```ignore
340    /// // Use standard layout (default)
341    /// agent.set_layout(LayoutTemplate::standard());
342    ///
343    /// // Add a sidebar
344    /// agent.set_layout(LayoutTemplate::with_sidebar("file_browser", 30));
345    ///
346    /// // Minimal layout (no status bar)
347    /// agent.set_layout(LayoutTemplate::minimal());
348    /// ```
349    pub fn set_layout(&mut self, template: LayoutTemplate) -> &mut Self {
350        self.layout_template = Some(template);
351        self
352    }
353
354    /// Set a custom key handler for the TUI.
355    ///
356    /// This allows full control over key handling behavior. For simpler
357    /// customization where you just want to change which keys trigger
358    /// which actions, use [`Self::set_key_bindings`] instead.
359    ///
360    /// # Example
361    ///
362    /// ```ignore
363    /// struct VimKeyHandler { mode: VimMode }
364    /// impl KeyHandler for VimKeyHandler {
365    ///     fn handle_key(&mut self, key: KeyEvent, ctx: &KeyContext) -> AppKeyResult {
366    ///         // Implement vim-style modal editing
367    ///     }
368    /// }
369    ///
370    /// let mut agent = AgentCore::new(&config)?;
371    /// agent.set_key_handler(VimKeyHandler { mode: VimMode::Normal });
372    /// ```
373    pub fn set_key_handler<H: KeyHandler>(&mut self, handler: H) -> &mut Self {
374        self.key_handler = Some(Box::new(handler));
375        self
376    }
377
378    /// Set custom key bindings using the default handler.
379    ///
380    /// This is a simpler alternative to [`Self::set_key_handler`] when you
381    /// only need to change which keys trigger which actions.
382    ///
383    /// # Example
384    ///
385    /// ```ignore
386    /// // Use minimal bindings (Esc to quit, arrow keys only)
387    /// let mut agent = AgentCore::new(&config)?;
388    /// agent.set_key_bindings(KeyBindings::minimal());
389    ///
390    /// // Or customize specific bindings
391    /// let mut bindings = KeyBindings::emacs();
392    /// bindings.quit = vec![KeyCombo::key(KeyCode::Esc)];
393    /// agent.set_key_bindings(bindings);
394    /// ```
395    pub fn set_key_bindings(&mut self, bindings: KeyBindings) -> &mut Self {
396        self.key_handler = Some(Box::new(DefaultKeyHandler::new(bindings)));
397        self
398    }
399
400    /// Set an exit handler for cleanup before quitting.
401    ///
402    /// The exit handler's `on_exit()` method is called when the user
403    /// confirms exit. If it returns `false`, the exit is cancelled.
404    ///
405    /// # Example
406    ///
407    /// ```ignore
408    /// struct SaveOnExitHandler { session_file: PathBuf }
409    /// impl ExitHandler for SaveOnExitHandler {
410    ///     fn on_exit(&mut self) -> bool {
411    ///         self.save_session();
412    ///         true // proceed with exit
413    ///     }
414    /// }
415    ///
416    /// let mut agent = AgentCore::new(&config)?;
417    /// agent.set_exit_handler(SaveOnExitHandler { session_file: path });
418    /// ```
419    pub fn set_exit_handler<H: ExitHandler>(&mut self, handler: H) -> &mut Self {
420        self.exit_handler = Some(Box::new(handler));
421        self
422    }
423
424    /// Set the slash commands for this agent.
425    ///
426    /// If not called, uses the default command set.
427    ///
428    /// # Example
429    ///
430    /// ```ignore
431    /// use agent_core::tui::commands::{CommandRegistry, CustomCommand, CommandResult};
432    ///
433    /// agent.set_commands(
434    ///     CommandRegistry::with_defaults()
435    ///         .add(CustomCommand::new("deploy", "Deploy app", |args, ctx| {
436    ///             CommandResult::Message(format!("Deployed to {}", args))
437    ///         }))
438    ///         .remove("quit")
439    ///         .build()
440    /// );
441    /// ```
442    pub fn set_commands(
443        &mut self,
444        commands: Vec<Box<dyn crate::tui::commands::SlashCommand>>,
445    ) -> &mut Self {
446        self.commands = Some(commands);
447        self
448    }
449
450    /// Set extension data available to custom commands.
451    ///
452    /// Commands can access this via `ctx.extension::<T>()`.
453    ///
454    /// # Example
455    ///
456    /// ```ignore
457    /// struct MyContext { api_key: String }
458    ///
459    /// agent.set_command_extension(MyContext {
460    ///     api_key: "secret".to_string()
461    /// });
462    /// ```
463    pub fn set_command_extension<T: std::any::Any + Send + 'static>(
464        &mut self,
465        ext: T,
466    ) -> &mut Self {
467        self.command_extension = Some(Box::new(ext));
468        self
469    }
470
471    /// Register tools with the agent.
472    ///
473    /// The callback receives references to the tool registry and interaction registries,
474    /// and should return the tool definitions to register.
475    ///
476    /// # Example
477    ///
478    /// ```ignore
479    /// core.register_tools(|registry, user_reg, perm_reg| {
480    ///     tools::register_all_tools(registry, user_reg, perm_reg)
481    /// })?;
482    /// ```
483    pub fn register_tools<F>(&mut self, f: F) -> Result<(), AgentError>
484    where
485        F: FnOnce(
486            &Arc<ToolRegistry>,
487            &Arc<UserInteractionRegistry>,
488            &Arc<PermissionRegistry>,
489        ) -> Result<Vec<LLMTool>, String>,
490    {
491        let tool_defs = f(
492            self.controller.tool_registry(),
493            &self.user_interaction_registry,
494            &self.permission_registry,
495        )
496        .map_err(AgentError::ToolRegistration)?;
497        self.tool_definitions = tool_defs;
498        Ok(())
499    }
500
501    /// Register tools with the agent using an async function.
502    ///
503    /// Similar to `register_tools`, but accepts an async closure. The closure
504    /// is executed using the agent's tokio runtime via `block_on`.
505    ///
506    /// # Example
507    ///
508    /// ```ignore
509    /// core.register_tools_async(|registry, user_reg, perm_reg| async move {
510    ///     tools::register_all_tools(&registry, user_reg, perm_reg).await
511    /// })?;
512    /// ```
513    pub fn register_tools_async<F, Fut>(&mut self, f: F) -> Result<(), AgentError>
514    where
515        F: FnOnce(Arc<ToolRegistry>, Arc<UserInteractionRegistry>, Arc<PermissionRegistry>) -> Fut,
516        Fut: std::future::Future<Output = Result<Vec<LLMTool>, String>>,
517    {
518        let tool_defs = self.runtime.block_on(f(
519            self.controller.tool_registry().clone(),
520            self.user_interaction_registry.clone(),
521            self.permission_registry.clone(),
522        ))
523        .map_err(AgentError::ToolRegistration)?;
524        self.tool_definitions = tool_defs;
525        Ok(())
526    }
527
528    /// Register a widget with the agent.
529    ///
530    /// Widgets are registered before calling `run()` and will be available
531    /// in the TUI application.
532    ///
533    /// # Example
534    ///
535    /// ```ignore
536    /// let mut agent = AgentCore::new(&MyConfig)?;
537    /// agent.register_widget(PermissionPanel::new());
538    /// agent.register_widget(QuestionPanel::new());
539    /// agent.run()
540    /// ```
541    pub fn register_widget<W: Widget>(&mut self, widget: W) -> &mut Self {
542        self.widgets_to_register.push(Box::new(widget));
543        self
544    }
545
546    /// Set a custom status bar widget to replace the default.
547    ///
548    /// This will unregister the default status bar and register the custom one.
549    ///
550    /// # Example
551    ///
552    /// ```ignore
553    /// use agent_core::tui::{StatusBar, StatusBarConfig};
554    ///
555    /// let mut agent = AgentCore::new(&MyConfig)?;
556    /// let custom_status_bar = StatusBar::new()
557    ///     .with_renderer(|data, theme| {
558    ///         vec![Line::from(format!(" {} | {}", data.model_name, data.session_id))]
559    ///     });
560    /// agent.set_status_bar(custom_status_bar);
561    /// agent.run()
562    /// ```
563    pub fn set_status_bar<W: Widget>(&mut self, status_bar: W) -> &mut Self {
564        self.custom_status_bar = Some(Box::new(status_bar));
565        self
566    }
567
568    /// Hide the default status bar.
569    ///
570    /// This will unregister the default status bar widget. Useful for minimal layouts
571    /// or when you want to implement your own status display.
572    ///
573    /// # Example
574    ///
575    /// ```ignore
576    /// let mut agent = AgentCore::new(&MyConfig)?;
577    /// agent.hide_status_bar();
578    /// agent.run()
579    /// ```
580    pub fn hide_status_bar(&mut self) -> &mut Self {
581        self.hide_status_bar = true;
582        self
583    }
584
585    /// Start the controller and input router as background tasks.
586    ///
587    /// This must be called before sending messages or creating sessions.
588    /// After calling this, the controller is running and ready to accept input.
589    pub fn start_background_tasks(&mut self) {
590        tracing::info!("{} starting background tasks", self.name);
591
592        // Start the controller event loop in a background task
593        let controller = self.controller.clone();
594        self.runtime.spawn(async move {
595            controller.start().await;
596        });
597        tracing::info!("Controller started");
598
599        // Start the input router in a background task
600        if let Some(to_controller_rx) = self.to_controller_rx.take() {
601            let router = InputRouter::new(
602                self.controller.clone(),
603                to_controller_rx,
604                self.cancel_token.clone(),
605            );
606            self.runtime.spawn(async move {
607                router.run().await;
608            });
609            tracing::info!("InputRouter started");
610        }
611    }
612
613    /// Internal helper to create a session and configure tools.
614    async fn create_session_internal(
615        controller: &Arc<LLMController>,
616        config: LLMSessionConfig,
617        tools: &[LLMTool],
618    ) -> Result<i64, crate::client::error::LlmError> {
619        let id = controller.create_session(config).await?;
620
621        // Set tools on the session after creation
622        if !tools.is_empty() {
623            if let Some(session) = controller.get_session(id).await {
624                session.set_tools(tools.to_vec()).await;
625            }
626        }
627
628        Ok(id)
629    }
630
631    /// Create an initial session using the default LLM provider.
632    ///
633    /// Returns the session ID, model name, and context limit.
634    pub fn create_initial_session(&mut self) -> Result<(i64, String, i32), AgentError> {
635        let registry = self.llm_registry.as_ref().ok_or_else(|| {
636            AgentError::NoConfiguration("No LLM registry available".to_string())
637        })?;
638
639        let config = registry.get_default().ok_or_else(|| {
640            AgentError::NoConfiguration("No default LLM provider configured".to_string())
641        })?;
642
643        let model = config.model.clone();
644        let context_limit = config.context_limit;
645
646        let controller = self.controller.clone();
647        let tool_definitions = self.tool_definitions.clone();
648
649        let session_id = self.runtime.block_on(Self::create_session_internal(
650            &controller,
651            config.clone(),
652            &tool_definitions,
653        ))?;
654
655        tracing::info!(
656            session_id = session_id,
657            model = %model,
658            "Created initial session"
659        );
660
661        Ok((session_id, model, context_limit))
662    }
663
664    /// Create a session with the given configuration.
665    ///
666    /// Returns the session ID or an error.
667    pub fn create_session(&self, config: LLMSessionConfig) -> Result<i64, AgentError> {
668        let controller = self.controller.clone();
669        let tool_definitions = self.tool_definitions.clone();
670
671        self.runtime
672            .block_on(Self::create_session_internal(
673                &controller,
674                config,
675                &tool_definitions,
676            ))
677            .map_err(AgentError::from)
678    }
679
680    /// Signal shutdown to all background tasks and the controller.
681    pub fn shutdown(&self) {
682        tracing::info!("{} shutting down", self.name);
683        self.cancel_token.cancel();
684
685        let controller = self.controller.clone();
686        self.runtime.block_on(async move {
687            controller.shutdown().await;
688        });
689
690        tracing::info!("{} shutdown complete", self.name);
691    }
692
693    /// Run the agent with the default TUI.
694    ///
695    /// This is the main entry point for running an agent. It:
696    /// 1. Starts background tasks (controller, input router)
697    /// 2. Creates an App with the configured settings
698    /// 3. Wires up all channels and registries
699    /// 4. Creates an initial session if LLM providers are configured
700    /// 5. Runs the TUI event loop
701    /// 6. Shuts down cleanly when the user quits
702    ///
703    /// # Example
704    ///
705    /// ```ignore
706    /// fn main() -> io::Result<()> {
707    ///     let mut agent = AgentCore::new(&MyConfig)?;
708    ///     agent.run()
709    /// }
710    /// ```
711    pub fn run(&mut self) -> io::Result<()> {
712        tracing::info!("{} starting", self.name);
713
714        // Start background tasks (controller, input router)
715        self.start_background_tasks();
716
717        // Create App with our configuration
718        let app_config = AppConfig {
719            agent_name: self.name.clone(),
720            version: self.version.clone(),
721            commands: self.commands.take(),
722            command_extension: self.command_extension.take(),
723            error_no_session: self.error_no_session.take(),
724            ..Default::default()
725        };
726        let mut app = App::with_config(app_config);
727
728        // Set conversation factory if provided
729        if let Some(factory) = self.conversation_factory.take() {
730            app.set_conversation_factory(move || factory());
731        }
732
733        // Handle status bar customization
734        if self.hide_status_bar {
735            // Remove the default status bar
736            app.widgets.remove(crate::tui::widgets::widget_ids::STATUS_BAR);
737        } else if let Some(custom_status_bar) = self.custom_status_bar.take() {
738            // Replace default status bar with custom one
739            app.widgets.insert(crate::tui::widgets::widget_ids::STATUS_BAR, custom_status_bar);
740        }
741
742        // Register widgets with the App
743        for widget in self.widgets_to_register.drain(..) {
744            // We need to re-box as the App's register_widget expects impl Widget
745            let id = widget.id();
746            app.widgets.insert(id, widget);
747        }
748        app.rebuild_priority_order();
749
750        // Wire up channels, controller, and registries to the App
751        app.set_to_controller(self.to_controller_tx.clone());
752        if let Some(rx) = self.from_controller_rx.take() {
753            app.set_from_controller(rx);
754        }
755        app.set_controller(self.controller.clone());
756        app.set_runtime_handle(self.runtime.handle().clone());
757        app.set_user_interaction_registry(self.user_interaction_registry.clone());
758        app.set_permission_registry(self.permission_registry.clone());
759
760        // Set layout template if specified
761        if let Some(layout) = self.layout_template.take() {
762            app.set_layout(layout);
763        }
764
765        // Set key handler if specified
766        if let Some(handler) = self.key_handler.take() {
767            app.set_key_handler_boxed(handler);
768        }
769
770        // Set exit handler if specified
771        if let Some(handler) = self.exit_handler.take() {
772            app.set_exit_handler_boxed(handler);
773        }
774
775        // Auto-create session if we have a configured LLM provider
776        match self.create_initial_session() {
777            Ok((session_id, model, context_limit)) => {
778                let session_info = SessionInfo::new(session_id, model.clone(), context_limit);
779                app.add_session(session_info);
780                app.set_session_id(session_id);
781                app.set_model_name(&model);
782                app.set_context_limit(context_limit);
783                tracing::info!(
784                    session_id = session_id,
785                    model = %model,
786                    "Auto-created session on startup"
787                );
788            }
789            Err(e) => {
790                tracing::warn!(error = %e, "No initial session created");
791            }
792        }
793
794        // Pass LLM registry to app for creating new sessions
795        if let Some(registry) = self.llm_registry.take() {
796            app.set_llm_registry(registry);
797        }
798
799        // Run the TUI (blocking)
800        let result = app.run();
801
802        // Shutdown the agent
803        self.shutdown();
804
805        tracing::info!("{} stopped", self.name);
806        result
807    }
808
809    // ---- Accessors ----
810
811    /// Returns a sender for sending messages to the controller.
812    pub fn to_controller_tx(&self) -> ToControllerTx {
813        self.to_controller_tx.clone()
814    }
815
816    /// Takes the receiver for messages from the controller (can only be called once).
817    pub fn take_from_controller_rx(&mut self) -> Option<FromControllerRx> {
818        self.from_controller_rx.take()
819    }
820
821    /// Returns a reference to the controller.
822    pub fn controller(&self) -> &Arc<LLMController> {
823        &self.controller
824    }
825
826    /// Returns a reference to the runtime.
827    pub fn runtime(&self) -> &Runtime {
828        &self.runtime
829    }
830
831    /// Returns a handle to the runtime.
832    pub fn runtime_handle(&self) -> tokio::runtime::Handle {
833        self.runtime.handle().clone()
834    }
835
836    /// Returns a reference to the user interaction registry.
837    pub fn user_interaction_registry(&self) -> &Arc<UserInteractionRegistry> {
838        &self.user_interaction_registry
839    }
840
841    /// Returns a reference to the permission registry.
842    pub fn permission_registry(&self) -> &Arc<PermissionRegistry> {
843        &self.permission_registry
844    }
845
846    /// Returns a reference to the LLM registry.
847    pub fn llm_registry(&self) -> Option<&LLMRegistry> {
848        self.llm_registry.as_ref()
849    }
850
851    /// Takes the LLM registry (can only be called once).
852    pub fn take_llm_registry(&mut self) -> Option<LLMRegistry> {
853        self.llm_registry.take()
854    }
855
856    /// Returns the cancellation token.
857    pub fn cancel_token(&self) -> CancellationToken {
858        self.cancel_token.clone()
859    }
860
861    /// Returns the agent name.
862    pub fn name(&self) -> &str {
863        &self.name
864    }
865
866    /// Returns a clone of the UI message sender.
867    ///
868    /// This can be used to send messages to the App's UI event loop.
869    pub fn from_controller_tx(&self) -> FromControllerTx {
870        self.from_controller_tx.clone()
871    }
872}
873
874/// Converts a ControllerEvent to a UiMessage for the TUI.
875///
876/// This function maps the internal controller events to UI-friendly messages
877/// that can be displayed in a terminal interface.
878pub fn convert_controller_event_to_ui_message(event: ControllerEvent) -> UiMessage {
879    match event {
880        ControllerEvent::StreamStart { session_id, .. } => {
881            // Silent - don't display stream start messages
882            UiMessage::System {
883                session_id,
884                message: String::new(),
885            }
886        }
887        ControllerEvent::TextChunk {
888            session_id,
889            text,
890            turn_id,
891        } => UiMessage::TextChunk {
892            session_id,
893            turn_id,
894            text,
895            input_tokens: 0,
896            output_tokens: 0,
897        },
898        ControllerEvent::ToolUseStart {
899            session_id,
900            tool_name,
901            turn_id,
902            ..
903        } => UiMessage::Display {
904            session_id,
905            turn_id,
906            message: format!("Executing tool: {}", tool_name),
907        },
908        ControllerEvent::ToolUse {
909            session_id,
910            tool,
911            display_name,
912            display_title,
913            turn_id,
914        } => UiMessage::ToolExecuting {
915            session_id,
916            turn_id,
917            tool_use_id: tool.id.clone(),
918            display_name: display_name.unwrap_or_else(|| tool.name.clone()),
919            display_title: display_title.unwrap_or_default(),
920        },
921        ControllerEvent::Complete {
922            session_id,
923            turn_id,
924            stop_reason,
925        } => UiMessage::Complete {
926            session_id,
927            turn_id,
928            input_tokens: 0,
929            output_tokens: 0,
930            stop_reason,
931        },
932        ControllerEvent::Error {
933            session_id,
934            error,
935            turn_id,
936        } => UiMessage::Error {
937            session_id,
938            turn_id,
939            error,
940        },
941        ControllerEvent::TokenUpdate {
942            session_id,
943            input_tokens,
944            output_tokens,
945            context_limit,
946        } => UiMessage::TokenUpdate {
947            session_id,
948            turn_id: None,
949            input_tokens,
950            output_tokens,
951            context_limit,
952        },
953        ControllerEvent::ToolResult {
954            session_id,
955            tool_use_id,
956            status,
957            error,
958            turn_id,
959            ..
960        } => UiMessage::ToolCompleted {
961            session_id,
962            turn_id,
963            tool_use_id,
964            status,
965            error,
966        },
967        ControllerEvent::CommandComplete {
968            session_id,
969            command,
970            success,
971            message,
972        } => UiMessage::CommandComplete {
973            session_id,
974            command,
975            success,
976            message,
977        },
978        ControllerEvent::UserInteractionRequired {
979            session_id,
980            tool_use_id,
981            request,
982            turn_id,
983        } => UiMessage::UserInteractionRequired {
984            session_id,
985            tool_use_id,
986            request,
987            turn_id,
988        },
989        ControllerEvent::PermissionRequired {
990            session_id,
991            tool_use_id,
992            request,
993            turn_id,
994        } => UiMessage::PermissionRequired {
995            session_id,
996            tool_use_id,
997            request,
998            turn_id,
999        },
1000    }
1001}
1002
1003#[cfg(test)]
1004mod tests {
1005    use super::*;
1006    use crate::controller::TurnId;
1007
1008    #[test]
1009    fn test_convert_text_chunk_event() {
1010        let event = ControllerEvent::TextChunk {
1011            session_id: 1,
1012            text: "Hello".to_string(),
1013            turn_id: Some(TurnId::new_user_turn(1)),
1014        };
1015
1016        let msg = convert_controller_event_to_ui_message(event);
1017
1018        match msg {
1019            UiMessage::TextChunk {
1020                session_id, text, ..
1021            } => {
1022                assert_eq!(session_id, 1);
1023                assert_eq!(text, "Hello");
1024            }
1025            _ => panic!("Expected TextChunk message"),
1026        }
1027    }
1028
1029    #[test]
1030    fn test_convert_error_event() {
1031        let event = ControllerEvent::Error {
1032            session_id: 1,
1033            error: "Test error".to_string(),
1034            turn_id: None,
1035        };
1036
1037        let msg = convert_controller_event_to_ui_message(event);
1038
1039        match msg {
1040            UiMessage::Error {
1041                session_id, error, ..
1042            } => {
1043                assert_eq!(session_id, 1);
1044                assert_eq!(error, "Test error");
1045            }
1046            _ => panic!("Expected Error message"),
1047        }
1048    }
1049}