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