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;
12use 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
21pub 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 let _ = dotenv();
47
48 let tokio_runtime = Runtime::new().ok();
50
51 let current_working_dir = std::env::current_dir()
53 .ok()
54 .map(|p| p.to_string_lossy().to_string());
55
56 let mut textarea = TextArea::default();
58 textarea.set_placeholder_text("Type your message here or type / for commands");
60 textarea.set_cursor_line_style(ratatui::style::Style::default());
61 textarea.set_style(ratatui::style::Style::default().fg(ratatui::style::Color::LightCyan));
63
64 let session_manager =
66 Some(SessionManager::new(100).with_system_message(DEFAULT_SESSION_PROMPT.to_string()));
67
68 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![], show_logs: false, selected_model: 0,
79 available_models: get_available_models(),
80 error_message: None,
81 debug_messages: false, message_scroll: ScrollState::new(),
83 log_scroll: ScrollState::new(), scroll_position: 0, last_query_time: std::time::Instant::now(),
86 last_message_time: std::time::Instant::now(), use_agent: false,
88 agent: None,
89 tokio_runtime,
90 agent_progress_rx: None,
91 api_key: None,
92 current_working_dir,
93 command_mode: false,
95 available_commands: get_available_commands(),
96 selected_command: 0,
97 show_command_menu: false,
98 permission_required: false,
100 pending_tool: None,
101 tool_permission_status: ToolPermissionStatus::Pending,
102 tool_execution_in_progress: false,
103 show_intermediate_steps: true, show_shortcuts_hint: true, show_detailed_shortcuts: false, cursor_position: 0, tasks: Vec::new(),
110 current_task_id: None,
111 task_scroll: ScrollState::new(),
112 task_scroll_position: 0, conversation_summaries: Vec::new(),
115 session_manager,
117 session_id,
119 }
120 }
121}
122
123impl CommandHandler for App {
125 fn check_command_mode(&mut self) {
126 let was_in_command_mode = self.command_mode;
128
129 let input_text = self.textarea.lines().join("\n");
131
132 self.input = input_text.clone();
134
135 self.command_mode = input_text.starts_with('/');
137 self.show_command_menu = self.command_mode && !input_text.contains(' ');
138
139 if self.command_mode {
141 let filtered = self.filtered_commands();
142
143 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 self.selected_command = 0;
153
154 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 self.available_commands.clone();
169 }
170
171 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 let filtered = self.filtered_commands();
182
183 if self.show_command_menu && !filtered.is_empty() {
184 let num_commands = filtered.len();
186
187 if num_commands == 0 {
189 return; }
191
192 self.selected_command = self.selected_command.min(num_commands - 1);
194
195 self.selected_command = (self.selected_command + 1) % num_commands;
197
198 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 let filtered = self.filtered_commands();
214
215 if self.show_command_menu && !filtered.is_empty() {
216 let num_commands = filtered.len();
218
219 if num_commands == 0 {
221 return; }
223
224 self.selected_command = self.selected_command.min(num_commands - 1);
226
227 self.selected_command = if self.selected_command == 0 {
229 num_commands - 1 } else {
231 self.selected_command - 1
232 };
233
234 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 let command_to_execute = if self.show_command_menu {
254 let filtered = self.filtered_commands();
256 if filtered.is_empty() {
257 return false;
258 }
259
260 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 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 true
277 }
278 "/clear" => {
279 self.clear_history();
280 self.messages.push("Conversation history cleared.".into());
281 true
282 }
283 "/debug" => {
284 self.debug_messages = !self.debug_messages;
286 self.show_logs = self.debug_messages; self.messages.push(format!(
289 "Debug messages {}.",
290 if self.debug_messages {
291 "enabled"
292 } else {
293 "disabled"
294 }
295 ));
296
297 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 self.messages
308 .push("Returning to normal conversation view.".into());
309 }
310
311 true
312 }
313 "/steps" => {
314 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 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 self.log_scroll.scroll_up(amount);
370 } else {
371 self.message_scroll.scroll_up(amount);
373 self.scroll_position = self.message_scroll.position;
375 }
376 }
377
378 fn scroll_down(&mut self, amount: usize) {
379 if self.show_logs {
380 self.log_scroll.scroll_down(amount);
382 } else {
383 self.message_scroll.scroll_down(amount);
385 self.scroll_position = self.message_scroll.position;
387 }
388 }
389
390 fn auto_scroll_to_bottom(&mut self) {
391 if self.show_logs {
392 self.log_scroll.scroll_to_bottom();
394 } else {
395 self.message_scroll.scroll_to_bottom();
397 self.scroll_position = self.message_scroll.position;
399 }
400 }
401
402 fn scroll_tasks_up(&mut self, amount: usize) {
403 self.task_scroll.scroll_up(amount);
405
406 self.task_scroll_position = self.task_scroll.position;
408 }
409
410 fn scroll_tasks_down(&mut self, amount: usize) {
411 self.task_scroll.scroll_down(amount);
413
414 self.task_scroll_position = self.task_scroll.position;
416 }
417}
418
419impl App {
421 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 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 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 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 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 pub fn complete_current_task(&mut self, tokens: u32) {
465 if let Some(task) = self.current_task_mut() {
466 task.complete(0, tokens); }
470 self.current_task_id = None;
471 }
472
473 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 let formatted_message = if args.is_empty() {
486 message.to_string()
487 } else {
488 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 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 self.logs.push(timestamped.clone());
508
509 let _ = self.write_log_to_file(×tamped);
511
512 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 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 let log_dir = self.get_log_directory();
548 if !log_dir.exists() {
549 std::fs::create_dir_all(&log_dir)?;
550 }
551
552 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 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 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 let supports_agent = model_config
613 .agentic_capabilities
614 .as_ref()
615 .map(|caps| !caps.is_empty())
616 .unwrap_or(false);
617
618 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 self.agent.is_some() {
637 return Ok(());
638 }
639 }
640 }
641 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 let is_ollama_model = model_name.contains("Local");
661
662 let needs_api_key = if is_ollama_model {
664 false } 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, }
673 };
674
675 if needs_api_key {
676 self.state = AppState::ApiKeyInput;
678 self.input.clear();
679 tx.send("api_key_needed".into())?;
680 return Ok(());
681 }
682
683 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 self.use_agent && self.agent.is_some() {
692 tx.send("setup_complete".into())?;
693 Ok(())
694 } else {
695 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 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 match tool_name {
721 "Edit" | "Replace" | "NotebookEditCell" => true, "Bash" => true, _ => false, }
726 }
727
728 fn request_tool_permission(&mut self, tool_name: &str, args: &str) -> ToolPermissionStatus {
729 if !self.requires_permission(tool_name) {
731 return ToolPermissionStatus::Granted;
732 }
733
734 if self.debug_messages {
736 self.log(
737 "Permission requested for tool: {} with args: {}",
738 &[tool_name, args],
739 );
740 }
741
742 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 let display_message = format!(
777 "[permission] ⚠️ Permission required: {} - Press 'y' to allow or 'n' to deny",
778 description
779 );
780
781 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 self.messages.push(display_message);
792 self.auto_scroll_to_bottom();
793
794 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 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 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 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 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 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 }
861}
862
863impl AgentManager for App {
864 fn setup_agent(&mut self) -> Result<()> {
865 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 let is_ollama_model = self.current_model().name.contains("Local");
872
873 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 if is_ollama_model {
883 LLMProvider::Ollama
884 } else {
885 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 let (tx, rx) = mpsc::channel();
899 self.agent_progress_rx = Some(rx);
900
901 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 if let Some(model) = self.get_agent_model() {
910 agent = agent.with_model(model);
911 }
912
913 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 agent.initialize_with_api_key(api_key).await
919 } else {
920 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 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 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 if self.should_compress() {
983 if self.debug_messages {
984 self.log("Auto-summarizing conversation before query", &[]);
985 }
986
987 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 if self.use_agent && self.agent.is_some() {
997 return self.query_with_agent(prompt);
998 }
999
1000 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 let model_name = self.current_model().file_name.clone();
1014 self.messages
1015 .push(format!("Attempted to use model: {}", model_name));
1016
1017 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 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 let runtime = match &self.tokio_runtime {
1035 Some(rt) => rt,
1036 None => return Err(anyhow::anyhow!("Async runtime not available")),
1037 };
1038
1039 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 let Some(session) = &mut self.session_manager {
1048 session.add_user_message(prompt.to_string());
1050
1051 let session_messages = session.get_messages_for_api();
1053
1054 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 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 let (progress_tx, progress_rx) = mpsc::channel();
1072 self.agent_progress_rx = Some(progress_rx);
1073
1074 self.messages.push("_AUTO_SCROLL_".to_string());
1076
1077 self.tool_execution_in_progress = true;
1079
1080 if self.debug_messages {
1083 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 let _ = self.write_log_to_file(&log_message);
1093 }
1094 let prompt_clone = prompt.to_string();
1095
1096 let (response_tx, response_rx) = mpsc::channel();
1098
1099 let app_permission_required = self.requires_permission_check();
1101
1102 runtime.spawn(async move {
1103 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 let (final_response_tx, final_response_rx) = tokio::sync::oneshot::channel();
1109
1110 tokio::spawn(async move {
1112 match agent_with_progress.execute(&prompt_clone).await {
1114 Ok(response) => {
1115 let processed_response =
1117 if response.trim().starts_with("{") && response.trim().ends_with("}") {
1118 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 let _ = final_response_tx.send(Ok(processed_response));
1137 }
1138 Err(e) => {
1139 let _ = final_response_tx.send(Err(e));
1141 }
1142 }
1143 });
1144
1145 let error_progress_tx = progress_tx.clone();
1148 let forwarder_progress_tx = progress_tx.clone();
1149
1150 let _progress_forwarder = tokio::spawn(async move {
1152 while let Some(msg) = tokio_progress_rx.recv().await {
1153 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 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 let _ = forwarder_progress_tx.send(format!(
1169 "[permission_request]{}|{}",
1170 tool_name, tool_args
1171 ));
1172
1173 let _ = forwarder_progress_tx.send("_AUTO_SCROLL_".to_string());
1175
1176 tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
1179 }
1180 }
1181 }
1182
1183 let _ = forwarder_progress_tx.send(msg);
1185 let _ = forwarder_progress_tx.send("_AUTO_SCROLL_".to_string());
1187 }
1188 });
1189
1190 match final_response_rx.await {
1192 Ok(Ok(response)) => {
1193 let _ = response_tx.send(Ok(response));
1196 }
1197 Ok(Err(e)) => {
1198 let _ = error_progress_tx
1200 .send(format!("[error] ❌ Error during processing: {}", e));
1201 let _ = response_tx.send(Err(e));
1202 }
1203 Err(_) => {
1204 let _ = response_tx.send(Err(anyhow::anyhow!(
1206 "Agent processing channel closed unexpectedly"
1207 )));
1208 }
1209 }
1210
1211 });
1213
1214 let result = response_rx.recv_timeout(Duration::from_secs(120))?;
1216
1217 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 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}