oli_tui/app/
mod.rs

1pub mod agent;
2pub mod commands;
3pub mod history;
4pub mod logger;
5pub mod models;
6pub mod permissions;
7pub mod state;
8pub mod utils;
9
10use anyhow::Result;
11use dotenv::dotenv;
12// IO operations are handled elsewhere in specific modules
13use std::path::Path;
14use std::sync::mpsc;
15use std::time::Duration;
16use tokio::runtime::Runtime;
17use tui_textarea::TextArea;
18
19use crate::app::utils::ScrollState;
20
21// Re-exports
22pub use agent::{determine_agent_model, determine_provider, AgentManager};
23pub use commands::{get_available_commands, CommandHandler, SpecialCommand};
24pub use history::ContextCompressor;
25pub use logger::Logger;
26pub use models::ModelManager;
27pub use permissions::{PendingToolExecution, PermissionHandler, ToolPermissionStatus};
28pub use state::{App, AppState};
29pub use utils::{ErrorHandler, Scrollable};
30
31use crate::agent::core::{Agent, LLMProvider};
32use crate::apis::api_client::{Message, SessionManager};
33use crate::models::{get_available_models, ModelConfig};
34use crate::prompts::DEFAULT_SESSION_PROMPT;
35use uuid::Uuid;
36
37impl Default for App {
38    fn default() -> Self {
39        Self::new()
40    }
41}
42
43impl App {
44    pub fn new() -> Self {
45        // Load environment variables from .env file if present
46        let _ = dotenv();
47
48        // Create tokio runtime for async operations
49        let tokio_runtime = Runtime::new().ok();
50
51        // Get current working directory
52        let current_working_dir = std::env::current_dir()
53            .ok()
54            .map(|p| p.to_string_lossy().to_string());
55
56        // Initialize TextArea for better input handling
57        let mut textarea = TextArea::default();
58        // Configure TextArea to match the app's style
59        textarea.set_placeholder_text("Type your message here or type / for commands");
60        textarea.set_cursor_line_style(ratatui::style::Style::default());
61        // Set a custom style for the first line's text (this will be combined with our prompt symbol)
62        textarea.set_style(ratatui::style::Style::default().fg(ratatui::style::Color::LightCyan));
63
64        // Initialize the session manager with default settings
65        let session_manager =
66            Some(SessionManager::new(100).with_system_message(DEFAULT_SESSION_PROMPT.to_string()));
67
68        // Generate a unique session ID
69        let session_id = Uuid::new_v4().to_string();
70
71        Self {
72            state: AppState::Setup,
73            textarea,
74            input: String::new(),
75            messages: vec![],
76            logs: vec![],     // Initialize empty log storage
77            show_logs: false, // Start with normal messages display
78            selected_model: 0,
79            available_models: get_available_models(),
80            error_message: None,
81            debug_messages: false, // Debug mode off by default
82            message_scroll: ScrollState::new(),
83            log_scroll: ScrollState::new(), // Initialize log scroll state
84            scroll_position: 0,             // Legacy field kept for compatibility
85            last_query_time: std::time::Instant::now(),
86            last_message_time: std::time::Instant::now(), // For animation effects
87            use_agent: false,
88            agent: None,
89            tokio_runtime,
90            agent_progress_rx: None,
91            api_key: None,
92            current_working_dir,
93            // Initialize command-related fields
94            command_mode: false,
95            available_commands: get_available_commands(),
96            selected_command: 0,
97            show_command_menu: false,
98            // Initialize tool permission-related fields
99            permission_required: false,
100            pending_tool: None,
101            tool_permission_status: ToolPermissionStatus::Pending,
102            tool_execution_in_progress: false,
103            show_intermediate_steps: true, // Default to showing intermediate steps
104            show_shortcuts_hint: true,     // Default to showing shortcut hints
105            show_detailed_shortcuts: false, // Default to not showing detailed shortcuts
106            // Initialize cursor position
107            cursor_position: 0, // Start at the beginning of the input
108            // Initialize task tracking
109            tasks: Vec::new(),
110            current_task_id: None,
111            task_scroll: ScrollState::new(),
112            task_scroll_position: 0, // Legacy field kept for compatibility
113            // Initialize conversation history tracking
114            conversation_summaries: Vec::new(),
115            // Initialize session manager
116            session_manager,
117            // Initialize session ID for logging
118            session_id,
119        }
120    }
121}
122
123// Implement the various traits for App
124impl CommandHandler for App {
125    fn check_command_mode(&mut self) {
126        // Track previous state
127        let was_in_command_mode = self.command_mode;
128
129        // Get the current text from the textarea
130        let input_text = self.textarea.lines().join("\n");
131
132        // Update the legacy input field for compatibility
133        self.input = input_text.clone();
134
135        // Update command mode state
136        self.command_mode = input_text.starts_with('/');
137        self.show_command_menu = self.command_mode && !input_text.contains(' ');
138
139        // Always reset the command selection in these cases:
140        if self.command_mode {
141            let filtered = self.filtered_commands();
142
143            // Reset when:
144            // 1. Just entered command mode (typed '/')
145            // 2. Selection is out of bounds
146            // 3. Input has changed significantly
147            let should_reset = (input_text.len() == 1 && !was_in_command_mode)
148                || (filtered.is_empty() || self.selected_command >= filtered.len());
149
150            if should_reset {
151                // Start from the beginning
152                self.selected_command = 0;
153
154                // Debug logging
155                if self.debug_messages {
156                    self.log(
157                        "Reset command selection. Input: '{}', Commands: {}",
158                        &[&input_text, &filtered.len().to_string()],
159                    );
160                }
161            }
162        }
163    }
164
165    fn filtered_commands(&self) -> Vec<SpecialCommand> {
166        if !self.command_mode || self.input.len() <= 1 {
167            // Return all commands when just typing "/"
168            return self.available_commands.clone();
169        }
170
171        // Filter commands that start with the input text
172        self.available_commands
173            .iter()
174            .filter(|cmd| cmd.name.starts_with(&self.input))
175            .cloned()
176            .collect()
177    }
178
179    fn select_next_command(&mut self) {
180        // Get filtered commands
181        let filtered = self.filtered_commands();
182
183        if self.show_command_menu && !filtered.is_empty() {
184            // Store the number of commands
185            let num_commands = filtered.len();
186
187            // Always ensure we're in bounds and wrap properly
188            if num_commands == 0 {
189                return; // No commands available
190            }
191
192            // Ensure we're in bounds first
193            self.selected_command = self.selected_command.min(num_commands - 1);
194
195            // Then move forward one position with wraparound
196            self.selected_command = (self.selected_command + 1) % num_commands;
197
198            // Debug message
199            if self.debug_messages {
200                self.log(
201                    "Selected command {} of {}",
202                    &[
203                        &(self.selected_command + 1).to_string(),
204                        &num_commands.to_string(),
205                    ],
206                );
207            }
208        }
209    }
210
211    fn select_prev_command(&mut self) {
212        // Get filtered commands
213        let filtered = self.filtered_commands();
214
215        if self.show_command_menu && !filtered.is_empty() {
216            // Store the number of commands
217            let num_commands = filtered.len();
218
219            // Always ensure we're in bounds and wrap properly
220            if num_commands == 0 {
221                return; // No commands available
222            }
223
224            // Ensure we're in bounds first
225            self.selected_command = self.selected_command.min(num_commands - 1);
226
227            // Calculate previous with wraparound
228            self.selected_command = if self.selected_command == 0 {
229                num_commands - 1 // Wrap to last command
230            } else {
231                self.selected_command - 1
232            };
233
234            // Debug message
235            if self.debug_messages {
236                self.log(
237                    "Selected command {} of {}",
238                    &[
239                        &(self.selected_command + 1).to_string(),
240                        &num_commands.to_string(),
241                    ],
242                );
243            }
244        }
245    }
246
247    fn execute_command(&mut self) -> bool {
248        if !self.command_mode {
249            return false;
250        }
251
252        // Get the command to execute (either selected or entered)
253        let command_to_execute = if self.show_command_menu {
254            // Get the filtered commands
255            let filtered = self.filtered_commands();
256            if filtered.is_empty() {
257                return false;
258            }
259
260            // Safely get a valid index into the filtered commands list
261            let valid_index = self.selected_command.min(filtered.len() - 1);
262            filtered[valid_index].name.clone()
263        } else {
264            self.input.clone()
265        };
266
267        // Execute the command
268        match command_to_execute.as_str() {
269            "/help" => {
270                self.messages.push("Available commands:".into());
271                for cmd in &self.available_commands {
272                    self.messages
273                        .push(format!("{} - {}", cmd.name, cmd.description));
274                }
275                // Removed empty line spacing for cleaner UI
276                true
277            }
278            "/clear" => {
279                self.clear_history();
280                self.messages.push("Conversation history cleared.".into());
281                true
282            }
283            "/debug" => {
284                // Toggle debug messages visibility and switch view mode
285                self.debug_messages = !self.debug_messages;
286                self.show_logs = self.debug_messages; // Switch to logs view when enabling debug mode
287
288                self.messages.push(format!(
289                    "Debug messages {}.",
290                    if self.debug_messages {
291                        "enabled"
292                    } else {
293                        "disabled"
294                    }
295                ));
296
297                // Add explanation of log view when enabling debug
298                if self.debug_messages {
299                    self.messages
300                        .push("Debug logs will be shown in a separate view.".into());
301                    self.messages.push(
302                        "The output pane now shows debug logs instead of conversation.".into(),
303                    );
304                    self.log("Debug mode enabled - logs are now being collected", &[]);
305                } else {
306                    // Switch back to normal view and add message
307                    self.messages
308                        .push("Returning to normal conversation view.".into());
309                }
310
311                true
312            }
313            "/steps" => {
314                // Toggle showing intermediate steps
315                self.show_intermediate_steps = !self.show_intermediate_steps;
316                self.messages.push(format!(
317                    "Intermediate steps display {}.",
318                    if self.show_intermediate_steps {
319                        "enabled"
320                    } else {
321                        "disabled"
322                    }
323                ));
324                if self.show_intermediate_steps {
325                    self.messages.push(
326                        "Tool usage and intermediate operations will be shown as they happen."
327                            .into(),
328                    );
329                } else {
330                    self.messages.push(
331                        "Only the final response will be shown without intermediate steps.".into(),
332                    );
333                }
334                true
335            }
336            "/summarize" => {
337                // Attempt to summarize conversation history
338                if let Err(e) = self.compress_context() {
339                    self.messages
340                        .push(format!("Error summarizing history: {}", e));
341                }
342                true
343            }
344            "/exit" => {
345                self.state = AppState::Error("quit".into());
346                true
347            }
348            _ => false,
349        }
350    }
351}
352
353impl Scrollable for App {
354    fn message_scroll_state(&mut self) -> &mut ScrollState {
355        if self.show_logs {
356            &mut self.log_scroll
357        } else {
358            &mut self.message_scroll
359        }
360    }
361
362    fn task_scroll_state(&mut self) -> &mut ScrollState {
363        &mut self.task_scroll
364    }
365
366    fn scroll_up(&mut self, amount: usize) {
367        if self.show_logs {
368            // Scroll logs
369            self.log_scroll.scroll_up(amount);
370        } else {
371            // Scroll messages
372            self.message_scroll.scroll_up(amount);
373            // Update legacy scroll position for compatibility
374            self.scroll_position = self.message_scroll.position;
375        }
376    }
377
378    fn scroll_down(&mut self, amount: usize) {
379        if self.show_logs {
380            // Scroll logs
381            self.log_scroll.scroll_down(amount);
382        } else {
383            // Scroll messages
384            self.message_scroll.scroll_down(amount);
385            // Update legacy scroll position for compatibility
386            self.scroll_position = self.message_scroll.position;
387        }
388    }
389
390    fn auto_scroll_to_bottom(&mut self) {
391        if self.show_logs {
392            // Scroll logs to bottom
393            self.log_scroll.scroll_to_bottom();
394        } else {
395            // Scroll messages to bottom
396            self.message_scroll.scroll_to_bottom();
397            // Update legacy scroll position for compatibility
398            self.scroll_position = self.message_scroll.position;
399        }
400    }
401
402    fn scroll_tasks_up(&mut self, amount: usize) {
403        // Use new scroll state
404        self.task_scroll.scroll_up(amount);
405
406        // Update legacy scroll position for compatibility
407        self.task_scroll_position = self.task_scroll.position;
408    }
409
410    fn scroll_tasks_down(&mut self, amount: usize) {
411        // Use new scroll state
412        self.task_scroll.scroll_down(amount);
413
414        // Update legacy scroll position for compatibility
415        self.task_scroll_position = self.task_scroll.position;
416    }
417}
418
419// Task management methods
420impl App {
421    /// Create a new task and set it as current
422    pub fn create_task(&mut self, description: &str) -> String {
423        let task = crate::app::state::Task::new(description);
424        let task_id = task.id.clone();
425        self.tasks.push(task);
426        self.current_task_id = Some(task_id.clone());
427        task_id
428    }
429
430    /// Get the current task if any
431    pub fn current_task(&self) -> Option<&crate::app::state::Task> {
432        if let Some(id) = &self.current_task_id {
433            self.tasks.iter().find(|t| &t.id == id)
434        } else {
435            None
436        }
437    }
438
439    /// Get the current task as mutable if any
440    pub fn current_task_mut(&mut self) -> Option<&mut crate::app::state::Task> {
441        if let Some(id) = &self.current_task_id {
442            let id_clone = id.clone();
443            self.tasks.iter_mut().find(|t| t.id == id_clone)
444        } else {
445            None
446        }
447    }
448
449    /// Add a tool use to the current task
450    pub fn add_tool_use(&mut self) {
451        if let Some(task) = self.current_task_mut() {
452            task.add_tool_use();
453        }
454    }
455
456    /// Add input tokens to the current task
457    pub fn add_input_tokens(&mut self, tokens: u32) {
458        if let Some(task) = self.current_task_mut() {
459            task.add_input_tokens(tokens);
460        }
461    }
462
463    /// Complete the current task
464    pub fn complete_current_task(&mut self, tokens: u32) {
465        if let Some(task) = self.current_task_mut() {
466            // We don't need to pass tool_count as parameter anymore,
467            // the Task now uses its internal counter
468            task.complete(0, tokens); // Value 0 is not used, task will use its internal tool_count
469        }
470        self.current_task_id = None;
471    }
472
473    /// Mark the current task as failed
474    pub fn fail_current_task(&mut self, error: &str) {
475        if let Some(task) = self.current_task_mut() {
476            task.fail(error);
477        }
478        self.current_task_id = None;
479    }
480}
481
482impl Logger for App {
483    fn log(&mut self, message: &str, args: &[&str]) {
484        // Format the message with provided arguments
485        let formatted_message = if args.is_empty() {
486            message.to_string()
487        } else {
488            // Simple placeholder replacement ({}), not as sophisticated as format!
489            let mut result = message.to_string();
490            for arg in args {
491                if let Some(pos) = result.find("{}") {
492                    result.replace_range(pos..pos + 2, arg);
493                }
494            }
495            result
496        };
497
498        // Add timestamp
499        let now = chrono::Local::now();
500        let timestamped = format!(
501            "[{}] {}",
502            now.format("%Y-%m-%d %H:%M:%S%.3f"),
503            formatted_message
504        );
505
506        // Store log message
507        self.logs.push(timestamped.clone());
508
509        // Automatically write to log file
510        let _ = self.write_log_to_file(&timestamped);
511
512        // Auto-scroll log view if currently showing logs
513        if self.show_logs {
514            self.auto_scroll_to_bottom();
515        }
516    }
517
518    fn toggle_log_view(&mut self) {
519        self.show_logs = !self.show_logs;
520
521        // Log the view change
522        if self.show_logs {
523            self.log("Switched to log view", &[]);
524        } else {
525            self.log("Switched to conversation view", &[]);
526        }
527    }
528
529    fn get_log_directory(&self) -> std::path::PathBuf {
530        let mut log_dir = dirs::home_dir().unwrap_or_else(|| std::path::PathBuf::from("."));
531        log_dir.push(".oli");
532        log_dir.push("logs");
533        log_dir
534    }
535
536    fn get_log_file_path(&self) -> std::path::PathBuf {
537        let log_dir = self.get_log_directory();
538        let date = chrono::Local::now().format("%Y-%m-%d").to_string();
539        let filename = format!("oli_{}_{}.log", date, self.session_id);
540        log_dir.join(filename)
541    }
542
543    fn write_log_to_file(&self, message: &str) -> Result<()> {
544        use std::io::Write;
545
546        // Create log directory if it doesn't exist
547        let log_dir = self.get_log_directory();
548        if !log_dir.exists() {
549            std::fs::create_dir_all(&log_dir)?;
550        }
551
552        // Append to log file
553        let log_path = self.get_log_file_path();
554        let mut file = std::fs::OpenOptions::new()
555            .create(true)
556            .append(true)
557            .open(log_path)?;
558
559        file.write_all(format!("{}\n", message).as_bytes())?;
560
561        Ok(())
562    }
563}
564
565impl ErrorHandler for App {
566    fn handle_error(&mut self, message: String) {
567        self.error_message = Some(message.clone());
568        self.messages.push(format!("Error: {}", message));
569
570        // Also log the error
571        if self.debug_messages {
572            self.log("ERROR: {}", &[&message]);
573        }
574    }
575}
576
577impl ModelManager for App {
578    fn current_model(&self) -> &ModelConfig {
579        &self.available_models[self.selected_model]
580    }
581
582    fn select_next_model(&mut self) {
583        self.selected_model = (self.selected_model + 1) % self.available_models.len();
584    }
585
586    fn select_prev_model(&mut self) {
587        self.selected_model = if self.selected_model == 0 {
588            self.available_models.len() - 1
589        } else {
590            self.selected_model - 1
591        };
592    }
593
594    fn get_agent_model(&self) -> Option<String> {
595        // Return the appropriate model ID based on the current selected model
596        let model_name = self.current_model().name.as_str();
597        let has_api_key = std::env::var("ANTHROPIC_API_KEY").is_ok()
598            || std::env::var("OPENAI_API_KEY").is_ok()
599            || self.api_key.is_some();
600
601        agent::determine_agent_model(model_name, has_api_key)
602    }
603
604    fn load_model(&mut self, _model_path: &Path) -> Result<()> {
605        if self.debug_messages {
606            self.log("Model loading requested", &[]);
607        }
608
609        let model_config = self.current_model();
610
611        // Check if the model supports agent capabilities
612        let supports_agent = model_config
613            .agentic_capabilities
614            .as_ref()
615            .map(|caps| !caps.is_empty())
616            .unwrap_or(false);
617
618        // Try setting up agent if supported
619        if supports_agent {
620            if let Err(e) = self.setup_agent() {
621                self.messages.push(format!(
622                    "WARNING: Failed to initialize agent capabilities: {}",
623                    e
624                ));
625                self.log("Failed to initialize agent: {}", &[&e.to_string()]);
626                self.use_agent = false;
627            } else if self.use_agent {
628                self.messages.push(
629                    "💡 Agent capabilities enabled! You can now use advanced code tasks.".into(),
630                );
631                self.messages
632                    .push("Try asking about files, editing code, or running commands.".into());
633                self.state = AppState::Chat;
634
635                // If agent is successfully set up, we're done
636                if self.agent.is_some() {
637                    return Ok(());
638                }
639            }
640        }
641        // Set appropriate app state
642        self.state = AppState::Chat;
643
644        Ok(())
645    }
646
647    fn setup_models(&mut self, tx: mpsc::Sender<String>) -> Result<()> {
648        if self.debug_messages {
649            self.log("setup_models called", &[]);
650        }
651
652        self.error_message = None;
653
654        let model_name = self.current_model().name.clone();
655
656        self.messages
657            .push(format!("Setting up model: {}", model_name));
658
659        // Check if this is an Ollama local model (which doesn't need an API key)
660        let is_ollama_model = model_name.contains("Local");
661
662        // Check if we need to ask for API key based on the selected model
663        let needs_api_key = if is_ollama_model {
664            false // Ollama models don't need API keys
665        } else {
666            match model_name.as_str() {
667                "GPT-4o" => std::env::var("OPENAI_API_KEY").is_err() && self.api_key.is_none(),
668                "Claude 3.7 Sonnet" => {
669                    std::env::var("ANTHROPIC_API_KEY").is_err() && self.api_key.is_none()
670                }
671                _ => true, // Default to requiring API key
672            }
673        };
674
675        if needs_api_key {
676            // Transition to API key input state
677            self.state = AppState::ApiKeyInput;
678            self.input.clear();
679            tx.send("api_key_needed".into())?;
680            return Ok(());
681        }
682
683        // Setup agent with the appropriate model
684        if let Err(e) = self.setup_agent() {
685            self.handle_error(format!("Failed to setup {}: {}", model_name, e));
686            tx.send("setup_failed".into())?;
687            return Ok(());
688        }
689
690        // If agent is successfully set up, we're done
691        if self.use_agent && self.agent.is_some() {
692            tx.send("setup_complete".into())?;
693            Ok(())
694        } else {
695            // Check if this is an Ollama model that should have worked
696            if model_name.contains("Local") {
697                self.handle_error(
698                    "Failed to connect to Ollama server. Make sure Ollama is running with 'ollama serve'".to_string(),
699                );
700                // Add a helpful message with instructions
701                self.messages
702                    .push("Run 'ollama serve' in a separate terminal window and try again.".into());
703                self.messages.push("If Ollama is already running, check that it's available at http://localhost:11434".into());
704            } else {
705                let provider_name = match model_name.as_str() {
706                    "GPT-4o" => "OpenAI",
707                    _ => "Anthropic",
708                };
709                self.handle_error(format!("{} API key not found or is invalid", provider_name));
710            }
711            tx.send("setup_failed".into())?;
712            Ok(())
713        }
714    }
715}
716
717impl PermissionHandler for App {
718    fn requires_permission(&self, tool_name: &str) -> bool {
719        // Tools that require permission for potentially destructive operations
720        match tool_name {
721            "Edit" | "Replace" | "NotebookEditCell" => true, // File modification
722            "Bash" => true,                                  // Shell commands (may be destructive)
723            // Add other tools that require permission here
724            _ => false, // Other tools don't require permission
725        }
726    }
727
728    fn request_tool_permission(&mut self, tool_name: &str, args: &str) -> ToolPermissionStatus {
729        // If permission is not required for this tool, auto-grant
730        if !self.requires_permission(tool_name) {
731            return ToolPermissionStatus::Granted;
732        }
733
734        // Log permission request if debug mode enabled
735        if self.debug_messages {
736            self.log(
737                "Permission requested for tool: {} with args: {}",
738                &[tool_name, args],
739            );
740        }
741
742        // Create a user-friendly description of what the tool will do
743        let description = match tool_name {
744            "Edit" => {
745                if let Some(file_path) = self.extract_argument(args, "file_path") {
746                    format!("Modify file '{}'", file_path)
747                } else {
748                    "Edit a file".to_string()
749                }
750            }
751            "Replace" => {
752                if let Some(file_path) = self.extract_argument(args, "file_path") {
753                    format!("Overwrite file '{}'", file_path)
754                } else {
755                    "Replace a file".to_string()
756                }
757            }
758            "NotebookEditCell" => {
759                if let Some(notebook_path) = self.extract_argument(args, "notebook_path") {
760                    format!("Edit Jupyter notebook '{}'", notebook_path)
761                } else {
762                    "Edit a Jupyter notebook".to_string()
763                }
764            }
765            "Bash" => {
766                if let Some(command) = self.extract_argument(args, "command") {
767                    format!("Execute command: '{}'", command)
768                } else {
769                    "Execute a shell command".to_string()
770                }
771            }
772            _ => format!("Execute tool: {}", tool_name),
773        };
774
775        // Create a message for display
776        let display_message = format!(
777            "[permission] ⚠️ Permission required: {} - Press 'y' to allow or 'n' to deny",
778            description
779        );
780
781        // Set up the permission request
782        self.permission_required = true;
783        self.pending_tool = Some(PendingToolExecution {
784            tool_name: tool_name.to_string(),
785            tool_args: args.to_string(),
786            description: description.clone(),
787        });
788        self.tool_permission_status = ToolPermissionStatus::Pending;
789
790        // Add a message to indicate permission is needed
791        self.messages.push(display_message);
792        self.auto_scroll_to_bottom();
793
794        // Return pending status - UI will handle getting actual permission
795        ToolPermissionStatus::Pending
796    }
797
798    fn handle_permission_response(&mut self, granted: bool) {
799        if granted {
800            self.tool_permission_status = ToolPermissionStatus::Granted;
801            self.messages
802                .push("[permission] ✅ Permission granted, executing tool...".to_string());
803
804            // Log permission grant if debug mode enabled
805            if self.debug_messages {
806                self.log(
807                    "Permission GRANTED for tool: {}",
808                    &[&self
809                        .pending_tool
810                        .as_ref()
811                        .map_or("unknown".to_string(), |t| t.tool_name.clone())],
812                );
813            }
814        } else {
815            self.tool_permission_status = ToolPermissionStatus::Denied;
816            self.messages
817                .push("[permission] ❌ Permission denied, skipping tool execution".to_string());
818
819            // Log permission denial if debug mode enabled
820            if self.debug_messages {
821                self.log(
822                    "Permission DENIED for tool: {}",
823                    &[&self
824                        .pending_tool
825                        .as_ref()
826                        .map_or("unknown".to_string(), |t| t.tool_name.clone())],
827                );
828            }
829        }
830        self.auto_scroll_to_bottom();
831    }
832
833    fn extract_argument(&self, args: &str, arg_name: &str) -> Option<String> {
834        // Simple parsing of JSON-like string to extract a specific argument
835        if let Some(start_idx) = args.find(&format!("\"{}\":", arg_name)) {
836            let value_start = args[start_idx..].find(":").map(|i| start_idx + i + 1)?;
837            let value_text = args[value_start..].trim();
838
839            // Check if value is a quoted string
840            if let Some(stripped) = value_text.strip_prefix("\"") {
841                let end_idx = stripped.find("\"").map(|i| value_start + i + 1)?;
842                Some(value_text[1..end_idx].to_string())
843            } else {
844                // Non-string value - try to extract until comma or closing brace
845                let end_chars = [',', '}'];
846                let end_idx = end_chars
847                    .iter()
848                    .filter_map(|c| value_text.find(*c))
849                    .min()
850                    .map(|i| value_start + i)?;
851                Some(value_text[..end_idx - value_start].trim().to_string())
852            }
853        } else {
854            None
855        }
856    }
857
858    fn requires_permission_check(&self) -> bool {
859        true // Default to requiring permission for risky operations
860    }
861}
862
863impl AgentManager for App {
864    fn setup_agent(&mut self) -> Result<()> {
865        // Check if API keys are available either from env vars or from user input
866        let has_anthropic_key =
867            std::env::var("ANTHROPIC_API_KEY").is_ok() || self.api_key.is_some();
868        let has_openai_key = std::env::var("OPENAI_API_KEY").is_ok() || self.api_key.is_some();
869
870        // Check if the selected model is an Ollama model
871        let is_ollama_model = self.current_model().name.contains("Local");
872
873        // Determine appropriate provider based on the selected model
874        let provider = match agent::determine_provider(
875            self.current_model().name.as_str(),
876            has_anthropic_key,
877            has_openai_key,
878        ) {
879            Some(provider) => provider,
880            None => {
881                // Check if this is an Ollama model (which doesn't need API keys)
882                if is_ollama_model {
883                    LLMProvider::Ollama
884                } else {
885                    // No valid provider found
886                    self.messages.push(
887                        "No API key found for any provider. Agent features will be disabled."
888                            .into(),
889                    );
890                    self.messages.push("To enable agent features, set ANTHROPIC_API_KEY or OPENAI_API_KEY environment variable.".into());
891                    self.use_agent = false;
892                    return Ok(());
893                }
894            }
895        };
896
897        // Create progress channel
898        let (tx, rx) = mpsc::channel();
899        self.agent_progress_rx = Some(rx);
900
901        // Create the agent with API key if provided by user
902        let mut agent = if let Some(api_key) = &self.api_key {
903            Agent::new_with_api_key(provider.clone(), api_key.clone())
904        } else {
905            Agent::new(provider.clone())
906        };
907
908        // Add model if specified
909        if let Some(model) = self.get_agent_model() {
910            agent = agent.with_model(model);
911        }
912
913        // Initialize agent in the tokio runtime
914        if let Some(runtime) = &self.tokio_runtime {
915            runtime.block_on(async {
916                let result = if let Some(api_key) = self.api_key.clone() {
917                    // If we have a direct API key, use it (handles both user-input and env var)
918                    agent.initialize_with_api_key(api_key).await
919                } else {
920                    // Otherwise try to initialize from environment variables
921                    agent.initialize().await
922                };
923
924                if let Err(e) = result {
925                    tx.send(format!("Failed to initialize agent: {}", e))
926                        .unwrap();
927                }
928            });
929
930            self.agent = Some(agent);
931            self.use_agent = true;
932
933            // Show provider-specific message
934            match provider {
935                LLMProvider::Anthropic => {
936                    self.messages
937                        .push("Claude 3.7 Sonnet agent capabilities enabled!".into());
938                    self.log(
939                        "Agent capabilities enabled using Anthropic Claude provider",
940                        &[],
941                    );
942                }
943                LLMProvider::OpenAI => {
944                    self.messages
945                        .push("GPT-4o agent capabilities enabled!".into());
946                    self.log("Agent capabilities enabled using OpenAI provider", &[]);
947                }
948                LLMProvider::Ollama => {
949                    // Get the model name to show in the message
950                    let model_name = self.current_model().file_name.clone();
951                    self.messages.push(format!(
952                        "Local Ollama {} agent capabilities enabled!",
953                        model_name
954                    ));
955                    self.log(
956                        "Agent capabilities enabled using Ollama provider with model: {}",
957                        &[&model_name],
958                    );
959                }
960            }
961        } else {
962            self.messages
963                .push("Failed to create async runtime. Agent features will be disabled.".into());
964            self.log("Failed to create async runtime for agent", &[]);
965            self.use_agent = false;
966        }
967
968        Ok(())
969    }
970
971    fn query_model(&mut self, prompt: &str) -> Result<String> {
972        if self.debug_messages {
973            let truncated_prompt = if prompt.len() > 50 {
974                format!("{}...", &prompt[..50])
975            } else {
976                prompt.to_string()
977            };
978            self.log("Querying model with: {}", &[&truncated_prompt]);
979        }
980
981        // Check if the conversation needs to be summarized
982        if self.should_compress() {
983            if self.debug_messages {
984                self.log("Auto-summarizing conversation before query", &[]);
985            }
986
987            // Try to summarize, but continue even if it fails
988            if let Err(e) = self.compress_context() {
989                if self.debug_messages {
990                    self.log("Failed to summarize: {}", &[&e.to_string()]);
991                }
992            }
993        }
994
995        // Try using agent if enabled
996        if self.use_agent && self.agent.is_some() {
997            return self.query_with_agent(prompt);
998        }
999
1000        // Check if this is an Ollama model
1001        if self.current_model().name.contains("Local") {
1002            let error_msg =
1003                "Failed to initialize Ollama model. Please make sure Ollama is running with 'ollama serve'.";
1004            self.messages.push(format!("ERROR: {}", error_msg));
1005            self.messages
1006                .push("Run 'ollama serve' in a separate terminal window and try again.".into());
1007            self.messages.push(
1008                "If Ollama is already running, check that it's available at http://localhost:11434"
1009                    .into(),
1010            );
1011
1012            // Get the model name (clone it to avoid borrow issues)
1013            let model_name = self.current_model().file_name.clone();
1014            self.messages
1015                .push(format!("Attempted to use model: {}", model_name));
1016
1017            // Suggest downloading the model if needed
1018            self.messages.push(format!(
1019                "If this model is not available, run: ollama pull {}",
1020                model_name
1021            ));
1022
1023            Err(anyhow::anyhow!(error_msg))
1024        } else {
1025            // Other models that should be using API clients
1026            let error_msg = "API client setup failed. Please check your API keys.";
1027            self.messages.push(format!("ERROR: {}", error_msg));
1028            Err(anyhow::anyhow!(error_msg))
1029        }
1030    }
1031
1032    fn query_with_agent(&mut self, prompt: &str) -> Result<String> {
1033        // Make sure we have a tokio runtime
1034        let runtime = match &self.tokio_runtime {
1035            Some(rt) => rt,
1036            None => return Err(anyhow::anyhow!("Async runtime not available")),
1037        };
1038
1039        // Make sure we have an agent
1040        let agent_opt = self.agent.clone();
1041        let mut agent = match agent_opt {
1042            Some(agent) => agent,
1043            None => return Err(anyhow::anyhow!("Agent not initialized")),
1044        };
1045
1046        // If we have a session manager, get conversation history and update the agent
1047        if let Some(session) = &mut self.session_manager {
1048            // Add the current user query to the session
1049            session.add_user_message(prompt.to_string());
1050
1051            // Get the full conversation history from the session manager
1052            let session_messages = session.get_messages_for_api();
1053
1054            // Update the agent's conversation history with all messages
1055            // The session already contains the user query, so no need to add it again
1056            let mut agent_mut = agent.clone();
1057            agent_mut.clear_history();
1058            for msg in session_messages {
1059                agent_mut.add_message(msg);
1060            }
1061            agent = agent_mut;
1062        } else {
1063            // If we don't have a session manager, add the user query directly to the agent
1064            let mut agent_mut = agent.clone();
1065            agent_mut.clear_history();
1066            agent_mut.add_message(Message::user(prompt.to_string()));
1067            agent = agent_mut;
1068        }
1069
1070        // Create a progress channel
1071        let (progress_tx, progress_rx) = mpsc::channel();
1072        self.agent_progress_rx = Some(progress_rx);
1073
1074        // Force immediate update of the UI without adding unnecessary spacing
1075        self.messages.push("_AUTO_SCROLL_".to_string());
1076
1077        // Set tool execution flag
1078        self.tool_execution_in_progress = true;
1079
1080        // We'll add the log directly to the logs vector instead of using the log method
1081        // to avoid borrowing issues with the runtime
1082        if self.debug_messages {
1083            // Create a timestamp
1084            let now = chrono::Local::now();
1085            let log_message = format!(
1086                "[{}] Tool execution started",
1087                now.format("%Y-%m-%d %H:%M:%S%.3f")
1088            );
1089            self.logs.push(log_message.clone());
1090
1091            // Also write to log file without using the log method
1092            let _ = self.write_log_to_file(&log_message);
1093        }
1094        let prompt_clone = prompt.to_string();
1095
1096        // Process this as a background task in the tokio runtime
1097        let (response_tx, response_rx) = mpsc::channel();
1098
1099        // Need to pass app state for tool permission checks
1100        let app_permission_required = self.requires_permission_check();
1101
1102        runtime.spawn(async move {
1103            // Set up the agent with progress sender
1104            let (tokio_progress_tx, mut tokio_progress_rx) = tokio::sync::mpsc::channel(100);
1105            let agent_with_progress = agent.with_progress_sender(tokio_progress_tx);
1106
1107            // Create a channel for the response
1108            let (final_response_tx, final_response_rx) = tokio::sync::oneshot::channel();
1109
1110            // Execute the query in a separate task
1111            tokio::spawn(async move {
1112                // Execute the actual query in background
1113                match agent_with_progress.execute(&prompt_clone).await {
1114                    Ok(response) => {
1115                        // Process response format
1116                        let processed_response =
1117                            if response.trim().starts_with("{") && response.trim().ends_with("}") {
1118                                // If it's JSON, ensure it's properly formatted
1119                                match serde_json::from_str::<serde_json::Value>(&response) {
1120                                    Ok(json) => {
1121                                        if let Ok(pretty) = serde_json::to_string_pretty(&json) {
1122                                            pretty
1123                                        } else {
1124                                            response
1125                                        }
1126                                    }
1127                                    Err(_) => response,
1128                                }
1129                            } else {
1130                                response
1131                            };
1132
1133                        // Signal that we're in the final response phase - but can't access progress_tx from here
1134                        // We'll handle the final message in the outer scope
1135                        // Send final response through the oneshot channel
1136                        let _ = final_response_tx.send(Ok(processed_response));
1137                    }
1138                    Err(e) => {
1139                        // Send error through the oneshot channel
1140                        let _ = final_response_tx.send(Err(e));
1141                    }
1142                }
1143            });
1144
1145            // Forward progress messages in real-time while waiting for the final response
1146            // Need to clone the progress sender for use in multiple places
1147            let error_progress_tx = progress_tx.clone();
1148            let forwarder_progress_tx = progress_tx.clone();
1149
1150            // Create a separate task to forward progress messages (don't need to track the handle)
1151            let _progress_forwarder = tokio::spawn(async move {
1152                while let Some(msg) = tokio_progress_rx.recv().await {
1153                    // Check for tool execution messages that require permission
1154                    if app_permission_required
1155                        && (msg.contains("Using tool: Edit")
1156                            || msg.contains("Using tool: Replace")
1157                            || msg.contains("Using tool: Bash")
1158                            || msg.contains("Using tool: NotebookEditCell"))
1159                    {
1160                        // Extract tool name and args
1161                        if let Some(tool_info) = msg.strip_prefix("Using tool: ") {
1162                            let parts: Vec<&str> = tool_info.splitn(2, " with args: ").collect();
1163                            if parts.len() == 2 {
1164                                let tool_name = parts[0];
1165                                let tool_args = parts[1];
1166
1167                                // Send special permission request message
1168                                let _ = forwarder_progress_tx.send(format!(
1169                                    "[permission_request]{}|{}",
1170                                    tool_name, tool_args
1171                                ));
1172
1173                                // Add auto-scroll flag to ensure the permission dialog shows
1174                                let _ = forwarder_progress_tx.send("_AUTO_SCROLL_".to_string());
1175
1176                                // Wait a bit to allow UI to process the permission request
1177                                // This is not ideal but works as a simple solution
1178                                tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
1179                            }
1180                        }
1181                    }
1182
1183                    // For each progress message, add an auto-scroll marker to ensure the UI updates
1184                    let _ = forwarder_progress_tx.send(msg);
1185                    // Add auto-scroll flag to ensure the UI updates in real-time
1186                    let _ = forwarder_progress_tx.send("_AUTO_SCROLL_".to_string());
1187                }
1188            });
1189
1190            // Wait for the final response
1191            match final_response_rx.await {
1192                Ok(Ok(response)) => {
1193                    // No need for finalizing messages - maintain clean async style
1194                    // Send the final response
1195                    let _ = response_tx.send(Ok(response));
1196                }
1197                Ok(Err(e)) => {
1198                    // Send error message using the cloned sender
1199                    let _ = error_progress_tx
1200                        .send(format!("[error] ❌ Error during processing: {}", e));
1201                    let _ = response_tx.send(Err(e));
1202                }
1203                Err(_) => {
1204                    // Channel closed unexpectedly
1205                    let _ = response_tx.send(Err(anyhow::anyhow!(
1206                        "Agent processing channel closed unexpectedly"
1207                    )));
1208                }
1209            }
1210
1211            // No need to explicitly abort, the task will end when the tokio runtime is dropped
1212        });
1213
1214        // Wait for the response with a timeout (2 minutes) and return the final result
1215        let result = response_rx.recv_timeout(Duration::from_secs(120))?;
1216
1217        // Clear tool execution state
1218        self.tool_execution_in_progress = false;
1219        self.permission_required = false;
1220        self.pending_tool = None;
1221
1222        if self.debug_messages {
1223            self.log("Tool execution completed", &[]);
1224        }
1225
1226        // For now, we extract tokens in the UI layer based on response length
1227        // In the future, we could update this to use actual token counts from the API
1228        // The token usage will be recorded when completing the task in ui/events.rs
1229
1230        // If successful, store the response in the session manager
1231        if let Ok(response) = &result {
1232            if let Some(session) = &mut self.session_manager {
1233                session.add_assistant_message(response.clone());
1234            }
1235        }
1236
1237        result
1238    }
1239}