Skip to main content

limit_cli/
tui_bridge.rs

1use crate::agent_bridge::{AgentBridge, AgentEvent};
2use crate::clipboard::ClipboardManager;
3use crate::error::CliError;
4use crate::file_finder::FileFinder;
5use crate::session::SessionManager;
6use crossterm::event::{
7    self, DisableBracketedPaste, DisableMouseCapture, EnableBracketedPaste, EnableMouseCapture,
8    Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers, MouseButton, MouseEventKind,
9};
10use crossterm::execute;
11use crossterm::terminal::{EnterAlternateScreen, LeaveAlternateScreen};
12use limit_tui::components::{
13    calculate_popup_area, ActivityFeed, ChatView, FileAutocompleteWidget, FileMatchData, Message,
14    Spinner,
15};
16use ratatui::{
17    backend::CrosstermBackend,
18    layout::{Constraint, Direction, Layout},
19    style::{Color, Modifier, Style},
20    text::{Line, Span},
21    widgets::{Block, Borders, Paragraph, Wrap},
22    Frame, Terminal,
23};
24use std::io;
25use std::sync::{Arc, Mutex};
26use tokio::sync::mpsc;
27
28/// Maximum paste size to prevent memory issues (100KB)
29const MAX_PASTE_SIZE: usize = 100 * 1024;
30
31/// State for file autocomplete popup
32#[derive(Debug, Clone)]
33pub struct FileAutocompleteState {
34    /// Whether autocomplete popup is visible
35    pub is_active: bool,
36    /// Query typed after @ (e.g., "Cargo" in "@Cargo")
37    pub query: String,
38    /// Start position of @ in input_text
39    pub trigger_pos: usize,
40    /// List of matching files
41    pub matches: Vec<FileMatchData>,
42    /// Currently selected index in matches
43    pub selected_index: usize,
44}
45
46/// Debug log to file (bypasses tracing)
47fn debug_log(msg: &str) {
48    use std::fs::OpenOptions;
49    use std::io::Write;
50    if let Ok(mut file) = OpenOptions::new()
51        .create(true)
52        .append(true)
53        .open(std::env::var("HOME").unwrap_or_else(|_| ".".to_string()) + "/.limit/logs/tui.log")
54    {
55        let timestamp = chrono::Local::now().format("%H:%M:%S%.3f");
56        let _ = writeln!(file, "[{}] {}", timestamp, msg);
57    }
58}
59/// TUI state for displaying agent events
60#[derive(Debug, Clone, PartialEq, Default)]
61pub enum TuiState {
62    #[default]
63    Idle,
64    Thinking,
65}
66
67/// Bridge connecting limit-cli REPL to limit-tui components
68pub struct TuiBridge {
69    /// Agent bridge for processing messages (wrapped for thread-safe access)
70    agent_bridge: Arc<Mutex<AgentBridge>>,
71    /// Event receiver from the agent
72    event_rx: mpsc::UnboundedReceiver<AgentEvent>,
73    /// Current TUI state
74    state: Arc<Mutex<TuiState>>,
75    /// Chat view for displaying conversation
76    chat_view: Arc<Mutex<ChatView>>,
77    /// Activity feed for showing tool activities
78    activity_feed: Arc<Mutex<ActivityFeed>>,
79    /// Spinner for thinking state
80    /// Spinner for thinking state
81    spinner: Arc<Mutex<Spinner>>,
82    /// Conversation history
83    messages: Arc<Mutex<Vec<limit_llm::Message>>>,
84    /// Total input tokens for the session
85    total_input_tokens: Arc<Mutex<u64>>,
86    /// Total output tokens for the session
87    total_output_tokens: Arc<Mutex<u64>>,
88    /// Session manager for persistence
89    session_manager: Arc<Mutex<SessionManager>>,
90    /// Current session ID
91    session_id: Arc<Mutex<String>>,
92    /// Current operation ID (to ignore events from old operations)
93    operation_id: Arc<Mutex<u64>>,
94}
95
96impl TuiBridge {
97    /// Create a new TuiBridge with the given agent bridge and event channel
98    pub fn new(
99        agent_bridge: AgentBridge,
100        event_rx: mpsc::UnboundedReceiver<AgentEvent>,
101    ) -> Result<Self, CliError> {
102        let session_manager = SessionManager::new().map_err(|e| {
103            CliError::ConfigError(format!("Failed to create session manager: {}", e))
104        })?;
105
106        // Always create a new session on TUI startup
107        let session_id = session_manager
108            .create_new_session()
109            .map_err(|e| CliError::ConfigError(format!("Failed to create session: {}", e)))?;
110        tracing::info!("Created new TUI session: {}", session_id);
111
112        // Start with empty messages - never load previous session
113        let messages: Vec<limit_llm::Message> = Vec::new();
114
115        // Get token counts from session info
116        let sessions = session_manager.list_sessions().unwrap_or_default();
117        let session_info = sessions.iter().find(|s| s.id == session_id);
118        let initial_input = session_info.map(|s| s.total_input_tokens).unwrap_or(0);
119        let initial_output = session_info.map(|s| s.total_output_tokens).unwrap_or(0);
120
121        let chat_view = Arc::new(Mutex::new(ChatView::new()));
122
123        // Add loaded messages to chat view for display
124        for msg in &messages {
125            match msg.role {
126                limit_llm::Role::User => {
127                    let chat_msg = Message::user(msg.content.clone().unwrap_or_default());
128                    chat_view.lock().unwrap().add_message(chat_msg);
129                }
130                limit_llm::Role::Assistant => {
131                    let content = msg.content.clone().unwrap_or_default();
132                    let chat_msg = Message::assistant(content);
133                    chat_view.lock().unwrap().add_message(chat_msg);
134                }
135                limit_llm::Role::System => {
136                    // Skip system messages in display
137                }
138                limit_llm::Role::Tool => {
139                    // Skip tool messages in display
140                }
141            }
142        }
143
144        tracing::info!("Loaded {} messages into chat view", messages.len());
145
146        // Add system message to indicate this is a new session
147        let session_short_id = format!("...{}", &session_id[session_id.len().saturating_sub(8)..]);
148        let welcome_msg =
149            Message::system(format!("🆕 New TUI session started: {}", session_short_id));
150        chat_view.lock().unwrap().add_message(welcome_msg);
151
152        // Add model info as system message
153        let model_name = agent_bridge.model().to_string();
154        if !model_name.is_empty() {
155            let model_msg = Message::system(format!("Using model: {}", model_name));
156            chat_view.lock().unwrap().add_message(model_msg);
157        }
158
159        Ok(Self {
160            agent_bridge: Arc::new(Mutex::new(agent_bridge)),
161            event_rx,
162            state: Arc::new(Mutex::new(TuiState::Idle)),
163            chat_view,
164            activity_feed: Arc::new(Mutex::new(ActivityFeed::new())),
165            spinner: Arc::new(Mutex::new(Spinner::new("Thinking..."))),
166            messages: Arc::new(Mutex::new(messages)),
167            total_input_tokens: Arc::new(Mutex::new(initial_input)),
168            total_output_tokens: Arc::new(Mutex::new(initial_output)),
169            session_manager: Arc::new(Mutex::new(session_manager)),
170            session_id: Arc::new(Mutex::new(session_id)),
171            operation_id: Arc::new(Mutex::new(0)),
172        })
173    }
174
175    /// Get a clone of the agent bridge Arc for spawning tasks
176    pub fn agent_bridge_arc(&self) -> Arc<Mutex<AgentBridge>> {
177        self.agent_bridge.clone()
178    }
179
180    /// Get locked access to the agent bridge (for compatibility)
181    #[allow(dead_code)]
182    pub fn agent_bridge(&self) -> std::sync::MutexGuard<'_, AgentBridge> {
183        self.agent_bridge.lock().unwrap()
184    }
185
186    /// Get the current TUI state
187    pub fn state(&self) -> TuiState {
188        self.state.lock().unwrap().clone()
189    }
190
191    /// Get a reference to the chat view
192    pub fn chat_view(&self) -> &Arc<Mutex<ChatView>> {
193        &self.chat_view
194    }
195
196    /// Get a reference to the spinner
197    pub fn spinner(&self) -> &Arc<Mutex<Spinner>> {
198        &self.spinner
199    }
200
201    /// Get a reference to the activity feed
202    pub fn activity_feed(&self) -> &Arc<Mutex<ActivityFeed>> {
203        &self.activity_feed
204    }
205
206    /// Process events from the agent and update TUI state
207    pub fn process_events(&mut self) -> Result<(), CliError> {
208        let mut event_count = 0;
209        let current_op_id = self.operation_id();
210
211        while let Ok(event) = self.event_rx.try_recv() {
212            event_count += 1;
213
214            // Get operation_id from event
215            let event_op_id = match &event {
216                AgentEvent::Thinking { operation_id } => *operation_id,
217                AgentEvent::ToolStart { operation_id, .. } => *operation_id,
218                AgentEvent::ToolComplete { operation_id, .. } => *operation_id,
219                AgentEvent::ContentChunk { operation_id, .. } => *operation_id,
220                AgentEvent::Done { operation_id } => *operation_id,
221                AgentEvent::Cancelled { operation_id } => *operation_id,
222                AgentEvent::Error { operation_id, .. } => *operation_id,
223                AgentEvent::TokenUsage { operation_id, .. } => *operation_id,
224            };
225
226            debug_log(&format!(
227                "process_events: event_op_id={}, current_op_id={}, event={:?}",
228                event_op_id, current_op_id, std::mem::discriminant(&event)
229            ));
230
231            // Ignore events from old operations
232            if event_op_id != current_op_id {
233                debug_log(&format!(
234                    "process_events: Ignoring event from old operation {} (current: {})",
235                    event_op_id, current_op_id
236                ));
237                continue;
238            }
239
240            match event {
241                AgentEvent::Thinking { operation_id: _ } => {
242                    debug_log("process_events: Thinking event received - setting state to Thinking");
243                    *self.state.lock().unwrap() = TuiState::Thinking;
244                    debug_log(&format!("process_events: state is now {:?}", self.state()));
245                }
246                AgentEvent::ToolStart {
247                    operation_id: _,
248                    name,
249                    args,
250                } => {
251                    debug_log(&format!("process_events: ToolStart event - {}", name));
252                    let activity_msg = Self::format_activity_message(&name, &args);
253                    // Add to activity feed instead of changing state
254                    self.activity_feed.lock().unwrap().add(activity_msg, true);
255                }
256                AgentEvent::ToolComplete {
257                    operation_id: _,
258                    name: _,
259                    result: _,
260                } => {
261                    debug_log("process_events: ToolComplete event");
262                    // Mark current activity as complete
263                    self.activity_feed.lock().unwrap().complete_current();
264                }
265                AgentEvent::ContentChunk {
266                    operation_id: _,
267                    chunk,
268                } => {
269                    debug_log(&format!(
270                        "process_events: ContentChunk event ({} chars)",
271                        chunk.len()
272                    ));
273                    self.chat_view
274                        .lock()
275                        .unwrap()
276                        .append_to_last_assistant(&chunk);
277                }
278                AgentEvent::Done { operation_id: _ } => {
279                    debug_log("process_events: Done event received");
280                    *self.state.lock().unwrap() = TuiState::Idle;
281                    // Mark all activities as complete when LLM finishes
282                    self.activity_feed.lock().unwrap().complete_all();
283                }
284                AgentEvent::Cancelled { operation_id: _ } => {
285                    debug_log("process_events: Cancelled event received");
286                    *self.state.lock().unwrap() = TuiState::Idle;
287                    // Mark all activities as complete
288                    self.activity_feed.lock().unwrap().complete_all();
289                }
290                AgentEvent::Error {
291                    operation_id: _,
292                    message,
293                } => {
294                    debug_log(&format!("process_events: Error event - {}", message));
295                    // Reset state to Idle so user can continue
296                    *self.state.lock().unwrap() = TuiState::Idle;
297                    let chat_msg = Message::system(format!("Error: {}", message));
298                    self.chat_view.lock().unwrap().add_message(chat_msg);
299                }
300                AgentEvent::TokenUsage {
301                    operation_id: _,
302                    input_tokens,
303                    output_tokens,
304                } => {
305                    debug_log(&format!(
306                        "process_events: TokenUsage event - in={}, out={}",
307                        input_tokens, output_tokens
308                    ));
309                    // Accumulate token counts for display
310                    *self.total_input_tokens.lock().unwrap() += input_tokens;
311                    *self.total_output_tokens.lock().unwrap() += output_tokens;
312                }
313            }
314        }
315        if event_count > 0 {
316            debug_log(&format!("process_events: processed {} events", event_count));
317        }
318        Ok(())
319    }
320
321    fn format_activity_message(tool_name: &str, args: &serde_json::Value) -> String {
322        match tool_name {
323            "file_read" => args
324                .get("path")
325                .and_then(|p| p.as_str())
326                .map(|p| format!("Reading {}...", Self::truncate_path(p, 40)))
327                .unwrap_or_else(|| "Reading file...".to_string()),
328            "file_write" => args
329                .get("path")
330                .and_then(|p| p.as_str())
331                .map(|p| format!("Writing {}...", Self::truncate_path(p, 40)))
332                .unwrap_or_else(|| "Writing file...".to_string()),
333            "file_edit" => args
334                .get("path")
335                .and_then(|p| p.as_str())
336                .map(|p| format!("Editing {}...", Self::truncate_path(p, 40)))
337                .unwrap_or_else(|| "Editing file...".to_string()),
338            "bash" => args
339                .get("command")
340                .and_then(|c| c.as_str())
341                .map(|c| format!("Running {}...", Self::truncate_command(c, 30)))
342                .unwrap_or_else(|| "Executing command...".to_string()),
343            "git_status" => "Checking git status...".to_string(),
344            "git_diff" => "Checking git diff...".to_string(),
345            "git_log" => "Checking git log...".to_string(),
346            "git_add" => "Staging files...".to_string(),
347            "git_commit" => "Creating commit...".to_string(),
348            "git_push" => "Pushing to remote...".to_string(),
349            "git_pull" => "Pulling from remote...".to_string(),
350            "git_clone" => args
351                .get("url")
352                .and_then(|u| u.as_str())
353                .map(|u| format!("Cloning {}...", Self::truncate_path(u, 40)))
354                .unwrap_or_else(|| "Cloning repository...".to_string()),
355            "grep" => args
356                .get("pattern")
357                .and_then(|p| p.as_str())
358                .map(|p| format!("Searching for '{}'...", Self::truncate_command(p, 30)))
359                .unwrap_or_else(|| "Searching...".to_string()),
360            "ast_grep" => args
361                .get("pattern")
362                .and_then(|p| p.as_str())
363                .map(|p| format!("AST searching '{}'...", Self::truncate_command(p, 25)))
364                .unwrap_or_else(|| "AST searching...".to_string()),
365            "lsp" => args
366                .get("command")
367                .and_then(|c| c.as_str())
368                .map(|c| format!("Running LSP {}...", c))
369                .unwrap_or_else(|| "Running LSP...".to_string()),
370            _ => format!("Executing {}...", tool_name),
371        }
372    }
373
374    fn truncate_path(s: &str, max_len: usize) -> String {
375        if s.len() <= max_len {
376            s.to_string()
377        } else {
378            format!("...{}", &s[s.len().saturating_sub(max_len - 3)..])
379        }
380    }
381
382    fn truncate_command(s: &str, max_len: usize) -> String {
383        if s.len() <= max_len {
384            s.to_string()
385        } else {
386            format!("{}...", &s[..max_len.saturating_sub(3)])
387        }
388    }
389
390    /// Add a user message to the chat
391    pub fn add_user_message(&self, content: String) {
392        let msg = Message::user(content);
393        self.chat_view.lock().unwrap().add_message(msg);
394    }
395
396    /// Tick the spinner animation
397    pub fn tick_spinner(&self) {
398        self.spinner.lock().unwrap().tick();
399    }
400
401    /// Check if agent is busy
402    pub fn is_busy(&self) -> bool {
403        !matches!(self.state(), TuiState::Idle)
404    }
405
406    /// Get current operation ID
407    pub fn operation_id(&self) -> u64 {
408        self.operation_id.lock().map(|id| *id).unwrap_or(0)
409    }
410
411    /// Increment and get new operation ID
412    pub fn next_operation_id(&self) -> u64 {
413        if let Ok(mut id) = self.operation_id.lock() {
414            *id += 1;
415            *id
416        } else {
417            0
418        }
419    }
420
421    /// Get total input tokens for the session
422    pub fn total_input_tokens(&self) -> u64 {
423        self.total_input_tokens
424            .lock()
425            .map(|guard| *guard)
426            .unwrap_or(0)
427    }
428
429    /// Get total output tokens for the session
430    pub fn total_output_tokens(&self) -> u64 {
431        self.total_output_tokens
432            .lock()
433            .map(|guard| *guard)
434            .unwrap_or(0)
435    }
436
437    /// Get the current session ID
438    pub fn session_id(&self) -> String {
439        self.session_id
440            .lock()
441            .map(|guard| guard.clone())
442            .unwrap_or_else(|_| String::from("unknown"))
443    }
444
445    /// Save the current session
446    pub fn save_session(&self) -> Result<(), CliError> {
447        let session_id = self
448            .session_id
449            .lock()
450            .map(|guard| guard.clone())
451            .unwrap_or_else(|_| String::from("unknown"));
452
453        let messages = self
454            .messages
455            .lock()
456            .map(|guard| guard.clone())
457            .unwrap_or_default();
458
459        let input_tokens = self
460            .total_input_tokens
461            .lock()
462            .map(|guard| *guard)
463            .unwrap_or(0);
464
465        let output_tokens = self
466            .total_output_tokens
467            .lock()
468            .map(|guard| *guard)
469            .unwrap_or(0);
470
471        tracing::debug!(
472            "Saving session {} with {} messages, {} in tokens, {} out tokens",
473            session_id,
474            messages.len(),
475            input_tokens,
476            output_tokens
477        );
478
479        let session_manager = self.session_manager.lock().map_err(|e| {
480            CliError::ConfigError(format!("Failed to acquire session manager lock: {}", e))
481        })?;
482
483        session_manager.save_session(&session_id, &messages, input_tokens, output_tokens)?;
484        tracing::info!(
485            "✓ Session {} saved successfully ({} messages, {} in tokens, {} out tokens)",
486            session_id,
487            messages.len(),
488            input_tokens,
489            output_tokens
490        );
491        Ok(())
492    }
493}
494
495/// TUI Application for running the limit CLI in a terminal UI
496pub struct TuiApp {
497    tui_bridge: TuiBridge,
498    terminal: Terminal<CrosstermBackend<io::Stdout>>,
499    running: bool,
500    input_text: String,
501    cursor_pos: usize,
502    status_message: String,
503    status_is_error: bool,
504    cursor_blink_state: bool,
505    cursor_blink_timer: std::time::Instant,
506    /// Mouse selection state
507    mouse_selection_start: Option<(u16, u16)>,
508    /// Clipboard manager
509    clipboard: Option<ClipboardManager>,
510    /// File autocomplete state
511    file_autocomplete: Option<FileAutocompleteState>,
512    /// File finder instance
513    file_finder: FileFinder,
514    /// Last ESC press time for double-ESC detection
515    last_esc_time: Option<std::time::Instant>,
516    /// Cancellation token for current LLM operation
517    cancellation_token: Option<tokio_util::sync::CancellationToken>,
518}
519
520impl TuiApp {
521    /// Create a new TUI application
522    pub fn new(tui_bridge: TuiBridge) -> Result<Self, CliError> {
523        let backend = CrosstermBackend::new(io::stdout());
524        let terminal =
525            Terminal::new(backend).map_err(|e| CliError::IoError(io::Error::other(e)))?;
526
527        let session_id = tui_bridge.session_id();
528        tracing::info!("TUI started with session: {}", session_id);
529
530        let clipboard = match ClipboardManager::new() {
531            Ok(cb) => {
532                debug_log("✓ Clipboard initialized successfully");
533                tracing::info!("Clipboard initialized successfully");
534                Some(cb)
535            }
536            Err(e) => {
537                debug_log(&format!("✗ Clipboard initialization failed: {}", e));
538                tracing::warn!("Clipboard unavailable: {}", e);
539                None
540            }
541        };
542
543        // Initialize file finder with current directory
544        let working_dir = std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("."));
545        let file_finder = FileFinder::new(working_dir);
546
547        Ok(Self {
548            tui_bridge,
549            terminal,
550            running: true,
551            input_text: String::new(),
552            cursor_pos: 0,
553            status_message: "Ready - Type a message and press Enter".to_string(),
554            status_is_error: false,
555            cursor_blink_state: true,
556            cursor_blink_timer: std::time::Instant::now(),
557            mouse_selection_start: None,
558            clipboard,
559            file_autocomplete: None,
560            file_finder,
561            last_esc_time: None,
562            cancellation_token: None,
563        })
564    }
565
566    /// Run the TUI event loop
567    pub fn run(&mut self) -> Result<(), CliError> {
568        // Enter alternate screen - creates a clean buffer for TUI
569        execute!(std::io::stdout(), EnterAlternateScreen)
570            .map_err(|e| CliError::IoError(io::Error::other(e)))?;
571
572        // Enable mouse capture for scroll support
573        execute!(std::io::stdout(), EnableMouseCapture)
574            .map_err(|e| CliError::IoError(io::Error::other(e)))?;
575
576        // Enable bracketed paste for multi-line paste support
577        execute!(std::io::stdout(), EnableBracketedPaste)
578            .map_err(|e| CliError::IoError(io::Error::other(e)))?;
579
580        crossterm::terminal::enable_raw_mode()
581            .map_err(|e| CliError::IoError(io::Error::other(e)))?;
582
583        // Guard to ensure cleanup on panic
584        struct AlternateScreenGuard;
585        impl Drop for AlternateScreenGuard {
586            fn drop(&mut self) {
587                let _ = crossterm::terminal::disable_raw_mode();
588                let _ = execute!(std::io::stdout(), DisableBracketedPaste);
589                let _ = execute!(std::io::stdout(), DisableMouseCapture);
590                let _ = execute!(std::io::stdout(), LeaveAlternateScreen);
591            }
592        }
593        let _guard = AlternateScreenGuard;
594
595        self.run_inner()
596    }
597
598    fn run_inner(&mut self) -> Result<(), CliError> {
599        while self.running {
600            // Process events from the agent
601            self.tui_bridge.process_events()?;
602
603            // Update spinner if in thinking state
604            if matches!(self.tui_bridge.state(), TuiState::Thinking) {
605                self.tui_bridge.tick_spinner();
606            }
607
608            // Update status based on state
609            self.update_status();
610
611            // Handle user input with poll timeout
612            if crossterm::event::poll(std::time::Duration::from_millis(100))
613                .map_err(|e| CliError::IoError(io::Error::other(e)))?
614            {
615                match event::read().map_err(|e| CliError::IoError(io::Error::other(e)))? {
616                    Event::Key(key) => {
617                        // Log ALL key events (not just Press) to debug modifier detection
618                        debug_log(&format!(
619                            "Event::Key - code={:?} mod={:?} kind={:?}",
620                            key.code, key.modifiers, key.kind
621                        ));
622
623                        if key.kind == KeyEventKind::Press {
624                            self.handle_key_event(key)?;
625                        }
626                    }
627                    Event::Mouse(mouse) => {
628                        match mouse.kind {
629                            MouseEventKind::Down(MouseButton::Left) => {
630                                debug_log(&format!(
631                                    "MouseDown at ({}, {})",
632                                    mouse.column, mouse.row
633                                ));
634                                self.mouse_selection_start = Some((mouse.column, mouse.row));
635                                // Map screen position to message/offset and start selection
636                                let chat = self.tui_bridge.chat_view().lock().unwrap();
637                                debug_log(&format!(
638                                    "  render_positions count: {}",
639                                    chat.render_position_count()
640                                ));
641                                if let Some((msg_idx, char_offset)) =
642                                    chat.screen_to_text_pos(mouse.column, mouse.row)
643                                {
644                                    debug_log(&format!(
645                                        "  -> Starting selection at msg={}, offset={}",
646                                        msg_idx, char_offset
647                                    ));
648                                    drop(chat);
649                                    self.tui_bridge
650                                        .chat_view()
651                                        .lock()
652                                        .unwrap()
653                                        .start_selection(msg_idx, char_offset);
654                                } else {
655                                    debug_log("  -> No match, clearing selection");
656                                    drop(chat);
657                                    self.tui_bridge
658                                        .chat_view()
659                                        .lock()
660                                        .unwrap()
661                                        .clear_selection();
662                                }
663                            }
664                            MouseEventKind::Drag(MouseButton::Left) => {
665                                if self.mouse_selection_start.is_some() {
666                                    // Extend selection to current position
667                                    let chat = self.tui_bridge.chat_view().lock().unwrap();
668                                    if let Some((msg_idx, char_offset)) =
669                                        chat.screen_to_text_pos(mouse.column, mouse.row)
670                                    {
671                                        drop(chat);
672                                        self.tui_bridge
673                                            .chat_view()
674                                            .lock()
675                                            .unwrap()
676                                            .extend_selection(msg_idx, char_offset);
677                                    }
678                                }
679                            }
680                            MouseEventKind::Up(MouseButton::Left) => {
681                                self.mouse_selection_start = None;
682                            }
683                            MouseEventKind::ScrollUp => {
684                                let mut chat = self.tui_bridge.chat_view().lock().unwrap();
685                                chat.scroll_up();
686                            }
687                            MouseEventKind::ScrollDown => {
688                                let mut chat = self.tui_bridge.chat_view().lock().unwrap();
689                                chat.scroll_down();
690                            }
691                            _ => {}
692                        }
693                    }
694                    Event::Paste(pasted) => {
695                        if !self.tui_bridge.is_busy() {
696                            self.insert_paste(&pasted);
697                        }
698                    }
699                    _ => {}
700                }
701            } else {
702                // No key event - tick cursor blink
703                self.tick_cursor_blink();
704            }
705
706            // Draw the TUI
707            self.draw()?;
708        }
709
710        // Save session before exiting
711        if let Err(e) = self.tui_bridge.save_session() {
712            tracing::error!("Failed to save session: {}", e);
713        }
714
715        Ok(())
716    }
717
718    fn update_status(&mut self) {
719        let session_id = self.tui_bridge.session_id();
720        let has_activity = self
721            .tui_bridge
722            .activity_feed()
723            .lock()
724            .unwrap()
725            .has_in_progress();
726
727        match self.tui_bridge.state() {
728            TuiState::Idle => {
729                if has_activity {
730                    // Show spinner when there are in-progress activities
731                    let spinner = self.tui_bridge.spinner().lock().unwrap();
732                    self.status_message = format!("{} Processing...", spinner.current_frame());
733                } else {
734                    self.status_message = format!(
735                        "Ready | Session: {}",
736                        session_id.chars().take(8).collect::<String>()
737                    );
738                }
739                self.status_is_error = false;
740            }
741            TuiState::Thinking => {
742                let spinner = self.tui_bridge.spinner().lock().unwrap();
743                self.status_message = format!("{} Thinking...", spinner.current_frame());
744                self.status_is_error = false;
745            }
746        }
747    }
748
749    /// Insert pasted text at cursor position without submitting
750    fn insert_paste(&mut self, text: &str) {
751        // Enforce size limit to prevent memory issues
752        let text = if text.len() > MAX_PASTE_SIZE {
753            self.status_message = "Paste truncated (too large)".to_string();
754            self.status_is_error = true;
755            // Find valid UTF-8 boundary at approximately MAX_PASTE_SIZE
756            &text[..text
757                .char_indices()
758                .nth(MAX_PASTE_SIZE)
759                .map(|(i, _)| i)
760                .unwrap_or(text.len())]
761        } else {
762            text
763        };
764
765        // Normalize newlines (some terminals convert \n to \r)
766        let normalized = text.replace("\r", "\n");
767        self.input_text.insert_str(self.cursor_pos, &normalized);
768        self.cursor_pos += normalized.len();
769    }
770
771    /// Check if the current key event is a copy/paste shortcut
772    /// Returns true for Ctrl+C/V on Linux/Windows, Cmd+C/V on macOS
773    /// Note: Some macOS terminals report Cmd as CONTROL instead of SUPER
774    fn is_copy_paste_modifier(&self, key: &KeyEvent, char: char) -> bool {
775        #[cfg(target_os = "macos")]
776        {
777            // Accept both SUPER and CONTROL on macOS since terminal emulators vary
778            let has_super = key.modifiers.contains(KeyModifiers::SUPER);
779            let has_ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
780            let result = key.code == KeyCode::Char(char) && (has_super || has_ctrl);
781            debug_log(&format!("is_copy_paste_modifier('{}') macOS: code={:?}, mod={:?}, super={}, ctrl={}, result={}", 
782                char, key.code, key.modifiers, has_super, has_ctrl, result));
783            result
784        }
785        #[cfg(not(target_os = "macos"))]
786        {
787            let result =
788                key.code == KeyCode::Char(char) && key.modifiers.contains(KeyModifiers::CONTROL);
789            debug_log(&format!(
790                "is_copy_paste_modifier('{}') non-macOS: code={:?}, mod={:?}, ctrl={:?}, result={}",
791                char,
792                key.code,
793                key.modifiers,
794                KeyModifiers::CONTROL,
795                result
796            ));
797            result
798        }
799    }
800
801    fn tick_cursor_blink(&mut self) {
802        // Blink every 500ms for standard terminal cursor behavior
803        if self.cursor_blink_timer.elapsed().as_millis() > 500 {
804            self.cursor_blink_state = !self.cursor_blink_state;
805            self.cursor_blink_timer = std::time::Instant::now();
806        }
807    }
808
809    fn handle_key_event(&mut self, key: KeyEvent) -> Result<(), CliError> {
810        // Direct file logging (always works)
811        debug_log(&format!(
812            "handle_key_event: code={:?} mod={:?} kind={:?}",
813            key.code, key.modifiers, key.kind
814        ));
815
816        // Special log for 'c' and 'v' keys to debug copy/paste
817        if matches!(key.code, KeyCode::Char('c') | KeyCode::Char('v')) {
818            debug_log(&format!(
819                ">>> SPECIAL: '{}' key detected with modifiers: {:?} (SUPER={:?}, CONTROL={:?})",
820                if matches!(key.code, KeyCode::Char('c')) {
821                    'c'
822                } else {
823                    'v'
824                },
825                key.modifiers,
826                key.modifiers.contains(KeyModifiers::SUPER),
827                key.modifiers.contains(KeyModifiers::CONTROL)
828            ));
829        }
830
831        // Copy selection to clipboard (Ctrl/Cmd+C)
832        if self.is_copy_paste_modifier(&key, 'c') {
833            debug_log("✓ Copy shortcut CONFIRMED - processing...");
834            let mut chat = self.tui_bridge.chat_view().lock().unwrap();
835            let has_selection = chat.has_selection();
836            debug_log(&format!("has_selection={}", has_selection));
837
838            if has_selection {
839                if let Some(selected) = chat.get_selected_text() {
840                    debug_log(&format!("Selected text length={}", selected.len()));
841                    if !selected.is_empty() {
842                        if let Some(ref clipboard) = self.clipboard {
843                            debug_log("Attempting to copy to clipboard...");
844                            match clipboard.set_text(&selected) {
845                                Ok(()) => {
846                                    debug_log("✓ Clipboard copy successful");
847                                    self.status_message = "Copied to clipboard".to_string();
848                                    self.status_is_error = false;
849                                }
850                                Err(e) => {
851                                    debug_log(&format!("✗ Clipboard copy failed: {}", e));
852                                    self.status_message = format!("Clipboard error: {}", e);
853                                    self.status_is_error = true;
854                                }
855                            }
856                        } else {
857                            debug_log("✗ Clipboard not available (None)");
858                            self.status_message = "Clipboard not available".to_string();
859                            self.status_is_error = true;
860                        }
861                    } else {
862                        debug_log("Selected text is empty");
863                    }
864                    chat.clear_selection();
865                } else {
866                    debug_log("get_selected_text() returned None");
867                }
868                return Ok(());
869            }
870
871            // No selection - do nothing (Ctrl+C is only for copying text)
872            debug_log("Ctrl/Cmd+C with no selection - ignoring");
873            return Ok(());
874        }
875
876        // Paste from clipboard (Ctrl/Cmd+V)
877        if self.is_copy_paste_modifier(&key, 'v') && !self.tui_bridge.is_busy() {
878            debug_log("✓ Paste shortcut CONFIRMED - processing...");
879            if let Some(ref clipboard) = self.clipboard {
880                debug_log("Attempting to read from clipboard...");
881                match clipboard.get_text() {
882                    Ok(text) if !text.is_empty() => {
883                        debug_log(&format!("Read {} chars from clipboard", text.len()));
884                        self.insert_paste(&text);
885                    }
886                    Ok(_) => {
887                        debug_log("Clipboard is empty");
888                    } // Empty clipboard
889                    Err(e) => {
890                        debug_log(&format!("✗ Failed to read clipboard: {}", e));
891                        self.status_message = format!("Could not read clipboard: {}", e);
892                        self.status_is_error = true;
893                    }
894                }
895            } else {
896                debug_log("✗ Clipboard not available (None)");
897                self.status_message = "Clipboard not available".to_string();
898                self.status_is_error = true;
899            }
900            return Ok(());
901        }
902
903        // Handle autocomplete navigation FIRST (before general scrolling)
904        if let Some(ref mut ac) = self.file_autocomplete {
905            if ac.is_active {
906                match key.code {
907                    KeyCode::Up => {
908                        if ac.selected_index > 0 {
909                            ac.selected_index -= 1;
910                        }
911                        return Ok(());
912                    }
913                    KeyCode::Down => {
914                        if ac.selected_index + 1 < ac.matches.len() {
915                            ac.selected_index += 1;
916                        }
917                        return Ok(());
918                    }
919                    KeyCode::Enter | KeyCode::Tab => {
920                        // Accept selected completion
921                        self.accept_file_completion();
922                        return Ok(());
923                    }
924                    KeyCode::Esc => {
925                        // Cancel autocomplete
926                        self.file_autocomplete = None;
927                        return Ok(());
928                    }
929                    _ => {}
930                }
931            }
932        }
933
934        // Handle ESC for cancellation (must be before is_busy check)
935        if key.code == KeyCode::Esc {
936            // If autocomplete is active, cancel it
937            if self.file_autocomplete.is_some() {
938                self.file_autocomplete = None;
939            } else if self.tui_bridge.is_busy() {
940                // Double-ESC to cancel current operation
941                let now = std::time::Instant::now();
942                let should_cancel = if let Some(last_esc) = self.last_esc_time {
943                    now.duration_since(last_esc) < std::time::Duration::from_millis(1000)
944                } else {
945                    false
946                };
947
948                if should_cancel {
949                    self.cancel_current_operation();
950                } else {
951                    // First ESC - show feedback
952                    self.status_message = "Press ESC again to cancel".to_string();
953                    self.status_is_error = false;
954                    self.last_esc_time = Some(now);
955                }
956            } else {
957                debug_log("Esc pressed, exiting");
958                self.running = false;
959            }
960            return Ok(());
961        }
962
963        // Allow scrolling even when agent is busy
964        // Calculate actual viewport height dynamically
965        let term_height = self.terminal.size().map(|s| s.height).unwrap_or(24);
966        let viewport_height = term_height
967            .saturating_sub(1) // status bar - input area (6 lines) - borders (~2)
968            .saturating_sub(7); // status (1) + input (6) + top/bottom borders (2) = 9
969        match key.code {
970            KeyCode::PageUp => {
971                let mut chat = self.tui_bridge.chat_view().lock().unwrap();
972                chat.scroll_page_up(viewport_height);
973                return Ok(());
974            }
975            KeyCode::PageDown => {
976                let mut chat = self.tui_bridge.chat_view().lock().unwrap();
977                chat.scroll_page_down(viewport_height);
978                return Ok(());
979            }
980            KeyCode::Up => {
981                let mut chat = self.tui_bridge.chat_view().lock().unwrap();
982                chat.scroll_up();
983                return Ok(());
984            }
985            KeyCode::Down => {
986                // Otherwise scroll chat
987                let mut chat = self.tui_bridge.chat_view().lock().unwrap();
988                chat.scroll_down();
989                return Ok(());
990            }
991            _ => {}
992        }
993
994        // Don't accept input while agent is busy
995        if self.tui_bridge.is_busy() {
996            debug_log("Agent busy, ignoring");
997            return Ok(());
998        }
999
1000        // Handle backspace - try multiple detection methods
1001        if self.handle_backspace(&key) {
1002            debug_log(&format!("Backspace handled, input: {:?}", self.input_text));
1003            return Ok(());
1004        }
1005
1006        match key.code {
1007            KeyCode::Delete => {
1008                if self.cursor_pos < self.input_text.len() {
1009                    let next_pos = self.next_char_pos();
1010                    self.input_text.drain(self.cursor_pos..next_pos);
1011                    debug_log(&format!("Delete: input now: {:?}", self.input_text));
1012                }
1013            }
1014            KeyCode::Left => {
1015                if self.cursor_pos > 0 {
1016                    self.cursor_pos = self.prev_char_pos();
1017                }
1018            }
1019            KeyCode::Right => {
1020                if self.cursor_pos < self.input_text.len() {
1021                    self.cursor_pos = self.next_char_pos();
1022                }
1023            }
1024            KeyCode::Home => {
1025                self.cursor_pos = 0;
1026            }
1027            KeyCode::End => {
1028                self.cursor_pos = self.input_text.len();
1029            }
1030            KeyCode::Enter => {
1031                // If autocomplete is active, it's already handled above
1032                self.handle_enter()?;
1033            }
1034            // Regular character input (including UTF-8)
1035            KeyCode::Char(c)
1036                if key.modifiers == KeyModifiers::NONE || key.modifiers == KeyModifiers::SHIFT =>
1037            {
1038                // Check if we're triggering autocomplete with @
1039                if c == '@' {
1040                    // Insert @ character
1041                    self.input_text.insert(self.cursor_pos, '@');
1042                    self.cursor_pos += 1;
1043
1044                    // Activate autocomplete
1045                    self.activate_file_autocomplete();
1046                } else {
1047                    // Check if autocomplete is active
1048                    let is_autocomplete_active = self
1049                        .file_autocomplete
1050                        .as_ref()
1051                        .map(|ac| ac.is_active)
1052                        .unwrap_or(false);
1053
1054                    if is_autocomplete_active {
1055                        // Clone query to avoid borrow issues
1056                        let query = self
1057                            .file_autocomplete
1058                            .as_ref()
1059                            .map(|ac| {
1060                                let mut q = ac.query.clone();
1061                                q.push(c);
1062                                q
1063                            })
1064                            .unwrap_or_default();
1065
1066                        // Get matches
1067                        let matches = self.get_file_matches(&query);
1068
1069                        // Update autocomplete state
1070                        if let Some(ref mut ac) = self.file_autocomplete {
1071                            ac.query.push(c);
1072                            ac.matches = matches;
1073                            ac.selected_index = 0;
1074                        }
1075
1076                        // Also insert the character in input
1077                        self.input_text.insert(self.cursor_pos, c);
1078                        self.cursor_pos += c.len_utf8();
1079                    } else {
1080                        // Normal character insertion
1081                        self.input_text.insert(self.cursor_pos, c);
1082                        self.cursor_pos += c.len_utf8();
1083                    }
1084                }
1085            }
1086            _ => {
1087                // Ignore other keys
1088            }
1089        }
1090
1091        Ok(())
1092    }
1093
1094    /// Handle backspace with multiple detection methods
1095    fn handle_backspace(&mut self, key: &KeyEvent) -> bool {
1096        // Method 1: Standard Backspace keycode
1097        if key.code == KeyCode::Backspace {
1098            debug_log("Backspace detected via KeyCode::Backspace");
1099            self.delete_char_before_cursor();
1100            return true;
1101        }
1102
1103        // Method 2: Ctrl+H (common backspace mapping)
1104        if key.code == KeyCode::Char('h') && key.modifiers == KeyModifiers::CONTROL {
1105            debug_log("Backspace detected via Ctrl+H");
1106            self.delete_char_before_cursor();
1107            return true;
1108        }
1109
1110        // Method 3: Check for DEL (127) or BS (8) characters
1111        if let KeyCode::Char(c) = key.code {
1112            if c == '\x7f' || c == '\x08' {
1113                debug_log(&format!("Backspace detected via char code: {}", c as u8));
1114                self.delete_char_before_cursor();
1115                return true;
1116            }
1117        }
1118
1119        false
1120    }
1121
1122    fn delete_char_before_cursor(&mut self) {
1123        debug_log(&format!(
1124            "delete_char: cursor={}, len={}, input={:?}",
1125            self.cursor_pos,
1126            self.input_text.len(),
1127            self.input_text
1128        ));
1129
1130        // If autocomplete is active, handle backspace specially
1131        let should_close_autocomplete = if let Some(ref ac) = self.file_autocomplete {
1132            if ac.is_active {
1133                // Check if query is empty
1134                ac.query.is_empty()
1135            } else {
1136                false
1137            }
1138        } else {
1139            false
1140        };
1141
1142        if should_close_autocomplete {
1143            // Need to delete the @ from input
1144            if self.cursor_pos > 0 {
1145                let prev_pos = self.prev_char_pos();
1146                if &self.input_text[prev_pos..self.cursor_pos] == "@" {
1147                    self.input_text.drain(prev_pos..self.cursor_pos);
1148                    self.cursor_pos = prev_pos;
1149                    self.file_autocomplete = None;
1150                    return;
1151                }
1152            }
1153        }
1154
1155        // Update autocomplete query if active
1156        if self
1157            .file_autocomplete
1158            .as_ref()
1159            .map(|ac| ac.is_active && !ac.query.is_empty())
1160            .unwrap_or(false)
1161        {
1162            let new_query = self
1163                .file_autocomplete
1164                .as_ref()
1165                .map(|ac| {
1166                    let mut q = ac.query.clone();
1167                    q.pop();
1168                    q
1169                })
1170                .unwrap_or_default();
1171
1172            let matches = self.get_file_matches(&new_query);
1173
1174            if let Some(ref mut ac) = self.file_autocomplete {
1175                ac.query.pop();
1176                ac.matches = matches;
1177                ac.selected_index = 0;
1178            }
1179        }
1180
1181        if self.cursor_pos > 0 {
1182            let prev_pos = self.prev_char_pos();
1183            debug_log(&format!("draining {}..{}", prev_pos, self.cursor_pos));
1184            self.input_text.drain(prev_pos..self.cursor_pos);
1185            self.cursor_pos = prev_pos;
1186            debug_log(&format!(
1187                "after delete: cursor={}, input={:?}",
1188                self.cursor_pos, self.input_text
1189            ));
1190        } else {
1191            debug_log("cursor at 0, nothing to delete");
1192        }
1193    }
1194
1195    /// Activate file autocomplete
1196    fn activate_file_autocomplete(&mut self) {
1197        let matches = self.get_file_matches("");
1198
1199        self.file_autocomplete = Some(FileAutocompleteState {
1200            is_active: true,
1201            query: String::new(),
1202            trigger_pos: self.cursor_pos - 1, // Position of @
1203            matches,
1204            selected_index: 0,
1205        });
1206
1207        debug_log(&format!(
1208            "Activated autocomplete at pos {}",
1209            self.cursor_pos - 1
1210        ));
1211    }
1212
1213    /// Get file matches for autocomplete
1214    fn get_file_matches(&mut self, query: &str) -> Vec<FileMatchData> {
1215        let files = self.file_finder.scan_files().clone();
1216        let matches = self.file_finder.filter_files(&files, query);
1217
1218        matches
1219            .into_iter()
1220            .map(|m| FileMatchData {
1221                path: m.path.to_string_lossy().to_string(),
1222                is_dir: m.is_dir,
1223            })
1224            .collect()
1225    }
1226
1227    /// Accept selected file completion
1228    fn accept_file_completion(&mut self) {
1229        if let Some(ref ac) = self.file_autocomplete {
1230            if let Some(selected) = ac.matches.get(ac.selected_index) {
1231                // Replace @query with @path
1232                let end_pos = self.cursor_pos;
1233
1234                // Calculate how much to remove (from @ to current cursor)
1235                let remove_start = ac.trigger_pos;
1236
1237                // Remove the query part (keep the @)
1238                self.input_text.drain(remove_start + 1..end_pos);
1239                self.cursor_pos = remove_start + 1;
1240
1241                // Insert the selected path
1242                self.input_text.insert_str(self.cursor_pos, &selected.path);
1243                self.cursor_pos += selected.path.len();
1244
1245                // Add space after completion for better UX
1246                self.input_text.insert(self.cursor_pos, ' ');
1247                self.cursor_pos += 1;
1248
1249                debug_log(&format!(
1250                    "Accepted completion: {} -> input now: {:?}",
1251                    selected.path, self.input_text
1252                ));
1253            }
1254        }
1255
1256        // Close autocomplete
1257        self.file_autocomplete = None;
1258    }
1259
1260    fn handle_enter(&mut self) -> Result<(), CliError> {
1261        let text = self.input_text.trim().to_string();
1262
1263        // Clear input FIRST for immediate visual feedback
1264        self.input_text.clear();
1265        self.cursor_pos = 0;
1266
1267        if text.is_empty() {
1268            return Ok(());
1269        }
1270
1271        tracing::info!("Enter pressed with text: {:?}", text);
1272
1273        // Handle commands locally (no LLM)
1274        let text_lower = text.to_lowercase();
1275        if text_lower == "/exit"
1276            || text_lower == "/quit"
1277            || text_lower == "exit"
1278            || text_lower == "quit"
1279        {
1280            tracing::info!("Exit command detected, exiting");
1281            self.running = false;
1282            return Ok(());
1283        }
1284
1285        if text_lower == "/clear" || text_lower == "clear" {
1286            tracing::info!("Clear command detected");
1287            self.tui_bridge.chat_view().lock().unwrap().clear();
1288            return Ok(());
1289        }
1290
1291        if text_lower == "/help" || text_lower == "help" {
1292            tracing::info!("Help command detected");
1293            let help_msg = Message::system(
1294                "Available commands:\n\
1295                 /help  - Show this help message\n\
1296                 /clear - Clear chat history\n\
1297                 /exit  - Exit the application\n\
1298                 /quit  - Exit the application\n\
1299                 /session list  - List all sessions\n\
1300                 /session new   - Create a new session\n\
1301                 /session load  <id> - Load a session by ID\n\
1302                 /share         - Copy session to clipboard (markdown)\n\
1303                 /share md      - Export session as markdown file\n\
1304                 /share json    - Export session as JSON file\n\
1305                 \n\
1306                 Page Up/Down - Scroll chat history"
1307                    .to_string(),
1308            );
1309            self.tui_bridge
1310                .chat_view()
1311                .lock()
1312                .unwrap()
1313                .add_message(help_msg);
1314            return Ok(());
1315        }
1316
1317        // Handle session commands
1318        if text_lower.starts_with("/session ") {
1319            let session_cmd = text.strip_prefix("/session ").unwrap();
1320            if session_cmd.trim() == "list" {
1321                self.handle_session_list()?;
1322                return Ok(());
1323            } else if session_cmd.trim() == "new" {
1324                self.handle_session_new()?;
1325                return Ok(());
1326            } else if session_cmd.starts_with("load ") {
1327                let session_id = session_cmd.strip_prefix("load ").unwrap().trim();
1328                self.handle_session_load(session_id)?;
1329                return Ok(());
1330            } else {
1331                let error_msg = Message::system(
1332                    "Usage: /session list, /session new, /session load <id>".to_string(),
1333                );
1334                self.tui_bridge
1335                    .chat_view()
1336                    .lock()
1337                    .unwrap()
1338                    .add_message(error_msg);
1339                return Ok(());
1340            }
1341        }
1342
1343        // Handle share commands
1344        if text_lower == "/share" || text_lower.starts_with("/share ") {
1345            let share_cmd = text.strip_prefix("/share ").unwrap_or("").trim();
1346            self.handle_share(share_cmd)?;
1347            return Ok(());
1348        }
1349
1350        // Add user message to chat immediately for visual feedback
1351        self.tui_bridge.add_user_message(text.clone());
1352
1353        // Get new operation ID and ensure state is Idle
1354        let operation_id = self.tui_bridge.next_operation_id();
1355        debug_log(&format!("handle_enter: new operation_id={}", operation_id));
1356        *self.tui_bridge.state.lock().unwrap() = TuiState::Idle;
1357
1358        // Create cancellation token for this operation
1359        let cancel_token = tokio_util::sync::CancellationToken::new();
1360        self.cancellation_token = Some(cancel_token.clone());
1361
1362        // Clone Arcs for the spawned thread
1363        let messages = self.tui_bridge.messages.clone();
1364        let agent_bridge = self.tui_bridge.agent_bridge_arc();
1365        let session_manager = self.tui_bridge.session_manager.clone();
1366        let session_id = self.tui_bridge.session_id();
1367        let total_input_tokens = self.tui_bridge.total_input_tokens.clone();
1368        let total_output_tokens = self.tui_bridge.total_output_tokens.clone();
1369
1370        tracing::debug!("Spawning LLM processing thread");
1371
1372        // Spawn a thread to process the message without blocking the UI
1373        std::thread::spawn(move || {
1374            // Create a new tokio runtime for this thread
1375            let rt = tokio::runtime::Runtime::new().unwrap();
1376
1377            // Safe: we're in a dedicated thread, this won't cause issues
1378            #[allow(clippy::await_holding_lock)]
1379            rt.block_on(async {
1380                // Check for cancellation BEFORE acquiring locks
1381                if cancel_token.is_cancelled() {
1382                    tracing::debug!("Operation cancelled before acquiring locks");
1383                    return;
1384                }
1385
1386                // Try to acquire locks with timeout to avoid blocking indefinitely
1387                let messages_guard = {
1388                    let mut attempts = 0;
1389                    loop {
1390                        if cancel_token.is_cancelled() {
1391                            tracing::debug!("Operation cancelled while waiting for messages lock");
1392                            return;
1393                        }
1394                        match messages.try_lock() {
1395                            Ok(guard) => break guard,
1396                            Err(std::sync::TryLockError::WouldBlock) => {
1397                                attempts += 1;
1398                                if attempts > 50 {
1399                                    tracing::error!("Timeout waiting for messages lock");
1400                                    return;
1401                                }
1402                                tokio::time::sleep(std::time::Duration::from_millis(100)).await;
1403                            }
1404                            Err(e) => {
1405                                tracing::error!("Failed to lock messages: {}", e);
1406                                return;
1407                            }
1408                        }
1409                    }
1410                };
1411
1412                let mut messages_guard = messages_guard;
1413
1414                // Check cancellation again before acquiring bridge lock
1415                if cancel_token.is_cancelled() {
1416                    tracing::debug!("Operation cancelled before acquiring bridge lock");
1417                    return;
1418                }
1419
1420                let bridge_guard = {
1421                    let mut attempts = 0;
1422                    loop {
1423                        if cancel_token.is_cancelled() {
1424                            tracing::debug!("Operation cancelled while waiting for bridge lock");
1425                            return;
1426                        }
1427                        match agent_bridge.try_lock() {
1428                            Ok(guard) => break guard,
1429                            Err(std::sync::TryLockError::WouldBlock) => {
1430                                attempts += 1;
1431                                if attempts > 50 {
1432                                    tracing::error!("Timeout waiting for bridge lock");
1433                                    return;
1434                                }
1435                                tokio::time::sleep(std::time::Duration::from_millis(100)).await;
1436                            }
1437                            Err(e) => {
1438                                tracing::error!("Failed to lock agent_bridge: {}", e);
1439                                return;
1440                            }
1441                        }
1442                    }
1443                };
1444
1445                let mut bridge = bridge_guard;
1446
1447                // Set cancellation token and operation ID
1448                bridge.set_cancellation_token(cancel_token.clone(), operation_id);
1449
1450                match bridge.process_message(&text, &mut messages_guard).await {
1451                    Ok(_response) => {
1452                        // Don't sync ChatView here - it causes race conditions with ContentChunk events
1453                        // ChatView is already updated via ContentChunk events during streaming
1454                        // The messages_guard is the authoritative source for session persistence
1455
1456                        // Auto-save session after successful response
1457                        let msgs = messages_guard.clone();
1458                        let input_tokens = *total_input_tokens.lock().unwrap();
1459                        let output_tokens = *total_output_tokens.lock().unwrap();
1460
1461                        if let Err(e) = session_manager.lock().unwrap().save_session(
1462                            &session_id,
1463                            &msgs,
1464                            input_tokens,
1465                            output_tokens,
1466                        ) {
1467                            tracing::error!("✗ Failed to auto-save session {}: {}", session_id, e);
1468                        } else {
1469                            tracing::info!(
1470                                "✓ Session {} auto-saved ({} messages, {} in, {} out tokens)",
1471                                session_id,
1472                                msgs.len(),
1473                                input_tokens,
1474                                output_tokens
1475                            );
1476                        }
1477                    }
1478                    Err(e) => {
1479                        // Check if it was a cancellation
1480                        let error_msg = e.to_string();
1481                        if error_msg.contains("cancelled") {
1482                            tracing::info!("Request cancelled by user");
1483                        } else {
1484                            tracing::error!("LLM error: {}", e);
1485                        }
1486                    }
1487                }
1488
1489                // Clear cancellation token
1490                bridge.clear_cancellation_token();
1491            });
1492        });
1493
1494        Ok(())
1495    }
1496
1497    /// Cancel current LLM operation
1498    fn cancel_current_operation(&mut self) {
1499        if let Some(ref token) = self.cancellation_token {
1500            token.cancel();
1501            debug_log("Cancellation token triggered");
1502
1503            // Increment operation ID to ignore subsequent events from old operation
1504            self.tui_bridge.next_operation_id();
1505
1506            // Force reset TUI state to Idle
1507            *self.tui_bridge.state.lock().unwrap() = TuiState::Idle;
1508
1509            // Update UI state
1510            self.status_message = "Operation cancelled".to_string();
1511            self.status_is_error = false;
1512
1513            // Clear activity feed
1514            self.tui_bridge
1515                .activity_feed()
1516                .lock()
1517                .unwrap()
1518                .complete_all();
1519
1520            // Add cancellation message to chat
1521            let cancel_msg = Message::system("⚠ Operation cancelled by user".to_string());
1522            self.tui_bridge
1523                .chat_view()
1524                .lock()
1525                .unwrap()
1526                .add_message(cancel_msg);
1527        }
1528        self.cancellation_token = None;
1529        self.last_esc_time = None;
1530    }
1531
1532    /// Handle /session list command
1533    fn handle_session_list(&self) -> Result<(), CliError> {
1534        tracing::info!("Session list command detected");
1535        let session_manager = self.tui_bridge.session_manager.lock().unwrap();
1536        let current_session_id = self.tui_bridge.session_id();
1537
1538        match session_manager.list_sessions() {
1539            Ok(sessions) => {
1540                if sessions.is_empty() {
1541                    let msg = Message::system("No sessions found.".to_string());
1542                    self.tui_bridge.chat_view().lock().unwrap().add_message(msg);
1543                } else {
1544                    let mut output = vec!["Sessions (most recent first):".to_string()];
1545                    for (i, session) in sessions.iter().enumerate() {
1546                        let current = if session.id == current_session_id {
1547                            " (current)"
1548                        } else {
1549                            ""
1550                        };
1551                        let short_id = if session.id.len() > 8 {
1552                            &session.id[..8]
1553                        } else {
1554                            &session.id
1555                        };
1556                        output.push(format!(
1557                            "  {}. {}{} - {} messages, {} in tokens, {} out tokens",
1558                            i + 1,
1559                            short_id,
1560                            current,
1561                            session.message_count,
1562                            session.total_input_tokens,
1563                            session.total_output_tokens
1564                        ));
1565                    }
1566                    let msg = Message::system(output.join("\n"));
1567                    self.tui_bridge.chat_view().lock().unwrap().add_message(msg);
1568                }
1569            }
1570            Err(e) => {
1571                let msg = Message::system(format!("Error listing sessions: {}", e));
1572                self.tui_bridge.chat_view().lock().unwrap().add_message(msg);
1573            }
1574        }
1575        Ok(())
1576    }
1577
1578    /// Handle /session new command
1579    fn handle_session_new(&mut self) -> Result<(), CliError> {
1580        tracing::info!("Session new command detected");
1581
1582        // Save current session (release lock before proceeding)
1583        let save_result = self.tui_bridge.save_session();
1584        if let Err(e) = &save_result {
1585            tracing::error!("Failed to save current session: {}", e);
1586            let msg = Message::system(format!("⚠ Warning: Failed to save current session: {}", e));
1587            if let Ok(mut chat) = self.tui_bridge.chat_view.try_lock() {
1588                chat.add_message(msg);
1589            }
1590        }
1591
1592        // Create new session (separate lock scope)
1593        let new_session_id = {
1594            let session_manager = self.tui_bridge.session_manager.lock().map_err(|e| {
1595                CliError::ConfigError(format!("Failed to acquire session manager lock: {}", e))
1596            })?;
1597
1598            session_manager
1599                .create_new_session()
1600                .map_err(|e| CliError::ConfigError(format!("Failed to create session: {}", e)))?
1601        };
1602
1603        let old_session_id = self.tui_bridge.session_id();
1604
1605        // Update session ID
1606        if let Ok(mut id_guard) = self.tui_bridge.session_id.try_lock() {
1607            *id_guard = new_session_id.clone();
1608        }
1609
1610        // Clear messages and reset token counts (separate locks)
1611        if let Ok(mut messages_guard) = self.tui_bridge.messages.try_lock() {
1612            messages_guard.clear();
1613        }
1614        if let Ok(mut input_guard) = self.tui_bridge.total_input_tokens.try_lock() {
1615            *input_guard = 0;
1616        }
1617        if let Ok(mut output_guard) = self.tui_bridge.total_output_tokens.try_lock() {
1618            *output_guard = 0;
1619        }
1620
1621        // IMPORTANT: Also clear the chat view to prevent duplicate messages
1622        if let Ok(mut chat) = self.tui_bridge.chat_view.try_lock() {
1623            chat.clear();
1624        }
1625
1626        tracing::info!(
1627            "Created new session: {} (old: {})",
1628            new_session_id,
1629            old_session_id
1630        );
1631
1632        // Add system message
1633        let session_short_id = if new_session_id.len() > 8 {
1634            &new_session_id[new_session_id.len().saturating_sub(8)..]
1635        } else {
1636            &new_session_id
1637        };
1638        let msg = Message::system(format!("🆕 New session created: {}", session_short_id));
1639
1640        if let Ok(mut chat) = self.tui_bridge.chat_view.try_lock() {
1641            chat.add_message(msg);
1642        }
1643
1644        Ok(())
1645    }
1646
1647    /// Handle /session load <id> command
1648    fn handle_session_load(&mut self, session_id: &str) -> Result<(), CliError> {
1649        tracing::info!("Session load command detected for session: {}", session_id);
1650
1651        // Save current session first (release lock before proceeding)
1652        let save_result = self.tui_bridge.save_session();
1653        if let Err(e) = &save_result {
1654            tracing::error!("Failed to save current session: {}", e);
1655            let msg = Message::system(format!("⚠ Warning: Failed to save current session: {}", e));
1656            if let Ok(mut chat) = self.tui_bridge.chat_view.try_lock() {
1657                chat.add_message(msg);
1658            }
1659        }
1660
1661        // Find session ID from partial match (acquire locks separately to avoid deadlock)
1662        let (full_session_id, session_info, messages) = {
1663            let session_manager = self.tui_bridge.session_manager.lock().map_err(|e| {
1664                CliError::ConfigError(format!("Failed to acquire session manager lock: {}", e))
1665            })?;
1666
1667            let sessions = session_manager
1668                .list_sessions()
1669                .map_err(|e| CliError::ConfigError(format!("Failed to list sessions: {}", e)))?;
1670
1671            let matched_session = if session_id.len() >= 8 {
1672                // Try exact match first
1673                sessions
1674                    .iter()
1675                    .find(|s| s.id == session_id)
1676                    // Then try prefix match
1677                    .or_else(|| sessions.iter().find(|s| s.id.starts_with(session_id)))
1678            } else {
1679                // Try prefix match for short IDs
1680                sessions.iter().find(|s| s.id.starts_with(session_id))
1681            };
1682
1683            match matched_session {
1684                Some(info) => {
1685                    let full_id = info.id.clone();
1686                    // Load messages within the same lock scope
1687                    let msgs = session_manager.load_session(&full_id).map_err(|e| {
1688                        CliError::ConfigError(format!("Failed to load session {}: {}", full_id, e))
1689                    })?;
1690                    (full_id, info.clone(), msgs)
1691                }
1692                None => {
1693                    let msg = Message::system(format!("❌ Session not found: {}", session_id));
1694                    if let Ok(mut chat) = self.tui_bridge.chat_view.try_lock() {
1695                        chat.add_message(msg);
1696                    }
1697                    return Ok(());
1698                }
1699            }
1700        };
1701
1702        // Update session ID and token counts (separate locks)
1703        if let Ok(mut id_guard) = self.tui_bridge.session_id.try_lock() {
1704            *id_guard = full_session_id.clone();
1705        }
1706        if let Ok(mut input_guard) = self.tui_bridge.total_input_tokens.try_lock() {
1707            *input_guard = session_info.total_input_tokens;
1708        }
1709        if let Ok(mut output_guard) = self.tui_bridge.total_output_tokens.try_lock() {
1710            *output_guard = session_info.total_output_tokens;
1711        }
1712
1713        // Update messages in TUI bridge
1714        if let Ok(mut messages_guard) = self.tui_bridge.messages.try_lock() {
1715            *messages_guard = messages.clone();
1716        }
1717
1718        // Clear chat view and reload messages
1719        if let Ok(mut chat) = self.tui_bridge.chat_view.try_lock() {
1720            chat.clear();
1721
1722            // Reload messages into chat view (no additional locks needed)
1723            for msg in &messages {
1724                match msg.role {
1725                    limit_llm::Role::User => {
1726                        let content = msg.content.as_deref().unwrap_or("");
1727                        let chat_msg = Message::user(content.to_string());
1728                        chat.add_message(chat_msg);
1729                    }
1730                    limit_llm::Role::Assistant => {
1731                        let content = msg.content.as_deref().unwrap_or("");
1732                        let chat_msg = Message::assistant(content.to_string());
1733                        chat.add_message(chat_msg);
1734                    }
1735                    _ => {}
1736                }
1737            }
1738
1739            tracing::info!(
1740                "Loaded session: {} ({} messages)",
1741                full_session_id,
1742                messages.len()
1743            );
1744
1745            // Add system message
1746            let session_short_id = if full_session_id.len() > 8 {
1747                &full_session_id[full_session_id.len().saturating_sub(8)..]
1748            } else {
1749                &full_session_id
1750            };
1751            let msg = Message::system(format!(
1752                "📂 Loaded session: {} ({} messages, {} in tokens, {} out tokens)",
1753                session_short_id,
1754                messages.len(),
1755                session_info.total_input_tokens,
1756                session_info.total_output_tokens
1757            ));
1758            chat.add_message(msg);
1759        }
1760
1761        Ok(())
1762    }
1763
1764    /// Handle /share command
1765    fn handle_share(&mut self, format_str: &str) -> Result<(), CliError> {
1766        use crate::session_share::{ExportFormat, SessionShare};
1767
1768        tracing::info!("Share command detected with format: {:?}", format_str);
1769
1770        // Determine export format
1771        let format = match format_str.to_lowercase().as_str() {
1772            "" | "clipboard" | "cb" => ExportFormat::Markdown,
1773            "md" | "markdown" => ExportFormat::Markdown,
1774            "json" => ExportFormat::Json,
1775            _ => {
1776                let msg = Message::system(
1777                    "Invalid format. Use: /share, /share md, /share json".to_string(),
1778                );
1779                self.tui_bridge.chat_view().lock().unwrap().add_message(msg);
1780                return Ok(());
1781            }
1782        };
1783
1784        // Get session data
1785        let session_id = self.tui_bridge.session_id();
1786        let messages = self
1787            .tui_bridge
1788            .messages
1789            .lock()
1790            .map(|guard| guard.clone())
1791            .unwrap_or_default();
1792        let total_input_tokens = self.tui_bridge.total_input_tokens();
1793        let total_output_tokens = self.tui_bridge.total_output_tokens();
1794        let model = self
1795            .tui_bridge
1796            .agent_bridge
1797            .lock()
1798            .ok()
1799            .map(|bridge| bridge.model().to_string());
1800
1801        // Check if there are messages to share
1802        let user_assistant_count = messages
1803            .iter()
1804            .filter(|m| matches!(m.role, limit_llm::Role::User | limit_llm::Role::Assistant))
1805            .count();
1806
1807        if user_assistant_count == 0 {
1808            let msg =
1809                Message::system("⚠ No messages to share. Start a conversation first.".to_string());
1810            self.tui_bridge.chat_view().lock().unwrap().add_message(msg);
1811            return Ok(());
1812        }
1813
1814        // Export based on format
1815        if format_str.is_empty() || format_str == "clipboard" || format_str == "cb" {
1816            // Copy to clipboard
1817            match SessionShare::generate_share_content(
1818                &session_id,
1819                &messages,
1820                total_input_tokens,
1821                total_output_tokens,
1822                model.clone(),
1823                format,
1824            ) {
1825                Ok(content) => {
1826                    if let Some(ref clipboard) = self.clipboard {
1827                        match clipboard.set_text(&content) {
1828                            Ok(()) => {
1829                                let short_id = &session_id[..session_id.len().min(8)];
1830                                let msg = Message::system(format!(
1831                                    "✓ Session {} copied to clipboard ({} messages, {} tokens)",
1832                                    short_id,
1833                                    user_assistant_count,
1834                                    total_input_tokens + total_output_tokens
1835                                ));
1836                                self.tui_bridge.chat_view().lock().unwrap().add_message(msg);
1837                            }
1838                            Err(e) => {
1839                                let msg = Message::system(format!(
1840                                    "❌ Failed to copy to clipboard: {}",
1841                                    e
1842                                ));
1843                                self.tui_bridge.chat_view().lock().unwrap().add_message(msg);
1844                            }
1845                        }
1846                    } else {
1847                        let msg = Message::system(
1848                            "❌ Clipboard not available. Try '/share md' to save as file."
1849                                .to_string(),
1850                        );
1851                        self.tui_bridge.chat_view().lock().unwrap().add_message(msg);
1852                    }
1853                }
1854                Err(e) => {
1855                    let msg =
1856                        Message::system(format!("❌ Failed to generate share content: {}", e));
1857                    self.tui_bridge.chat_view().lock().unwrap().add_message(msg);
1858                }
1859            }
1860        } else {
1861            // Save to file
1862            match SessionShare::export_session(
1863                &session_id,
1864                &messages,
1865                total_input_tokens,
1866                total_output_tokens,
1867                model,
1868                format,
1869            ) {
1870                Ok((filepath, export)) => {
1871                    let short_id = &session_id[..session_id.len().min(8)];
1872                    let extension = match format {
1873                        ExportFormat::Markdown => "md",
1874                        ExportFormat::Json => "json",
1875                    };
1876                    let msg = Message::system(format!(
1877                        "✓ Session {} exported to {}\n  ({} messages, {} tokens)\n  Location: ~/.limit/exports/",
1878                        short_id,
1879                        extension,
1880                        user_assistant_count,
1881                        total_input_tokens + total_output_tokens
1882                    ));
1883                    self.tui_bridge.chat_view().lock().unwrap().add_message(msg);
1884
1885                    tracing::info!(
1886                        "Session exported to {:?} ({} messages)",
1887                        filepath,
1888                        export.messages.len()
1889                    );
1890                }
1891                Err(e) => {
1892                    let msg = Message::system(format!("❌ Failed to export session: {}", e));
1893                    self.tui_bridge.chat_view().lock().unwrap().add_message(msg);
1894                }
1895            }
1896        }
1897
1898        Ok(())
1899    }
1900
1901    fn prev_char_pos(&self) -> usize {
1902        if self.cursor_pos == 0 {
1903            return 0;
1904        }
1905        // Start ONE position before cursor, then find char boundary
1906        let mut pos = self.cursor_pos - 1;
1907        while pos > 0 && !self.input_text.is_char_boundary(pos) {
1908            pos -= 1;
1909        }
1910        pos
1911    }
1912
1913    fn next_char_pos(&self) -> usize {
1914        if self.cursor_pos >= self.input_text.len() {
1915            return self.input_text.len();
1916        }
1917        // Start ONE position after cursor, then find char boundary
1918        let mut pos = self.cursor_pos + 1;
1919        while pos < self.input_text.len() && !self.input_text.is_char_boundary(pos) {
1920            pos += 1;
1921        }
1922        pos
1923    }
1924
1925    fn draw(&mut self) -> Result<(), CliError> {
1926        let chat_view = self.tui_bridge.chat_view().clone();
1927        let state = self.tui_bridge.state();
1928        let input_text = self.input_text.clone();
1929        let cursor_pos = self.cursor_pos;
1930        let status_message = self.status_message.clone();
1931        let status_is_error = self.status_is_error;
1932        let cursor_blink_state = self.cursor_blink_state;
1933        let tui_bridge = &self.tui_bridge;
1934        let file_autocomplete = self.file_autocomplete.clone();
1935
1936        self.terminal
1937            .draw(|f| {
1938                Self::draw_ui(
1939                    f,
1940                    &chat_view,
1941                    state,
1942                    &input_text,
1943                    cursor_pos,
1944                    &status_message,
1945                    status_is_error,
1946                    cursor_blink_state,
1947                    tui_bridge,
1948                    &file_autocomplete,
1949                );
1950            })
1951            .map_err(|e| CliError::IoError(io::Error::other(e)))?;
1952
1953        Ok(())
1954    }
1955
1956    /// Draw the TUI interface
1957    #[allow(clippy::too_many_arguments)]
1958    fn draw_ui(
1959        f: &mut Frame,
1960        chat_view: &Arc<Mutex<ChatView>>,
1961        _state: TuiState,
1962        input_text: &str,
1963        cursor_pos: usize,
1964        status_message: &str,
1965        status_is_error: bool,
1966        cursor_blink_state: bool,
1967        tui_bridge: &TuiBridge,
1968        file_autocomplete: &Option<FileAutocompleteState>,
1969    ) {
1970        let size = f.area();
1971
1972        // Check if we have activities to show
1973        let activity_count = tui_bridge.activity_feed().lock().unwrap().len();
1974        let activity_height = if activity_count > 0 {
1975            (activity_count as u16).min(3) // Max 3 lines for activity feed
1976        } else {
1977            0
1978        };
1979
1980        // Build constraints based on whether we have activities
1981        let constraints: Vec<Constraint> = vec![Constraint::Percentage(90)]; // Chat view
1982        let mut constraints = constraints;
1983        if activity_height > 0 {
1984            constraints.push(Constraint::Length(activity_height)); // Activity feed
1985        }
1986        constraints.push(Constraint::Length(1)); // Status bar
1987        constraints.push(Constraint::Length(6)); // Input area
1988
1989        // Split the screen
1990        let chunks = Layout::default()
1991            .direction(Direction::Vertical)
1992            .constraints(constraints.as_slice())
1993            .split(size);
1994
1995        let mut chunk_idx = 0;
1996
1997        // Draw chat view with border
1998        {
1999            let chat = chat_view.lock().unwrap();
2000            let total_input = tui_bridge.total_input_tokens();
2001            let total_output = tui_bridge.total_output_tokens();
2002            let title = format!(" Chat (↑{} ↓{}) ", total_input, total_output);
2003            let chat_block = Block::default()
2004                .borders(Borders::ALL)
2005                .title(title)
2006                .title_style(
2007                    Style::default()
2008                        .fg(Color::Cyan)
2009                        .add_modifier(Modifier::BOLD),
2010                );
2011            f.render_widget(&*chat, chat_block.inner(chunks[chunk_idx]));
2012            f.render_widget(chat_block, chunks[chunk_idx]);
2013            chunk_idx += 1;
2014        }
2015
2016        // Draw activity feed if present
2017        if activity_height > 0 {
2018            let activity_feed = tui_bridge.activity_feed().lock().unwrap();
2019            let activity_block = Block::default()
2020                .borders(Borders::NONE)
2021                .style(Style::default().bg(Color::Reset));
2022            let activity_inner = activity_block.inner(chunks[chunk_idx]);
2023            f.render_widget(activity_block, chunks[chunk_idx]);
2024            activity_feed.render(activity_inner, f.buffer_mut());
2025            chunk_idx += 1;
2026        }
2027
2028        // Draw status bar
2029        {
2030            let status_style = if status_is_error {
2031                Style::default().fg(Color::Red).bg(Color::Reset)
2032            } else {
2033                Style::default().fg(Color::Yellow)
2034            };
2035
2036            let status = Paragraph::new(Line::from(vec![
2037                Span::styled(" ● ", Style::default().fg(Color::Green)),
2038                Span::styled(status_message, status_style),
2039            ]));
2040            f.render_widget(status, chunks[chunk_idx]);
2041            chunk_idx += 1;
2042        }
2043
2044        // Draw input area with border
2045        {
2046            let input_block = Block::default()
2047                .borders(Borders::ALL)
2048                .title(" Input (Esc or /exit to quit) ")
2049                .title_style(Style::default().fg(Color::Cyan));
2050
2051            let input_inner = input_block.inner(chunks[chunk_idx]);
2052            f.render_widget(input_block, chunks[chunk_idx]);
2053
2054            // Build input line with cursor
2055            let before_cursor = &input_text[..cursor_pos];
2056            let at_cursor = if cursor_pos < input_text.len() {
2057                &input_text[cursor_pos
2058                    ..cursor_pos
2059                        + input_text[cursor_pos..]
2060                            .chars()
2061                            .next()
2062                            .map(|c| c.len_utf8())
2063                            .unwrap_or(0)]
2064            } else {
2065                " "
2066            };
2067            let after_cursor = if cursor_pos < input_text.len() {
2068                &input_text[cursor_pos + at_cursor.len()..]
2069            } else {
2070                ""
2071            };
2072
2073            let cursor_style = if cursor_blink_state {
2074                Style::default().bg(Color::White).fg(Color::Black)
2075            } else {
2076                Style::default().bg(Color::Reset).fg(Color::Reset)
2077            };
2078
2079            let input_line = if input_text.is_empty() {
2080                Line::from(vec![Span::styled(
2081                    "Type your message here...",
2082                    Style::default().fg(Color::DarkGray),
2083                )])
2084            } else {
2085                Line::from(vec![
2086                    Span::raw(before_cursor),
2087                    Span::styled(at_cursor, cursor_style),
2088                    Span::raw(after_cursor),
2089                ])
2090            };
2091
2092            let input_para = Paragraph::new(input_line).wrap(Wrap { trim: false });
2093            f.render_widget(input_para, input_inner);
2094        }
2095
2096        // Draw file autocomplete popup if active
2097        if let Some(ref ac) = file_autocomplete {
2098            if ac.is_active && !ac.matches.is_empty() {
2099                // Find the input area chunk (it's the last one)
2100                let input_area = chunks.last().unwrap();
2101
2102                // Calculate popup area above input
2103                let popup_area = calculate_popup_area(*input_area, ac.matches.len());
2104
2105                // Create and render the autocomplete widget
2106                let widget = FileAutocompleteWidget::new(&ac.matches, ac.selected_index, &ac.query);
2107                f.render_widget(widget, popup_area);
2108            }
2109        }
2110    }
2111}
2112
2113#[cfg(test)]
2114mod tests {
2115    use super::*;
2116
2117    /// Create a test config for AgentBridge
2118    fn create_test_config() -> limit_llm::Config {
2119        use limit_llm::ProviderConfig;
2120        let mut providers = std::collections::HashMap::new();
2121        providers.insert(
2122            "anthropic".to_string(),
2123            ProviderConfig {
2124                api_key: Some("test-key".to_string()),
2125                model: "claude-3-5-sonnet-20241022".to_string(),
2126                base_url: None,
2127                max_tokens: 4096,
2128                timeout: 60,
2129                max_iterations: 100,
2130                thinking_enabled: false,
2131                clear_thinking: true,
2132            },
2133        );
2134        limit_llm::Config {
2135            provider: "anthropic".to_string(),
2136            providers,
2137        }
2138    }
2139
2140    #[test]
2141    fn test_tui_bridge_new() {
2142        let config = create_test_config();
2143        let agent_bridge = AgentBridge::new(config).unwrap();
2144        let (_tx, rx) = mpsc::unbounded_channel();
2145
2146        let tui_bridge = TuiBridge::new(agent_bridge, rx).unwrap();
2147        assert_eq!(tui_bridge.state(), TuiState::Idle);
2148    }
2149
2150    #[test]
2151    fn test_tui_bridge_state() {
2152        let config = create_test_config();
2153        let agent_bridge = AgentBridge::new(config).unwrap();
2154        let (tx, rx) = mpsc::unbounded_channel();
2155
2156        let mut tui_bridge = TuiBridge::new(agent_bridge, rx).unwrap();
2157
2158        let op_id = tui_bridge.operation_id();
2159        tx.send(AgentEvent::Thinking { operation_id: op_id }).unwrap();
2160        tui_bridge.process_events().unwrap();
2161        assert!(matches!(tui_bridge.state(), TuiState::Thinking));
2162
2163        tx.send(AgentEvent::Done { operation_id: op_id }).unwrap();
2164        tui_bridge.process_events().unwrap();
2165        assert_eq!(tui_bridge.state(), TuiState::Idle);
2166    }
2167
2168    #[test]
2169    fn test_tui_bridge_chat_view() {
2170        let config = create_test_config();
2171        let agent_bridge = AgentBridge::new(config).unwrap();
2172        let (_tx, rx) = mpsc::unbounded_channel();
2173
2174        let tui_bridge = TuiBridge::new(agent_bridge, rx).unwrap();
2175
2176        tui_bridge.add_user_message("Hello".to_string());
2177        assert_eq!(tui_bridge.chat_view().lock().unwrap().message_count(), 3); // 1 user + 2 system (welcome + model)
2178    }
2179
2180    #[test]
2181    fn test_tui_state_default() {
2182        let state = TuiState::default();
2183        assert_eq!(state, TuiState::Idle);
2184    }
2185}