Skip to main content

agent_air_tui/
app.rs

1// The main TUI App for LLM agents
2//
3// This App provides a complete terminal UI with:
4// - Chat view with message history
5// - Text input with multi-line support
6// - Slash command popup
7// - Theme picker
8// - Session picker
9// - Question and permission panels
10
11use std::collections::{HashMap, HashSet};
12use std::io;
13use std::sync::Arc;
14use std::time::Instant;
15
16use crossterm::{
17    ExecutableCommand,
18    event::{
19        self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEvent, KeyEventKind,
20        KeyModifiers, MouseEventKind,
21    },
22    terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode},
23};
24use ratatui::{
25    Terminal,
26    layout::Rect,
27    prelude::CrosstermBackend,
28    widgets::{Block, Borders, Paragraph, Wrap},
29};
30use throbber_widgets_tui::{BRAILLE_EIGHT_DOUBLE, Throbber, ThrobberState};
31use tokio::runtime::Handle;
32use tokio::sync::mpsc;
33
34use crate::agent::{FromControllerRx, LLMRegistry, ToControllerTx, UiMessage};
35use crate::controller::{
36    ControlCmd, ControllerInputPayload, LLMController, PermissionPanelResponse, PermissionRegistry,
37    ToolResultStatus, TurnId, UserInteractionRegistry,
38};
39
40use super::commands::{
41    CommandContext, CommandResult, PendingAction, SlashCommand, is_slash_command, parse_command,
42};
43use super::keys::{
44    AppKeyAction, AppKeyResult, DefaultKeyHandler, ExitHandler, KeyBindings, KeyContext,
45    KeyHandler, NavigationHelper,
46};
47use super::layout::{LayoutContext, LayoutTemplate, WidgetSizes};
48use super::themes::{ThemePickerState, render_theme_picker};
49use super::widgets::{
50    BatchPermissionPanel, ChatView, ConversationView, ConversationViewFactory, PermissionPanel,
51    QuestionPanel, SessionInfo, SessionPickerState, SlashPopupState, StatusBar, StatusBarData,
52    TextInput, ToolStatus, Widget, WidgetAction, WidgetKeyContext, WidgetKeyResult,
53    render_session_picker, render_slash_popup, widget_ids,
54};
55use super::{app_theme, current_theme_name, default_theme_name, get_theme, init_theme};
56
57const PROMPT: &str = " \u{203A} ";
58const CONTINUATION_INDENT: &str = "   ";
59
60// Pending status messages for chat view spinner
61const PENDING_STATUS_TOOLS: &str = "running tools...";
62const PENDING_STATUS_LLM: &str = "Processing response from LLM...";
63
64/// Callback for dynamic processing messages (e.g., rotating messages).
65/// Called on each render frame when waiting for a response.
66pub type ProcessingMessageFn = Arc<dyn Fn() -> String + Send + Sync>;
67
68/// Configuration for the App
69pub struct AppConfig {
70    /// Agent name (displayed in title bar)
71    pub agent_name: String,
72    /// Agent version
73    pub version: String,
74    /// Slash commands (if None, uses default commands)
75    pub commands: Option<Vec<Box<dyn SlashCommand>>>,
76    /// Extension data available to commands via `ctx.extension::<T>()`
77    pub command_extension: Option<Box<dyn std::any::Any + Send>>,
78    /// Static message shown while processing (default: "Processing request...")
79    pub processing_message: String,
80    /// Optional callback for dynamic messages. When set, overrides processing_message.
81    /// Use this for rotating messages or context-aware status.
82    pub processing_message_fn: Option<ProcessingMessageFn>,
83    /// Error message shown when user submits but no session exists
84    pub error_no_session: Option<String>,
85}
86
87impl std::fmt::Debug for AppConfig {
88    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
89        f.debug_struct("AppConfig")
90            .field("agent_name", &self.agent_name)
91            .field("version", &self.version)
92            .field(
93                "commands",
94                &self
95                    .commands
96                    .as_ref()
97                    .map(|c| format!("<{} commands>", c.len())),
98            )
99            .field(
100                "command_extension",
101                &self.command_extension.as_ref().map(|_| "<extension>"),
102            )
103            .field("processing_message", &self.processing_message)
104            .field(
105                "processing_message_fn",
106                &self.processing_message_fn.as_ref().map(|_| "<fn>"),
107            )
108            .field("error_no_session", &self.error_no_session)
109            .finish()
110    }
111}
112
113impl Default for AppConfig {
114    fn default() -> Self {
115        Self {
116            agent_name: "Agent".to_string(),
117            version: "0.1.0".to_string(),
118            commands: None, // Use default commands
119            command_extension: None,
120            processing_message: "Processing request...".to_string(),
121            processing_message_fn: None,
122            error_no_session: None,
123        }
124    }
125}
126
127/// Terminal UI application with chat, input, and command handling.
128///
129/// Manages the main event loop, widget rendering, and communication with the controller.
130pub struct App {
131    /// Agent name
132    agent_name: String,
133
134    /// Agent version
135    version: String,
136
137    /// All available slash commands
138    commands: Vec<Box<dyn SlashCommand>>,
139
140    /// Extension data available to commands
141    command_extension: Option<Box<dyn std::any::Any + Send>>,
142
143    /// Processing message
144    processing_message: String,
145
146    /// Optional callback for dynamic processing messages
147    processing_message_fn: Option<ProcessingMessageFn>,
148
149    /// Error message shown when user submits but no session exists
150    error_no_session: Option<String>,
151
152    /// Whether the application should quit.
153    pub should_quit: bool,
154
155    /// Sender for messages to the controller
156    to_controller: Option<ToControllerTx>,
157
158    /// Receiver for messages from the controller
159    from_controller: Option<FromControllerRx>,
160
161    /// Reference to the controller for session management
162    controller: Option<Arc<LLMController>>,
163
164    /// LLM provider registry
165    llm_registry: Option<LLMRegistry>,
166
167    /// Tokio runtime handle for async operations
168    runtime_handle: Option<Handle>,
169
170    /// Current active session ID
171    session_id: i64,
172
173    /// Turn counter for user turns
174    user_turn_counter: i64,
175
176    /// Model name for display
177    model_name: String,
178
179    /// Current context usage (input tokens)
180    context_used: i64,
181
182    /// Context limit for the model
183    context_limit: i32,
184
185    /// Throbber animation state for progress indicator
186    throbber_state: ThrobberState,
187
188    /// Whether we're waiting for a response (set immediately on submit, before streaming starts)
189    pub waiting_for_response: bool,
190
191    /// When we started waiting for a response (for elapsed time display)
192    waiting_started: Option<Instant>,
193
194    /// Frame counter for throttling animation speed
195    animation_frame_counter: u8,
196
197    /// Current turn ID we're expecting responses for (to filter stale messages)
198    current_turn_id: Option<TurnId>,
199
200    /// Set of currently executing tool IDs (for spinner display)
201    executing_tools: HashSet<String>,
202
203    /// Registered widgets (keyed by ID)
204    pub widgets: HashMap<&'static str, Box<dyn Widget>>,
205
206    /// Cached sorted priority order for key event handling
207    pub widget_priority_order: Vec<&'static str>,
208
209    /// Filtered slash commands for the popup (indices into self.commands)
210    filtered_command_indices: Vec<usize>,
211
212    /// List of all sessions created in this instance
213    sessions: Vec<SessionInfo>,
214
215    /// Session state storage for conversation views (used for session switching)
216    session_states: HashMap<i64, Box<dyn std::any::Any + Send>>,
217
218    /// The conversation view (decoupled from ChatView)
219    conversation_view: Box<dyn ConversationView>,
220
221    /// Factory for creating new conversation views
222    conversation_factory: ConversationViewFactory,
223
224    /// Custom throbber message (overrides processing_message when set)
225    custom_throbber_message: Option<String>,
226
227    /// User interaction registry for responding to AskUserQuestions
228    user_interaction_registry: Option<Arc<UserInteractionRegistry>>,
229
230    /// Permission registry for responding to AskForPermissions
231    permission_registry: Option<Arc<PermissionRegistry>>,
232
233    /// Layout template for widget arrangement
234    layout_template: LayoutTemplate,
235
236    /// Key handler for customizable key bindings
237    key_handler: Box<dyn KeyHandler>,
238
239    /// Optional exit handler for cleanup before quitting
240    exit_handler: Option<Box<dyn ExitHandler>>,
241}
242
243impl App {
244    /// Create a new App with default configuration.
245    pub fn new() -> Self {
246        Self::with_config(AppConfig::default())
247    }
248
249    /// Create a new App with custom configuration.
250    pub fn with_config(config: AppConfig) -> Self {
251        use super::commands::default_commands;
252
253        // Initialize default theme
254        let theme_name = default_theme_name();
255        if let Some(theme) = get_theme(theme_name) {
256            init_theme(theme_name, theme);
257        }
258
259        // Use provided commands or default commands
260        let commands = config.commands.unwrap_or_else(default_commands);
261
262        // Create a default conversation factory (creates basic ChatView)
263        let default_factory: ConversationViewFactory = Box::new(|| Box::new(ChatView::new()));
264
265        let mut app = Self {
266            agent_name: config.agent_name,
267            version: config.version,
268            commands,
269            command_extension: config.command_extension,
270            processing_message: config.processing_message,
271            processing_message_fn: config.processing_message_fn,
272            error_no_session: config.error_no_session,
273            should_quit: false,
274            to_controller: None,
275            from_controller: None,
276            controller: None,
277            llm_registry: None,
278            runtime_handle: None,
279            session_id: 0,
280            user_turn_counter: 0,
281            model_name: "Not connected".to_string(),
282            context_used: 0,
283            context_limit: 0,
284            throbber_state: ThrobberState::default(),
285            waiting_for_response: false,
286            waiting_started: None,
287            animation_frame_counter: 0,
288            current_turn_id: None,
289            executing_tools: HashSet::new(),
290            widgets: HashMap::new(),
291            widget_priority_order: Vec::new(),
292            filtered_command_indices: Vec::new(),
293            sessions: Vec::new(),
294            session_states: HashMap::new(),
295            conversation_view: (default_factory)(),
296            conversation_factory: default_factory,
297            custom_throbber_message: None,
298            user_interaction_registry: None,
299            permission_registry: None,
300            layout_template: LayoutTemplate::default(),
301            key_handler: Box::new(DefaultKeyHandler::default()),
302            exit_handler: None,
303        };
304
305        // Register default widgets
306        app.register_widget(StatusBar::new());
307        app.register_widget(TextInput::new());
308
309        app
310    }
311
312    /// Register a widget with the app
313    ///
314    /// The widget will be stored and used for key handling and rendering.
315    /// Widgets are identified by their ID and stored in a priority order.
316    pub fn register_widget<W: Widget>(&mut self, widget: W) {
317        let id = widget.id();
318        self.widgets.insert(id, Box::new(widget));
319        self.rebuild_priority_order();
320    }
321
322    /// Rebuild the priority order cache after widget registration
323    pub fn rebuild_priority_order(&mut self) {
324        let mut order: Vec<_> = self.widgets.keys().copied().collect();
325        order.sort_by(|a, b| {
326            let priority_a = self.widgets.get(a).map(|w| w.priority()).unwrap_or(0);
327            let priority_b = self.widgets.get(b).map(|w| w.priority()).unwrap_or(0);
328            priority_b.cmp(&priority_a) // Descending order (higher priority first)
329        });
330        self.widget_priority_order = order;
331    }
332
333    /// Get a widget by ID
334    pub fn widget<W: Widget + 'static>(&self, id: &str) -> Option<&W> {
335        self.widgets
336            .get(id)
337            .and_then(|w| w.as_any().downcast_ref::<W>())
338    }
339
340    /// Get a widget by ID (mutable)
341    pub fn widget_mut<W: Widget + 'static>(&mut self, id: &str) -> Option<&mut W> {
342        self.widgets
343            .get_mut(id)
344            .and_then(|w| w.as_any_mut().downcast_mut::<W>())
345    }
346
347    /// Check if a widget is registered
348    pub fn has_widget(&self, id: &str) -> bool {
349        self.widgets.contains_key(id)
350    }
351
352    /// Check if any registered widget blocks input
353    fn any_widget_blocks_input(&self) -> bool {
354        self.widgets
355            .values()
356            .any(|w| w.is_active() && w.blocks_input())
357    }
358
359    /// Set the conversation view factory
360    ///
361    /// The factory is called to create new conversation views when:
362    /// - Creating a new session
363    /// - Clearing the current conversation (/clear command)
364    ///
365    /// # Example
366    ///
367    /// ```ignore
368    /// app.set_conversation_factory(|| {
369    ///     Box::new(ChatView::new()
370    ///         .with_title("My Agent")
371    ///         .with_initial_content(welcome_renderer))
372    /// });
373    /// ```
374    pub fn set_conversation_factory<F>(&mut self, factory: F)
375    where
376        F: Fn() -> Box<dyn ConversationView> + Send + Sync + 'static,
377    {
378        self.conversation_factory = Box::new(factory);
379        // Replace current conversation view with one from the new factory
380        self.conversation_view = (self.conversation_factory)();
381    }
382
383    /// Get a reference to the TextInput widget if registered
384    fn input(&self) -> Option<&TextInput> {
385        self.widget::<TextInput>(widget_ids::TEXT_INPUT)
386    }
387
388    /// Get a mutable reference to the TextInput widget if registered
389    fn input_mut(&mut self) -> Option<&mut TextInput> {
390        self.widget_mut::<TextInput>(widget_ids::TEXT_INPUT)
391    }
392
393    /// Check if the conversation view is currently streaming
394    fn is_chat_streaming(&self) -> bool {
395        self.conversation_view.is_streaming()
396    }
397
398    /// Get the agent name
399    pub fn agent_name(&self) -> &str {
400        &self.agent_name
401    }
402
403    /// Get the agent version
404    pub fn version(&self) -> &str {
405        &self.version
406    }
407
408    /// Set the channel for sending messages to the controller
409    pub fn set_to_controller(&mut self, tx: ToControllerTx) {
410        self.to_controller = Some(tx);
411    }
412
413    /// Set the channel for receiving messages from the controller
414    pub fn set_from_controller(&mut self, rx: FromControllerRx) {
415        self.from_controller = Some(rx);
416    }
417
418    /// Set the controller reference for session management
419    pub fn set_controller(&mut self, controller: Arc<LLMController>) {
420        self.controller = Some(controller);
421    }
422
423    /// Set the LLM provider registry
424    pub fn set_llm_registry(&mut self, registry: LLMRegistry) {
425        self.llm_registry = Some(registry);
426    }
427
428    /// Set the tokio runtime handle
429    pub fn set_runtime_handle(&mut self, handle: Handle) {
430        self.runtime_handle = Some(handle);
431    }
432
433    /// Set the user interaction registry
434    pub fn set_user_interaction_registry(&mut self, registry: Arc<UserInteractionRegistry>) {
435        self.user_interaction_registry = Some(registry);
436    }
437
438    /// Set the permission registry
439    pub fn set_permission_registry(&mut self, registry: Arc<PermissionRegistry>) {
440        self.permission_registry = Some(registry);
441    }
442
443    /// Set the session ID
444    pub fn set_session_id(&mut self, id: i64) {
445        self.session_id = id;
446    }
447
448    /// Set the model name
449    pub fn set_model_name(&mut self, name: impl Into<String>) {
450        self.model_name = name.into();
451    }
452
453    /// Set the context limit
454    pub fn set_context_limit(&mut self, limit: i32) {
455        self.context_limit = limit;
456    }
457
458    /// Set the layout template
459    pub fn set_layout(&mut self, template: LayoutTemplate) {
460        self.layout_template = template;
461    }
462
463    /// Set a custom key handler.
464    ///
465    /// This allows full control over key handling behavior.
466    /// For simpler customization, use [`Self::set_key_bindings`] instead.
467    pub fn set_key_handler<H: KeyHandler>(&mut self, handler: H) {
468        self.key_handler = Box::new(handler);
469    }
470
471    /// Set a boxed key handler directly.
472    ///
473    /// This is useful when you have a `Box<dyn KeyHandler>` already.
474    pub fn set_key_handler_boxed(&mut self, handler: Box<dyn KeyHandler>) {
475        self.key_handler = handler;
476    }
477
478    /// Set custom key bindings using the default handler.
479    ///
480    /// This is a simpler alternative to [`Self::set_key_handler`] when you
481    /// only need to change which keys trigger which actions.
482    pub fn set_key_bindings(&mut self, bindings: KeyBindings) {
483        self.key_handler = Box::new(DefaultKeyHandler::new(bindings));
484    }
485
486    /// Set an exit handler for cleanup before quitting.
487    ///
488    /// The exit handler's `on_exit()` method is called when the user
489    /// confirms exit. If it returns `false`, the exit is cancelled.
490    pub fn set_exit_handler<H: ExitHandler>(&mut self, handler: H) {
491        self.exit_handler = Some(Box::new(handler));
492    }
493
494    /// Set a boxed exit handler directly.
495    pub fn set_exit_handler_boxed(&mut self, handler: Box<dyn ExitHandler>) {
496        self.exit_handler = Some(handler);
497    }
498
499    /// Compute widget sizes for layout computation
500    fn compute_widget_sizes(&self, frame_height: u16) -> WidgetSizes {
501        let mut heights = HashMap::new();
502        let mut is_active = HashMap::new();
503
504        for (id, widget) in &self.widgets {
505            heights.insert(*id, widget.required_height(frame_height));
506            is_active.insert(*id, widget.is_active());
507        }
508
509        WidgetSizes { heights, is_active }
510    }
511
512    /// Build the layout context for rendering
513    fn build_layout_context<'a>(
514        &self,
515        frame_area: Rect,
516        show_throbber: bool,
517        prompt_len: usize,
518        indent_len: usize,
519        theme: &'a super::themes::Theme,
520    ) -> LayoutContext<'a> {
521        let frame_width = frame_area.width as usize;
522        let input_visual_lines = self
523            .input()
524            .map(|i| i.visual_line_count(frame_width, prompt_len, indent_len))
525            .unwrap_or(1);
526
527        let mut active_widgets = HashSet::new();
528        for (id, widget) in &self.widgets {
529            if widget.is_active() {
530                active_widgets.insert(*id);
531            }
532        }
533
534        LayoutContext {
535            frame_area,
536            show_throbber,
537            input_visual_lines,
538            theme,
539            active_widgets,
540        }
541    }
542
543    pub fn submit_message(&mut self) {
544        // Get content from input widget (if registered)
545        let content = match self.input_mut() {
546            Some(input) => input.take(),
547            None => return, // No input widget registered
548        };
549        if content.trim().is_empty() {
550            return;
551        }
552
553        // Check if this is a slash command
554        if is_slash_command(&content) {
555            self.execute_command(&content);
556            return;
557        }
558
559        // Add user message to chat and re-enable auto-scroll (user wants to see response)
560        self.conversation_view.enable_auto_scroll();
561        self.conversation_view.add_user_message(content.clone());
562
563        // Check if we have an active session
564        if self.session_id == 0 {
565            let msg = self.error_no_session.clone().unwrap_or_else(|| {
566                "No active session. Use /new-session to create one.".to_string()
567            });
568            self.conversation_view.add_system_message(msg);
569            return;
570        }
571
572        // Send to controller if channel is available
573        if let Some(ref tx) = self.to_controller {
574            self.user_turn_counter += 1;
575            let turn_id = TurnId::new_user_turn(self.user_turn_counter);
576            let payload = ControllerInputPayload::data(self.session_id, content, turn_id);
577
578            // Try to send (non-blocking)
579            if tx.try_send(payload).is_err() {
580                self.conversation_view
581                    .add_system_message("Failed to send message to controller".to_string());
582            } else {
583                // Immediately show throbber (before streaming starts)
584                self.waiting_for_response = true;
585                self.waiting_started = Some(Instant::now());
586                // Track the expected turn ID to filter stale messages
587                self.current_turn_id = Some(TurnId::new_user_turn(self.user_turn_counter));
588            }
589        }
590    }
591
592    /// Interrupt the current LLM request
593    pub fn interrupt_request(&mut self) {
594        // Only interrupt if we're actually waiting/streaming/executing tools
595        if !self.waiting_for_response
596            && !self.is_chat_streaming()
597            && self.executing_tools.is_empty()
598        {
599            return;
600        }
601
602        // Send interrupt command to controller
603        if let Some(ref tx) = self.to_controller {
604            let payload = ControllerInputPayload::control(self.session_id, ControlCmd::Interrupt);
605            if tx.try_send(payload).is_ok() {
606                // Reset waiting state immediately for responsive UI
607                self.waiting_for_response = false;
608                self.waiting_started = None;
609                self.executing_tools.clear();
610                // Keep partial streaming content visible (save it as a message)
611                self.conversation_view.complete_streaming();
612                // Clear turn ID so any stale messages from this turn are ignored
613                self.current_turn_id = None;
614                self.conversation_view
615                    .add_system_message("Request cancelled".to_string());
616            }
617        }
618    }
619
620    /// Execute a slash command using the trait-based system
621    fn execute_command(&mut self, input: &str) {
622        let Some((cmd_name, args)) = parse_command(input) else {
623            self.conversation_view
624                .add_system_message("Invalid command format".to_string());
625            return;
626        };
627
628        // Find the command by name
629        let cmd_idx = self.commands.iter().position(|c| c.name() == cmd_name);
630        let Some(cmd_idx) = cmd_idx else {
631            self.conversation_view
632                .add_system_message(format!("Unknown command: /{}", cmd_name));
633            return;
634        };
635
636        // Execute command and collect results (scoped to drop borrows)
637        let (result, pending_actions) = {
638            // Temporarily take command_extension to avoid borrow issues
639            let extension = self.command_extension.take();
640            let extension_ref = extension.as_ref().map(|e| e.as_ref() as &dyn std::any::Any);
641
642            let mut ctx = CommandContext::new(
643                self.session_id,
644                &self.agent_name,
645                &self.version,
646                &self.commands,
647                &mut *self.conversation_view,
648                self.to_controller.as_ref(),
649                extension_ref,
650            );
651
652            // Execute the command
653            let result = self.commands[cmd_idx].execute(args, &mut ctx);
654            let pending_actions = ctx.take_pending_actions();
655
656            // Restore extension before leaving scope
657            self.command_extension = extension;
658
659            (result, pending_actions)
660        };
661
662        // Handle pending actions (now that ctx is dropped)
663        for action in pending_actions {
664            match action {
665                PendingAction::OpenThemePicker => self.cmd_themes(),
666                PendingAction::OpenSessionPicker => self.cmd_sessions(),
667                PendingAction::ClearConversation => self.cmd_clear(),
668                PendingAction::CompactConversation => self.cmd_compact(),
669                PendingAction::CreateNewSession => {
670                    self.cmd_new_session();
671                }
672                PendingAction::Quit => {
673                    self.should_quit = true;
674                }
675            }
676        }
677
678        // Handle result
679        match result {
680            CommandResult::Ok | CommandResult::Handled => {}
681            CommandResult::Message(msg) => {
682                self.conversation_view.add_system_message(msg);
683            }
684            CommandResult::Error(err) => {
685                self.conversation_view
686                    .add_system_message(format!("Error: {}", err));
687            }
688            CommandResult::Quit => {
689                self.should_quit = true;
690            }
691        }
692    }
693
694    fn cmd_clear(&mut self) {
695        // Create a fresh conversation view using the factory
696        self.conversation_view = (self.conversation_factory)();
697        self.user_turn_counter = 0;
698
699        // Send Clear command to controller to clear session conversation
700        if self.session_id != 0
701            && let Some(ref tx) = self.to_controller
702        {
703            let payload = ControllerInputPayload::control(self.session_id, ControlCmd::Clear);
704            if let Err(e) = tx.try_send(payload) {
705                tracing::warn!("Failed to send clear command to controller: {}", e);
706            }
707        }
708    }
709
710    fn cmd_compact(&mut self) {
711        // Check if we have an active session
712        if self.session_id == 0 {
713            self.conversation_view
714                .add_system_message("No active session to compact".to_string());
715            return;
716        }
717
718        // Send Compact command to controller
719        if let Some(ref tx) = self.to_controller {
720            let payload = ControllerInputPayload::control(self.session_id, ControlCmd::Compact);
721            if tx.try_send(payload).is_ok() {
722                // Show spinner with "compacting..." message
723                self.waiting_for_response = true;
724                self.waiting_started = Some(Instant::now());
725                self.custom_throbber_message = Some("compacting...".to_string());
726            } else {
727                self.conversation_view
728                    .add_system_message("Failed to send compact command".to_string());
729            }
730        }
731    }
732
733    fn cmd_new_session(&mut self) -> String {
734        let Some(ref controller) = self.controller else {
735            return "Error: Controller not available".to_string();
736        };
737
738        let Some(ref handle) = self.runtime_handle else {
739            return "Error: Runtime not available".to_string();
740        };
741
742        let Some(ref registry) = self.llm_registry else {
743            return "Error: No LLM providers configured.\nSet ANTHROPIC_API_KEY or create config file".to_string();
744        };
745
746        // Get the default config from registry
747        let Some(config) = registry.get_default() else {
748            return "Error: No LLM providers configured.\nSet ANTHROPIC_API_KEY or create config file".to_string();
749        };
750
751        let model = config.model.clone();
752        let context_limit = config.context_limit;
753        let config = config.clone();
754
755        // Create session using the runtime handle
756        let controller = controller.clone();
757        let session_id = match handle.block_on(async { controller.create_session(config).await }) {
758            Ok(id) => id,
759            Err(e) => {
760                return format!("Error: Failed to create session: {}", e);
761            }
762        };
763
764        // Add session to the sessions list
765        let session_info = SessionInfo::new(session_id, model.clone(), context_limit);
766        self.sessions.push(session_info);
767
768        // Save current conversation state before switching to new session
769        if self.session_id != 0 {
770            let state = self.conversation_view.save_state();
771            self.session_states.insert(self.session_id, state);
772        }
773
774        // Create new conversation view for the new session
775        self.conversation_view = (self.conversation_factory)();
776
777        self.session_id = session_id;
778        self.model_name = model.clone();
779        self.context_limit = context_limit;
780        self.context_used = 0;
781        self.user_turn_counter = 0;
782
783        // Return empty string - no system message needed
784        String::new()
785    }
786
787    fn cmd_themes(&mut self) {
788        if let Some(widget) = self.widgets.get_mut(widget_ids::THEME_PICKER)
789            && let Some(picker) = widget.as_any_mut().downcast_mut::<ThemePickerState>()
790        {
791            let current_name = current_theme_name();
792            let current_theme = app_theme();
793            picker.activate(&current_name, current_theme);
794        }
795    }
796
797    fn cmd_sessions(&mut self) {
798        // Update context_used for current session before displaying
799        if let Some(session) = self.sessions.iter_mut().find(|s| s.id == self.session_id) {
800            session.context_used = self.context_used;
801        }
802
803        if let Some(widget) = self.widgets.get_mut(widget_ids::SESSION_PICKER)
804            && let Some(picker) = widget.as_any_mut().downcast_mut::<SessionPickerState>()
805        {
806            picker.activate(self.sessions.clone(), self.session_id);
807        }
808    }
809
810    /// Switch to a different session by ID
811    pub fn switch_session(&mut self, session_id: i64) {
812        // Don't switch if already on this session
813        if session_id == self.session_id {
814            return;
815        }
816
817        // Update context for current session before switching
818        if let Some(session) = self.sessions.iter_mut().find(|s| s.id == self.session_id) {
819            session.context_used = self.context_used;
820        }
821
822        // Save current conversation state
823        let state = self.conversation_view.save_state();
824        self.session_states.insert(self.session_id, state);
825
826        // Find the target session
827        if let Some(session) = self.sessions.iter().find(|s| s.id == session_id) {
828            self.session_id = session_id;
829            self.model_name = session.model.clone();
830            self.context_used = session.context_used;
831            self.context_limit = session.context_limit;
832            self.user_turn_counter = 0;
833
834            // Restore conversation state for this session, or create new one
835            if let Some(stored_state) = self.session_states.remove(&session_id) {
836                self.conversation_view.restore_state(stored_state);
837            } else {
838                self.conversation_view = (self.conversation_factory)();
839            }
840        }
841    }
842
843    /// Add a session to the sessions list
844    pub fn add_session(&mut self, info: SessionInfo) {
845        self.sessions.push(info);
846    }
847
848    /// Submit the question panel response
849    fn submit_question_panel_response(
850        &mut self,
851        tool_use_id: String,
852        response: crate::controller::AskUserQuestionsResponse,
853    ) {
854        // Respond to the interaction via the registry
855        if let (Some(registry), Some(handle)) =
856            (&self.user_interaction_registry, &self.runtime_handle)
857        {
858            let registry = registry.clone();
859            handle.spawn(async move {
860                if let Err(e) = registry.respond(&tool_use_id, response).await {
861                    tracing::error!(%tool_use_id, ?e, "Failed to respond to interaction");
862                }
863            });
864        }
865
866        // Deactivate the widget
867        if let Some(widget) = self.widgets.get_mut(widget_ids::QUESTION_PANEL)
868            && let Some(panel) = widget.as_any_mut().downcast_mut::<QuestionPanel>()
869        {
870            panel.deactivate();
871        }
872    }
873
874    /// Cancel the question panel (closes without responding, tool will get an error)
875    fn cancel_question_panel_response(&mut self, tool_use_id: String) {
876        // Cancel the pending interaction via the registry
877        if let (Some(registry), Some(handle)) =
878            (&self.user_interaction_registry, &self.runtime_handle)
879        {
880            let registry = registry.clone();
881            handle.spawn(async move {
882                if let Err(e) = registry.cancel(&tool_use_id).await {
883                    tracing::warn!(%tool_use_id, ?e, "Failed to cancel interaction");
884                }
885            });
886        }
887
888        // Deactivate the widget
889        if let Some(widget) = self.widgets.get_mut(widget_ids::QUESTION_PANEL)
890            && let Some(panel) = widget.as_any_mut().downcast_mut::<QuestionPanel>()
891        {
892            panel.deactivate();
893        }
894    }
895
896    /// Submit the permission panel response
897    fn submit_permission_panel_response(
898        &mut self,
899        tool_use_id: String,
900        response: PermissionPanelResponse,
901    ) {
902        // Respond to the permission request via the registry
903        if let (Some(registry), Some(handle)) = (&self.permission_registry, &self.runtime_handle) {
904            let registry = registry.clone();
905            handle.spawn(async move {
906                if let Err(e) = registry.respond_to_request(&tool_use_id, response).await {
907                    tracing::error!(%tool_use_id, ?e, "Failed to respond to permission request");
908                }
909            });
910        }
911
912        // Deactivate the widget
913        if let Some(widget) = self.widgets.get_mut(widget_ids::PERMISSION_PANEL)
914            && let Some(panel) = widget.as_any_mut().downcast_mut::<PermissionPanel>()
915        {
916            panel.deactivate();
917        }
918    }
919
920    /// Cancel the permission panel (closes without responding, denies permission)
921    fn cancel_permission_panel_response(&mut self, tool_use_id: String) {
922        // Cancel the pending permission via the registry
923        if let (Some(registry), Some(handle)) = (&self.permission_registry, &self.runtime_handle) {
924            let registry = registry.clone();
925            handle.spawn(async move {
926                if let Err(e) = registry.cancel(&tool_use_id).await {
927                    tracing::warn!(%tool_use_id, ?e, "Failed to cancel permission request");
928                }
929            });
930        }
931
932        // Deactivate the widget
933        if let Some(widget) = self.widgets.get_mut(widget_ids::PERMISSION_PANEL)
934            && let Some(panel) = widget.as_any_mut().downcast_mut::<PermissionPanel>()
935        {
936            panel.deactivate();
937        }
938    }
939
940    /// Submit the batch permission panel response
941    fn submit_batch_permission_response(
942        &mut self,
943        batch_id: String,
944        response: crate::permissions::BatchPermissionResponse,
945    ) {
946        // Respond to the batch permission request via the registry
947        if let (Some(registry), Some(handle)) = (&self.permission_registry, &self.runtime_handle) {
948            let registry = registry.clone();
949            handle.spawn(async move {
950                if let Err(e) = registry.respond_to_batch(&batch_id, response).await {
951                    tracing::error!(%batch_id, ?e, "Failed to respond to batch permission request");
952                }
953            });
954        }
955
956        // Deactivate the widget
957        if let Some(widget) = self.widgets.get_mut(widget_ids::BATCH_PERMISSION_PANEL)
958            && let Some(panel) = widget
959                .as_any_mut()
960                .downcast_mut::<crate::widgets::BatchPermissionPanel>()
961        {
962            panel.deactivate();
963        }
964    }
965
966    /// Cancel the batch permission panel (closes without responding, denies all)
967    fn cancel_batch_permission_response(&mut self, batch_id: String) {
968        // Cancel the pending batch permission via the registry
969        if let (Some(registry), Some(handle)) = (&self.permission_registry, &self.runtime_handle) {
970            let registry = registry.clone();
971            handle.spawn(async move {
972                if let Err(e) = registry.cancel_batch(&batch_id).await {
973                    tracing::warn!(%batch_id, ?e, "Failed to cancel batch permission request");
974                }
975            });
976        }
977
978        // Deactivate the widget
979        if let Some(widget) = self.widgets.get_mut(widget_ids::BATCH_PERMISSION_PANEL)
980            && let Some(panel) = widget
981                .as_any_mut()
982                .downcast_mut::<crate::widgets::BatchPermissionPanel>()
983        {
984            panel.deactivate();
985        }
986    }
987
988    /// Process any pending messages from the controller
989    fn process_controller_messages(&mut self) {
990        // Collect all available messages first to avoid borrow issues
991        let mut messages = Vec::new();
992
993        if let Some(ref mut rx) = self.from_controller {
994            loop {
995                match rx.try_recv() {
996                    Ok(msg) => messages.push(msg),
997                    Err(mpsc::error::TryRecvError::Empty) => break,
998                    Err(mpsc::error::TryRecvError::Disconnected) => {
999                        tracing::warn!("Controller channel disconnected");
1000                        break;
1001                    }
1002                }
1003            }
1004        }
1005
1006        // Process collected messages
1007        for msg in messages {
1008            self.handle_ui_message(msg);
1009        }
1010    }
1011
1012    /// Handle a UI message from the controller
1013    fn handle_ui_message(&mut self, msg: UiMessage) {
1014        match msg {
1015            UiMessage::TextChunk { text, turn_id, .. } => {
1016                // Filter stale messages from cancelled requests
1017                if !self.is_current_turn(&turn_id) {
1018                    return;
1019                }
1020                self.conversation_view.append_streaming(&text);
1021            }
1022            UiMessage::Display { message, .. } => {
1023                self.conversation_view.add_system_message(message);
1024            }
1025            UiMessage::Complete {
1026                turn_id,
1027                stop_reason,
1028                ..
1029            } => {
1030                // Filter stale Complete messages from cancelled requests
1031                if !self.is_current_turn(&turn_id) {
1032                    return;
1033                }
1034
1035                // Check if this is a tool_use stop - if so, tools will execute
1036                let is_tool_use = stop_reason.as_deref() == Some("tool_use");
1037
1038                self.conversation_view.complete_streaming();
1039
1040                // Only stop waiting if this is NOT a tool_use stop
1041                if !is_tool_use {
1042                    self.waiting_for_response = false;
1043                    self.waiting_started = None;
1044                }
1045            }
1046            UiMessage::TokenUpdate {
1047                input_tokens,
1048                context_limit,
1049                ..
1050            } => {
1051                self.context_used = input_tokens;
1052                self.context_limit = context_limit;
1053            }
1054            UiMessage::Error { error, turn_id, .. } => {
1055                // Clear state if: current turn matches OR we have active turn but error has no turn_id
1056                // The second condition prevents stale state when errors don't include turn_id
1057                let should_process = self.is_current_turn(&turn_id)
1058                    || (self.current_turn_id.is_some() && turn_id.is_none());
1059                if !should_process {
1060                    return;
1061                }
1062                self.conversation_view.complete_streaming();
1063                self.waiting_for_response = false;
1064                self.waiting_started = None;
1065                self.current_turn_id = None;
1066                self.conversation_view
1067                    .add_system_message(format!("Error: {}", error));
1068            }
1069            UiMessage::System { message, .. } => {
1070                self.conversation_view.add_system_message(message);
1071            }
1072            UiMessage::ToolExecuting {
1073                tool_use_id,
1074                display_name,
1075                display_title,
1076                ..
1077            } => {
1078                self.executing_tools.insert(tool_use_id.clone());
1079                self.conversation_view.add_tool_message(
1080                    &tool_use_id,
1081                    &display_name,
1082                    &display_title,
1083                );
1084            }
1085            UiMessage::ToolCompleted {
1086                tool_use_id,
1087                status,
1088                error,
1089                ..
1090            } => {
1091                self.executing_tools.remove(&tool_use_id);
1092                let tool_status = if status == ToolResultStatus::Success {
1093                    ToolStatus::Completed
1094                } else {
1095                    ToolStatus::Failed(error.unwrap_or_default())
1096                };
1097                self.conversation_view
1098                    .update_tool_status(&tool_use_id, tool_status);
1099            }
1100            UiMessage::CommandComplete {
1101                command,
1102                success,
1103                message,
1104                ..
1105            } => {
1106                self.waiting_for_response = false;
1107                self.waiting_started = None;
1108                self.custom_throbber_message = None;
1109
1110                match command {
1111                    ControlCmd::Compact => {
1112                        if let Some(msg) = message {
1113                            self.conversation_view.add_system_message(msg);
1114                        }
1115                    }
1116                    ControlCmd::Clear => {}
1117                    _ => {
1118                        tracing::debug!(?command, ?success, "Command completed");
1119                    }
1120                }
1121            }
1122            UiMessage::UserInteractionRequired {
1123                session_id,
1124                tool_use_id,
1125                request,
1126                turn_id,
1127            } => {
1128                if session_id == self.session_id {
1129                    self.conversation_view
1130                        .update_tool_status(&tool_use_id, ToolStatus::WaitingForUser);
1131                    // Activate via widget registry if registered
1132                    if let Some(widget) = self.widgets.get_mut(widget_ids::QUESTION_PANEL)
1133                        && let Some(panel) = widget.as_any_mut().downcast_mut::<QuestionPanel>()
1134                    {
1135                        panel.activate(tool_use_id, session_id, request, turn_id);
1136                    }
1137                }
1138            }
1139            UiMessage::PermissionRequired {
1140                session_id,
1141                tool_use_id,
1142                request,
1143                turn_id,
1144            } => {
1145                if session_id == self.session_id {
1146                    self.conversation_view
1147                        .update_tool_status(&tool_use_id, ToolStatus::WaitingForUser);
1148                    // Activate via widget registry if registered
1149                    if let Some(widget) = self.widgets.get_mut(widget_ids::PERMISSION_PANEL)
1150                        && let Some(panel) = widget.as_any_mut().downcast_mut::<PermissionPanel>()
1151                    {
1152                        panel.activate(tool_use_id, session_id, request, turn_id);
1153                    }
1154                }
1155            }
1156            UiMessage::BatchPermissionRequired {
1157                session_id,
1158                batch,
1159                turn_id,
1160            } => {
1161                if session_id == self.session_id {
1162                    // Mark all tools in the batch as waiting for user
1163                    for request in &batch.requests {
1164                        self.conversation_view
1165                            .update_tool_status(&request.id, ToolStatus::WaitingForUser);
1166                    }
1167                    // Activate BatchPermissionPanel widget if registered
1168                    if let Some(widget) = self.widgets.get_mut(widget_ids::BATCH_PERMISSION_PANEL)
1169                        && let Some(panel) =
1170                            widget.as_any_mut().downcast_mut::<BatchPermissionPanel>()
1171                    {
1172                        panel.activate(session_id, batch, turn_id);
1173                    }
1174                }
1175            }
1176        }
1177    }
1178
1179    /// Check if the given turn_id matches the current expected turn
1180    fn is_current_turn(&self, turn_id: &Option<TurnId>) -> bool {
1181        match (&self.current_turn_id, turn_id) {
1182            (Some(current), Some(incoming)) => current == incoming,
1183            (None, _) => false,
1184            (Some(_), None) => false,
1185        }
1186    }
1187
1188    pub fn scroll_up(&mut self) {
1189        self.conversation_view.scroll_up();
1190    }
1191
1192    pub fn scroll_down(&mut self) {
1193        self.conversation_view.scroll_down();
1194    }
1195
1196    /// Get the current working directory with home directory substitution
1197    fn get_cwd(&self) -> String {
1198        std::env::current_dir()
1199            .map(|p| {
1200                let path_str = p.display().to_string();
1201                if let Some(home) = std::env::var_os("HOME") {
1202                    let home_str = home.to_string_lossy();
1203                    if path_str.starts_with(home_str.as_ref()) {
1204                        return format!("~{}", &path_str[home_str.len()..]);
1205                    }
1206                }
1207                path_str
1208            })
1209            .unwrap_or_else(|_| "unknown".to_string())
1210    }
1211
1212    /// Handle key events
1213    fn handle_key(&mut self, key: KeyCode, modifiers: KeyModifiers) {
1214        // Build key context for the handler
1215        let key_event = KeyEvent::new(key, modifiers);
1216        let context = KeyContext {
1217            input_empty: self.input().map(|i| i.is_empty()).unwrap_or(true),
1218            is_processing: self.waiting_for_response || self.is_chat_streaming(),
1219            widget_blocking: self.any_widget_blocks_input(),
1220        };
1221
1222        // Let the key handler process first
1223        // (handler manages exit confirmation state internally)
1224        let result = self.key_handler.handle_key(key_event, &context);
1225
1226        match result {
1227            AppKeyResult::Handled => return,
1228            AppKeyResult::Action(action) => {
1229                self.execute_key_action(action);
1230                return;
1231            }
1232            AppKeyResult::NotHandled => {
1233                // Continue to widget dispatch
1234            }
1235        }
1236
1237        // Try to send key event to registered widgets (by priority order)
1238        let theme = app_theme();
1239        let nav = NavigationHelper::new(self.key_handler.bindings());
1240        let widget_ctx = WidgetKeyContext { theme: &theme, nav };
1241
1242        // Collect widget IDs to check (we need to avoid borrow issues)
1243        let widget_ids_to_check: Vec<&'static str> = self.widget_priority_order.clone();
1244
1245        for widget_id in widget_ids_to_check {
1246            if let Some(widget) = self.widgets.get_mut(widget_id)
1247                && widget.is_active()
1248            {
1249                match widget.handle_key(key_event, &widget_ctx) {
1250                    WidgetKeyResult::Handled => return,
1251                    WidgetKeyResult::Action(action) => {
1252                        self.process_widget_action(action);
1253                        return;
1254                    }
1255                    WidgetKeyResult::NotHandled => {
1256                        // Continue to next widget or fall through to input handling
1257                    }
1258                }
1259            }
1260        }
1261
1262        // Handle slash popup specially (needs input buffer access)
1263        if self.is_slash_popup_active() {
1264            self.handle_slash_popup_key(key);
1265        }
1266    }
1267
1268    /// Execute an application-level key action.
1269    fn execute_key_action(&mut self, action: AppKeyAction) {
1270        match action {
1271            AppKeyAction::MoveUp => {
1272                if let Some(input) = self.input_mut() {
1273                    input.move_up();
1274                }
1275            }
1276            AppKeyAction::MoveDown => {
1277                if let Some(input) = self.input_mut() {
1278                    input.move_down();
1279                }
1280            }
1281            AppKeyAction::MoveLeft => {
1282                if let Some(input) = self.input_mut() {
1283                    input.move_left();
1284                }
1285            }
1286            AppKeyAction::MoveRight => {
1287                if let Some(input) = self.input_mut() {
1288                    input.move_right();
1289                }
1290            }
1291            AppKeyAction::MoveLineStart => {
1292                if let Some(input) = self.input_mut() {
1293                    input.move_to_line_start();
1294                }
1295            }
1296            AppKeyAction::MoveLineEnd => {
1297                if let Some(input) = self.input_mut() {
1298                    input.move_to_line_end();
1299                }
1300            }
1301            AppKeyAction::DeleteCharBefore => {
1302                if let Some(input) = self.input_mut() {
1303                    input.delete_char_before();
1304                }
1305            }
1306            AppKeyAction::DeleteCharAt => {
1307                if let Some(input) = self.input_mut() {
1308                    input.delete_char_at();
1309                }
1310            }
1311            AppKeyAction::KillLine => {
1312                if let Some(input) = self.input_mut() {
1313                    input.kill_line();
1314                }
1315            }
1316            AppKeyAction::InsertNewline => {
1317                if let Some(input) = self.input_mut() {
1318                    input.insert_char('\n');
1319                }
1320            }
1321            AppKeyAction::InsertChar(c) => {
1322                if let Some(input) = self.input_mut() {
1323                    input.insert_char(c);
1324                }
1325                // Check if we just typed "/" as the first character for slash popup
1326                if c == '/' && self.input().map(|i| i.buffer() == "/").unwrap_or(false) {
1327                    self.activate_slash_popup();
1328                }
1329            }
1330            AppKeyAction::Submit => {
1331                self.submit_message();
1332            }
1333            AppKeyAction::Interrupt => {
1334                self.interrupt_request();
1335            }
1336            AppKeyAction::Quit => {
1337                self.should_quit = true;
1338            }
1339            AppKeyAction::RequestExit => {
1340                // Call exit handler if set, otherwise just quit
1341                let should_quit = self
1342                    .exit_handler
1343                    .as_mut()
1344                    .map(|h| h.on_exit())
1345                    .unwrap_or(true);
1346                if should_quit {
1347                    self.should_quit = true;
1348                }
1349            }
1350            AppKeyAction::ActivateSlashPopup => {
1351                self.activate_slash_popup();
1352            }
1353            AppKeyAction::Custom(_) => {
1354                // Custom actions are handled by the custom_action_handler if set.
1355                // Users implementing custom key bindings should set a handler
1356                // via with_custom_action_handler() to receive these actions.
1357            }
1358        }
1359    }
1360
1361    /// Process an action returned by a widget
1362    fn process_widget_action(&mut self, action: WidgetAction) {
1363        match action {
1364            WidgetAction::SubmitQuestion {
1365                tool_use_id,
1366                response,
1367            } => {
1368                self.submit_question_panel_response(tool_use_id, response);
1369            }
1370            WidgetAction::CancelQuestion { tool_use_id } => {
1371                self.cancel_question_panel_response(tool_use_id);
1372            }
1373            WidgetAction::SubmitPermission {
1374                tool_use_id,
1375                response,
1376            } => {
1377                self.submit_permission_panel_response(tool_use_id, response);
1378            }
1379            WidgetAction::CancelPermission { tool_use_id } => {
1380                self.cancel_permission_panel_response(tool_use_id);
1381            }
1382            WidgetAction::SubmitBatchPermission { batch_id, response } => {
1383                self.submit_batch_permission_response(batch_id, response);
1384            }
1385            WidgetAction::CancelBatchPermission { batch_id } => {
1386                self.cancel_batch_permission_response(batch_id);
1387            }
1388            WidgetAction::SwitchSession { session_id } => {
1389                self.switch_session(session_id);
1390            }
1391            WidgetAction::ExecuteCommand { command } => {
1392                // Handle slash popup command selection
1393                if command.starts_with("__SLASH_INDEX_") {
1394                    if let Ok(idx) = command
1395                        .trim_start_matches("__SLASH_INDEX_")
1396                        .parse::<usize>()
1397                    {
1398                        self.execute_slash_command_at_index(idx);
1399                    }
1400                } else {
1401                    self.execute_command(&command);
1402                }
1403            }
1404            WidgetAction::Close => {
1405                // Widget closed itself (e.g., theme picker confirm/cancel)
1406            }
1407        }
1408    }
1409
1410    /// Check if the slash popup is active
1411    fn is_slash_popup_active(&self) -> bool {
1412        self.widgets
1413            .get(widget_ids::SLASH_POPUP)
1414            .map(|w| w.is_active())
1415            .unwrap_or(false)
1416    }
1417
1418    /// Filter commands by prefix and return indices into self.commands
1419    fn filter_command_indices(&self, prefix: &str) -> Vec<usize> {
1420        let search_term = prefix.trim_start_matches('/').to_lowercase();
1421        self.commands
1422            .iter()
1423            .enumerate()
1424            .filter(|(_, cmd)| cmd.name().to_lowercase().starts_with(&search_term))
1425            .map(|(i, _)| i)
1426            .collect()
1427    }
1428
1429    /// Activate the slash popup
1430    fn activate_slash_popup(&mut self) {
1431        // Filter first to avoid borrow issues
1432        let indices = self.filter_command_indices("/");
1433        let count = indices.len();
1434        self.filtered_command_indices = indices;
1435
1436        if let Some(widget) = self.widgets.get_mut(widget_ids::SLASH_POPUP)
1437            && let Some(popup) = widget.as_any_mut().downcast_mut::<SlashPopupState>()
1438        {
1439            popup.activate();
1440            popup.set_filtered_count(count);
1441        }
1442    }
1443
1444    /// Handle key events for the slash popup (needs special handling for input buffer)
1445    fn handle_slash_popup_key(&mut self, key: KeyCode) {
1446        match key {
1447            KeyCode::Up => {
1448                if let Some(widget) = self.widgets.get_mut(widget_ids::SLASH_POPUP)
1449                    && let Some(popup) = widget.as_any_mut().downcast_mut::<SlashPopupState>()
1450                {
1451                    popup.select_previous();
1452                }
1453            }
1454            KeyCode::Down => {
1455                if let Some(widget) = self.widgets.get_mut(widget_ids::SLASH_POPUP)
1456                    && let Some(popup) = widget.as_any_mut().downcast_mut::<SlashPopupState>()
1457                {
1458                    popup.select_next();
1459                }
1460            }
1461            KeyCode::Enter => {
1462                let selected_idx = self
1463                    .widgets
1464                    .get(widget_ids::SLASH_POPUP)
1465                    .and_then(|w| w.as_any().downcast_ref::<SlashPopupState>())
1466                    .map(|p| p.selected_index)
1467                    .unwrap_or(0);
1468                self.execute_slash_command_at_index(selected_idx);
1469            }
1470            KeyCode::Esc => {
1471                if let Some(input) = self.input_mut() {
1472                    input.clear();
1473                }
1474                if let Some(widget) = self.widgets.get_mut(widget_ids::SLASH_POPUP)
1475                    && let Some(popup) = widget.as_any_mut().downcast_mut::<SlashPopupState>()
1476                {
1477                    popup.deactivate();
1478                }
1479                self.filtered_command_indices.clear();
1480            }
1481            KeyCode::Backspace => {
1482                let is_just_slash = self.input().map(|i| i.buffer() == "/").unwrap_or(false);
1483                if is_just_slash {
1484                    if let Some(input) = self.input_mut() {
1485                        input.clear();
1486                    }
1487                    if let Some(widget) = self.widgets.get_mut(widget_ids::SLASH_POPUP)
1488                        && let Some(popup) = widget.as_any_mut().downcast_mut::<SlashPopupState>()
1489                    {
1490                        popup.deactivate();
1491                    }
1492                    self.filtered_command_indices.clear();
1493                } else {
1494                    if let Some(input) = self.input_mut() {
1495                        input.delete_char_before();
1496                    }
1497                    let buffer = self
1498                        .input()
1499                        .map(|i| i.buffer().to_string())
1500                        .unwrap_or_default();
1501                    self.filtered_command_indices = self.filter_command_indices(&buffer);
1502                    if let Some(widget) = self.widgets.get_mut(widget_ids::SLASH_POPUP)
1503                        && let Some(popup) = widget.as_any_mut().downcast_mut::<SlashPopupState>()
1504                    {
1505                        popup.set_filtered_count(self.filtered_command_indices.len());
1506                    }
1507                }
1508            }
1509            KeyCode::Char(c) => {
1510                if let Some(input) = self.input_mut() {
1511                    input.insert_char(c);
1512                }
1513                let buffer = self
1514                    .input()
1515                    .map(|i| i.buffer().to_string())
1516                    .unwrap_or_default();
1517                self.filtered_command_indices = self.filter_command_indices(&buffer);
1518                if let Some(widget) = self.widgets.get_mut(widget_ids::SLASH_POPUP)
1519                    && let Some(popup) = widget.as_any_mut().downcast_mut::<SlashPopupState>()
1520                {
1521                    popup.set_filtered_count(self.filtered_command_indices.len());
1522                }
1523            }
1524            _ => {
1525                if let Some(widget) = self.widgets.get_mut(widget_ids::SLASH_POPUP)
1526                    && let Some(popup) = widget.as_any_mut().downcast_mut::<SlashPopupState>()
1527                {
1528                    popup.deactivate();
1529                }
1530            }
1531        }
1532    }
1533
1534    /// Execute slash command at the given index in filtered list
1535    fn execute_slash_command_at_index(&mut self, idx: usize) {
1536        // Get the command index from the filtered list
1537        if let Some(&cmd_idx) = self.filtered_command_indices.get(idx)
1538            && let Some(cmd) = self.commands.get(cmd_idx)
1539        {
1540            let cmd_name = cmd.name().to_string();
1541            if let Some(input) = self.input_mut() {
1542                input.clear();
1543                for c in format!("/{}", cmd_name).chars() {
1544                    input.insert_char(c);
1545                }
1546            }
1547            if let Some(widget) = self.widgets.get_mut(widget_ids::SLASH_POPUP)
1548                && let Some(popup) = widget.as_any_mut().downcast_mut::<SlashPopupState>()
1549            {
1550                popup.deactivate();
1551            }
1552            self.filtered_command_indices.clear();
1553            self.submit_message();
1554        }
1555    }
1556
1557    pub fn run(&mut self) -> io::Result<()> {
1558        enable_raw_mode()?;
1559        io::stdout().execute(EnterAlternateScreen)?;
1560        io::stdout().execute(EnableMouseCapture)?;
1561
1562        let mut terminal = Terminal::new(CrosstermBackend::new(io::stdout()))?;
1563
1564        while !self.should_quit {
1565            self.process_controller_messages();
1566
1567            let show_throbber = self.waiting_for_response
1568                || self.is_chat_streaming()
1569                || !self.executing_tools.is_empty();
1570
1571            // Advance animations
1572            if show_throbber {
1573                self.animation_frame_counter = self.animation_frame_counter.wrapping_add(1);
1574                if self.animation_frame_counter.is_multiple_of(6) {
1575                    self.throbber_state.calc_next();
1576                    self.conversation_view.step_spinner();
1577                }
1578            }
1579
1580            let prompt_len = PROMPT.chars().count();
1581            let indent_len = CONTINUATION_INDENT.len();
1582
1583            terminal.draw(|frame| {
1584                self.render_frame(frame, show_throbber, prompt_len, indent_len);
1585            })?;
1586
1587            // Event handling
1588            let mut net_scroll: i32 = 0;
1589
1590            while event::poll(std::time::Duration::from_millis(0))? {
1591                match event::read()? {
1592                    Event::Key(key) => {
1593                        if key.kind == KeyEventKind::Press {
1594                            self.handle_key(key.code, key.modifiers);
1595                        }
1596                    }
1597                    Event::Mouse(mouse) => match mouse.kind {
1598                        MouseEventKind::ScrollUp => net_scroll -= 1,
1599                        MouseEventKind::ScrollDown => net_scroll += 1,
1600                        _ => {}
1601                    },
1602                    _ => {}
1603                }
1604            }
1605
1606            // Apply scroll
1607            if net_scroll < 0 {
1608                for _ in 0..(-net_scroll) {
1609                    self.scroll_up();
1610                }
1611            } else if net_scroll > 0 {
1612                for _ in 0..net_scroll {
1613                    self.scroll_down();
1614                }
1615            }
1616
1617            if net_scroll == 0 {
1618                std::thread::sleep(std::time::Duration::from_millis(16));
1619            }
1620        }
1621
1622        io::stdout().execute(DisableMouseCapture)?;
1623        disable_raw_mode()?;
1624        io::stdout().execute(LeaveAlternateScreen)?;
1625
1626        Ok(())
1627    }
1628
1629    fn render_frame(
1630        &mut self,
1631        frame: &mut ratatui::Frame,
1632        show_throbber: bool,
1633        prompt_len: usize,
1634        indent_len: usize,
1635    ) {
1636        let frame_area = frame.area();
1637        let frame_width = frame_area.width as usize;
1638        let frame_height = frame_area.height;
1639        let theme = app_theme();
1640
1641        // Compute layout using the layout system
1642        let ctx =
1643            self.build_layout_context(frame_area, show_throbber, prompt_len, indent_len, &theme);
1644        let sizes = self.compute_widget_sizes(frame_height);
1645        let layout = self.layout_template.compute(&ctx, &sizes);
1646
1647        // Check overlay widget states (for cursor hiding)
1648        let theme_picker_active = sizes.is_active(widget_ids::THEME_PICKER);
1649        let session_picker_active = sizes.is_active(widget_ids::SESSION_PICKER);
1650        let question_panel_active = sizes.is_active(widget_ids::QUESTION_PANEL);
1651        let permission_panel_active = sizes.is_active(widget_ids::PERMISSION_PANEL);
1652        let batch_permission_panel_active = sizes.is_active(widget_ids::BATCH_PERMISSION_PANEL);
1653
1654        // Collect status bar data before taking mutable borrow
1655        let status_bar_data = StatusBarData {
1656            cwd: self.get_cwd(),
1657            model_name: self.model_name.clone(),
1658            context_used: self.context_used,
1659            context_limit: self.context_limit,
1660            session_id: self.session_id,
1661            status_hint: self.key_handler.status_hint(),
1662            is_waiting: show_throbber,
1663            waiting_elapsed: self.waiting_started.map(|t| t.elapsed()),
1664            input_empty: self.input().map(|i| i.is_empty()).unwrap_or(true),
1665            panels_active: question_panel_active
1666                || permission_panel_active
1667                || batch_permission_panel_active,
1668        };
1669
1670        // Update status bar with collected data
1671        if let Some(widget) = self.widgets.get_mut(widget_ids::STATUS_BAR)
1672            && let Some(status_bar) = widget.as_any_mut().downcast_mut::<StatusBar>()
1673        {
1674            status_bar.update_data(status_bar_data);
1675        }
1676
1677        // Render widgets in the order specified by the layout
1678        for widget_id in &layout.render_order {
1679            // Skip overlays (rendered last) and special widgets
1680            if *widget_id == widget_ids::THEME_PICKER || *widget_id == widget_ids::SESSION_PICKER {
1681                continue;
1682            }
1683
1684            // Get the area for this widget
1685            let Some(area) = layout.widget_areas.get(widget_id) else {
1686                continue;
1687            };
1688
1689            // Handle special widgets that need custom rendering
1690            match *widget_id {
1691                id if id == widget_ids::CHAT_VIEW => {
1692                    // Conversation view is rendered via the trait, not as a widget
1693                    let pending_status: Option<&str> = if !self.executing_tools.is_empty() {
1694                        Some(PENDING_STATUS_TOOLS)
1695                    } else if self.waiting_for_response && !self.is_chat_streaming() {
1696                        Some(PENDING_STATUS_LLM)
1697                    } else {
1698                        None
1699                    };
1700                    self.conversation_view
1701                        .render(frame, *area, &theme, pending_status);
1702                }
1703                id if id == widget_ids::TEXT_INPUT => {
1704                    // Input is rendered specially below (with throbber logic)
1705                }
1706                id if id == widget_ids::SLASH_POPUP => {
1707                    if let Some(widget) = self.widgets.get(widget_ids::SLASH_POPUP)
1708                        && let Some(popup_state) = widget.as_any().downcast_ref::<SlashPopupState>()
1709                    {
1710                        // Build filtered commands from indices
1711                        let filtered: Vec<&dyn SlashCommand> = self
1712                            .filtered_command_indices
1713                            .iter()
1714                            .filter_map(|&i| self.commands.get(i).map(|c| c.as_ref()))
1715                            .collect();
1716                        render_slash_popup(popup_state, &filtered, frame, *area, &theme);
1717                    }
1718                }
1719                _ => {
1720                    // Generic widget rendering
1721                    if let Some(widget) = self.widgets.get_mut(widget_id)
1722                        && widget.is_active()
1723                    {
1724                        widget.render(frame, *area, &theme);
1725                    }
1726                }
1727            }
1728        }
1729
1730        // Render input or throbber (special handling)
1731        if let Some(input_area) = layout.input_area
1732            && !question_panel_active
1733            && !permission_panel_active
1734            && !batch_permission_panel_active
1735        {
1736            if show_throbber {
1737                let default_message;
1738                let message = if let Some(msg) = &self.custom_throbber_message {
1739                    msg.as_str()
1740                } else if let Some(ref msg_fn) = self.processing_message_fn {
1741                    default_message = msg_fn();
1742                    &default_message
1743                } else {
1744                    &self.processing_message
1745                };
1746                let throbber = Throbber::default()
1747                    .label(message)
1748                    .style(theme.throbber_label)
1749                    .throbber_style(theme.throbber_spinner)
1750                    .throbber_set(BRAILLE_EIGHT_DOUBLE);
1751
1752                let throbber_block = Block::default()
1753                    .borders(Borders::TOP | Borders::BOTTOM)
1754                    .border_style(theme.input_border);
1755                let inner = throbber_block.inner(input_area);
1756                let throbber_inner = Rect::new(
1757                    inner.x + 1,
1758                    inner.y,
1759                    inner.width.saturating_sub(1),
1760                    inner.height,
1761                );
1762                frame.render_widget(throbber_block, input_area);
1763                frame.render_stateful_widget(throbber, throbber_inner, &mut self.throbber_state);
1764            } else if let Some(input) = self.input() {
1765                let input_lines: Vec<String> = input
1766                    .buffer()
1767                    .split('\n')
1768                    .enumerate()
1769                    .map(|(i, line)| {
1770                        if i == 0 {
1771                            format!("{}{}", PROMPT, line)
1772                        } else {
1773                            format!("{}{}", CONTINUATION_INDENT, line)
1774                        }
1775                    })
1776                    .collect();
1777                let input_text = if input_lines.is_empty() {
1778                    PROMPT.to_string()
1779                } else {
1780                    input_lines.join("\n")
1781                };
1782
1783                let input_box = Paragraph::new(input_text)
1784                    .block(
1785                        Block::default()
1786                            .borders(Borders::TOP | Borders::BOTTOM)
1787                            .border_style(theme.input_border),
1788                    )
1789                    .wrap(Wrap { trim: false });
1790                frame.render_widget(input_box, input_area);
1791
1792                // Only show cursor if no overlay is active
1793                if !theme_picker_active && !session_picker_active {
1794                    let (cursor_rel_x, cursor_rel_y) =
1795                        input.cursor_display_position_wrapped(frame_width, prompt_len, indent_len);
1796                    let cursor_x = input_area.x + cursor_rel_x;
1797                    let cursor_y = input_area.y + 1 + cursor_rel_y;
1798                    frame.set_cursor_position((cursor_x, cursor_y));
1799                }
1800            }
1801        }
1802
1803        // Render overlay widgets (theme picker, session picker) - always on top
1804        if theme_picker_active
1805            && let Some(widget) = self.widgets.get(widget_ids::THEME_PICKER)
1806            && let Some(picker) = widget.as_any().downcast_ref::<ThemePickerState>()
1807        {
1808            render_theme_picker(picker, frame, frame_area);
1809        }
1810
1811        if session_picker_active
1812            && let Some(widget) = self.widgets.get(widget_ids::SESSION_PICKER)
1813            && let Some(picker) = widget.as_any().downcast_ref::<SessionPickerState>()
1814        {
1815            render_session_picker(picker, frame, frame_area, &theme);
1816        }
1817    }
1818}
1819
1820impl Default for App {
1821    fn default() -> Self {
1822        Self::new()
1823    }
1824}