use crate::drive::convert_tree_to_items; use crate::core::shared::state::AppState; use color_eyre::Result; use crossterm::{ event::{self, Event, KeyCode, KeyModifiers}, execute, terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, }; use ratatui::{ backend::CrosstermBackend, layout::{Constraint, Direction, Layout, Rect}, style::{Color, Modifier, Style}, text::{Line, Span}, widgets::{Block, Borders, List, ListItem, Paragraph, Wrap}, Frame, Terminal, }; use std::io; use std::sync::Arc; use std::sync::Mutex; mod chat_panel; mod editor; pub mod file_tree; mod log_panel; mod status_panel; pub mod wizard; use chat_panel::ChatPanel; use editor::Editor; use file_tree::{FileTree, TreeNode}; use log_panel::{init_logger, LogPanel}; use status_panel::StatusPanel; #[derive(Debug)] pub struct XtreeUI { app_state: Option>, file_tree: Option, status_panel: Option, log_panel: Arc>, chat_panel: Option, editor: Option, active_panel: ActivePanel, should_quit: bool, progress_channel: Option< Arc>>, >, state_channel: Option>>>>, bootstrap_status: String, } impl Default for XtreeUI { fn default() -> Self { Self::new() } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum ActivePanel { FileTree, Editor, Status, Logs, Chat, } impl XtreeUI { pub fn new() -> Self { let log_panel = Arc::new(Mutex::new(LogPanel::new())); Self { app_state: None, file_tree: None, status_panel: None, log_panel, chat_panel: None, editor: None, active_panel: ActivePanel::Logs, should_quit: false, progress_channel: None, state_channel: None, bootstrap_status: "Initializing...".to_string(), } } pub fn set_progress_channel( &mut self, rx: Arc>>, ) { self.progress_channel = Some(rx); } pub fn set_state_channel( &mut self, rx: Arc>>>, ) { self.state_channel = Some(rx); } pub fn set_app_state(&mut self, app_state: Arc) { self.file_tree = Some(FileTree::new(app_state.clone())); self.status_panel = Some(StatusPanel::new(app_state.clone())); self.chat_panel = Some(ChatPanel::new(app_state.clone())); self.app_state = Some(app_state); self.active_panel = ActivePanel::FileTree; self.bootstrap_status = "Ready".to_string(); } pub fn start_ui(&mut self) -> Result<()> { color_eyre::install()?; if !std::io::IsTerminal::is_terminal(&std::io::stdout()) { return Ok(()); } enable_raw_mode()?; let mut stdout = io::stdout(); execute!(stdout, EnterAlternateScreen)?; let backend = CrosstermBackend::new(stdout); let mut terminal = Terminal::new(backend)?; if let Err(e) = init_logger(self.log_panel.clone()) { eprintln!("Warning: Could not initialize UI logger: {e}"); } log::set_max_level(log::LevelFilter::Trace); let result = self.run_event_loop(&mut terminal); disable_raw_mode()?; execute!(terminal.backend_mut(), LeaveAlternateScreen)?; terminal.show_cursor()?; result } fn run_event_loop( &mut self, terminal: &mut Terminal>, ) -> Result<()> { let mut last_update = std::time::Instant::now(); let update_interval = std::time::Duration::from_millis(1000); let mut cursor_blink = false; let mut last_blink = std::time::Instant::now(); let rt = tokio::runtime::Runtime::new()?; loop { if self.app_state.is_none() { if let Some(ref state_rx) = self.state_channel { if let Ok(mut rx) = state_rx.try_lock() { if let Ok(app_state) = rx.try_recv() { self.file_tree = Some(FileTree::new(app_state.clone())); self.status_panel = Some(StatusPanel::new(app_state.clone())); self.chat_panel = Some(ChatPanel::new(app_state.clone())); self.app_state = Some(app_state); self.active_panel = ActivePanel::FileTree; self.bootstrap_status = "Ready".to_string(); if let Ok(mut log_panel) = self.log_panel.lock() { log_panel.add_log("AppState received - UI fully initialized"); } } } } } if let Some(ref progress_rx) = self.progress_channel { if let Ok(mut rx) = progress_rx.try_lock() { while let Ok(progress) = rx.try_recv() { self.bootstrap_status = match progress { crate::BootstrapProgress::StartingBootstrap => { "Starting bootstrap...".to_string() } crate::BootstrapProgress::InstallingComponent(name) => { format!("Installing: {name}") } crate::BootstrapProgress::StartingComponent(name) => { format!("Starting: {name}") } crate::BootstrapProgress::UploadingTemplates => { "Uploading templates...".to_string() } crate::BootstrapProgress::ConnectingDatabase => { "Connecting to database...".to_string() } crate::BootstrapProgress::StartingLLM => { "Starting LLM servers...".to_string() } crate::BootstrapProgress::BootstrapComplete => { "Bootstrap complete".to_string() } crate::BootstrapProgress::BootstrapError(msg) => { format!("Error: {msg}") } }; } } } if last_blink.elapsed() >= std::time::Duration::from_millis(500) { cursor_blink = !cursor_blink; last_blink = std::time::Instant::now(); } terminal.draw(|f| self.render(f, cursor_blink))?; if self.app_state.is_some() && last_update.elapsed() >= update_interval { if let Err(e) = rt.block_on(self.update_data()) { if let Ok(mut log_panel) = self.log_panel.lock() { log_panel.add_log(&format!("Update error: {e}")); } } last_update = std::time::Instant::now(); } if event::poll(std::time::Duration::from_millis(50))? { if let Event::Key(key) = event::read()? { if let Err(e) = rt.block_on(self.handle_input(key.code, key.modifiers)) { if let Ok(mut log_panel) = self.log_panel.lock() { log_panel.add_log(&format!("Input error: {e}")); } } if self.should_quit { break; } } } } Ok(()) } fn render(&mut self, f: &mut Frame, cursor_blink: bool) { let bg = Color::Rgb(0, 30, 100); let border_focused = Color::Rgb(85, 255, 255); let border_dim = Color::Rgb(170, 170, 170); let text = Color::Rgb(255, 255, 255); let highlight = Color::Rgb(0, 170, 170); let header_bg_color = Color::Rgb(170, 170, 170); let header_text_color = Color::Rgb(0, 0, 0); if self.app_state.is_none() { self.render_loading( f, bg, text, border_focused, header_bg_color, header_text_color, ); return; } let main_chunks = Layout::default() .direction(Direction::Vertical) .constraints([ Constraint::Length(3), Constraint::Min(0), Constraint::Length(12), ]) .split(f.area()); self.render_header(f, main_chunks[0], bg, header_bg_color, header_text_color); let content_chunks = Layout::default() .direction(Direction::Horizontal) .constraints([ Constraint::Percentage(25), Constraint::Percentage(40), Constraint::Percentage(35), ]) .split(main_chunks[1]); self.render_file_tree( f, content_chunks[0], bg, text, border_focused, border_dim, highlight, header_bg_color, header_text_color, ); if self.editor.is_some() { if let Some(editor) = &mut self.editor { let area = content_chunks[1]; editor.set_visible_lines(area.height.saturating_sub(4) as usize); let is_active = self.active_panel == ActivePanel::Editor; let border_color = if is_active { border_focused } else { border_dim }; let title_style = if is_active { Style::default() .fg(header_text_color) .bg(header_bg_color) .add_modifier(Modifier::BOLD) } else { Style::default().fg(header_text_color).bg(header_bg_color) }; let title_text = format!(" EDITOR: {} ", editor.file_path()); let block = Block::default() .title(Span::styled(title_text, title_style)) .borders(Borders::ALL) .border_style(Style::default().fg(border_color)) .style(Style::default().bg(bg)); let content = editor.render(cursor_blink); let paragraph = Paragraph::new(content) .block(block) .style(Style::default().fg(text)) .wrap(Wrap { trim: false }); f.render_widget(paragraph, area); } self.render_chat( f, content_chunks[2], bg, text, border_focused, border_dim, highlight, header_bg_color, header_text_color, ); } else { let right_chunks = Layout::default() .direction(Direction::Vertical) .constraints([Constraint::Percentage(50), Constraint::Percentage(50)]) .split(content_chunks[1]); self.render_status( f, right_chunks[0], bg, text, border_focused, border_dim, highlight, header_bg_color, header_text_color, ); self.render_status( f, content_chunks[2], bg, text, border_focused, border_dim, highlight, header_bg_color, header_text_color, ); } self.render_logs( f, main_chunks[2], bg, text, border_focused, border_dim, highlight, header_bg_color, header_text_color, ); } fn render_header( &self, f: &mut Frame, area: Rect, _bg: Color, header_bg_color: Color, header_text_color: Color, ) { let block = Block::default().style(Style::default().bg(header_bg_color)); f.render_widget(block, area); let title = if self.app_state.is_some() { let components = [ ("Tables", "postgres", "5432"), ("Cache", "valkey-server", "6379"), ("Drive", "minio", "9000"), ("LLM", "llama-server", "8081"), ]; let statuses: Vec = components .iter() .map(|(comp_name, process, _port)| { let running = status_panel::StatusPanel::check_component_running(process); let icon = if running { "●" } else { "○" }; format!("{icon} {comp_name}") }) .collect(); format!(" GENERAL BOTS ┃ {} ", statuses.join(" ┃ ")) } else { " GENERAL BOTS ".to_string() }; let title_len = title.len() as u16; let centered_x = (area.width.saturating_sub(title_len)) / 2; let centered_y = area.y + 1; let x = area.x + centered_x; let max_width = area.width.saturating_sub(x - area.x); let width = title_len.min(max_width); let title_span = Span::styled( title, Style::default() .fg(header_text_color) .bg(header_bg_color) .add_modifier(Modifier::BOLD), ); f.render_widget( Paragraph::new(Line::from(title_span)), Rect { x, y: centered_y, width, height: 1, }, ); } fn render_loading( &self, f: &mut Frame, bg: Color, text: Color, border: Color, header_bg_color: Color, header_text_color: Color, ) { let main_chunks = Layout::default() .direction(Direction::Vertical) .constraints([ Constraint::Length(3), Constraint::Min(0), Constraint::Length(12), ]) .split(f.area()); let header_block = Block::default().style(Style::default().bg(header_bg_color)); f.render_widget(header_block, main_chunks[0]); let title = " GENERAL BOTS "; let title_len = title.len() as u16; let centered_x = (main_chunks[0].width.saturating_sub(title_len)) / 2; let title_span = Span::styled( title, Style::default() .fg(header_text_color) .bg(header_bg_color) .add_modifier(Modifier::BOLD), ); f.render_widget( Paragraph::new(Line::from(title_span)), Rect { x: main_chunks[0].x + centered_x, y: main_chunks[0].y + 1, width: title_len, height: 1, }, ); let content_chunks = Layout::default() .direction(Direction::Horizontal) .constraints([ Constraint::Percentage(25), Constraint::Percentage(40), Constraint::Percentage(35), ]) .split(main_chunks[1]); let file_block = Block::default() .title(Span::styled( " FILE EXPLORER ", Style::default().fg(header_text_color).bg(header_bg_color), )) .borders(Borders::ALL) .border_style(Style::default().fg(border)) .style(Style::default().bg(bg)); let file_text = Paragraph::new("\n\n Loading files...") .block(file_block) .style(Style::default().fg(Color::DarkGray)); f.render_widget(file_text, content_chunks[0]); let middle_chunks = Layout::default() .direction(Direction::Vertical) .constraints([Constraint::Percentage(50), Constraint::Percentage(50)]) .split(content_chunks[1]); let status_block = Block::default() .title(Span::styled( " STATUS ", Style::default().fg(header_text_color).bg(header_bg_color), )) .borders(Borders::ALL) .border_style(Style::default().fg(border)) .style(Style::default().bg(bg)); let status_text = format!( "\n {}\n\n Components:\n ○ Vault\n ○ Database\n ○ Drive\n ○ Cache\n ○ LLM", self.bootstrap_status ); let status_para = Paragraph::new(status_text) .block(status_block) .style(Style::default().fg(text)); f.render_widget(status_para, middle_chunks[0]); let empty_block = Block::default() .borders(Borders::ALL) .border_style(Style::default().fg(Color::DarkGray)) .style(Style::default().bg(bg)); f.render_widget(empty_block, middle_chunks[1]); let chat_block = Block::default() .title(Span::styled( " CHAT ", Style::default().fg(header_text_color).bg(header_bg_color), )) .borders(Borders::ALL) .border_style(Style::default().fg(border)) .style(Style::default().bg(bg)); let chat_text = Paragraph::new("\n\n Connecting...") .block(chat_block) .style(Style::default().fg(Color::DarkGray)); f.render_widget(chat_text, content_chunks[2]); let logs_block = Block::default() .title(Span::styled( " SYSTEM LOGS ", Style::default().fg(header_text_color).bg(header_bg_color), )) .borders(Borders::ALL) .border_style(Style::default().fg(border)) .style(Style::default().bg(bg)); let logs_visible_lines = main_chunks[2].height.saturating_sub(2) as usize; let logs_content = { if let Ok(panel) = self.log_panel.lock() { panel.render(logs_visible_lines) } else { String::from(" Waiting for logs...") } }; let logs_para = Paragraph::new(logs_content) .block(logs_block) .style(Style::default().fg(text)) .wrap(Wrap { trim: false }); f.render_widget(logs_para, main_chunks[2]); } fn render_file_tree( &self, f: &mut Frame, area: Rect, bg: Color, text: Color, border_focused: Color, border_dim: Color, highlight: Color, header_bg_color: Color, header_text_color: Color, ) { if let Some(file_tree) = &self.file_tree { let items = file_tree.render_items(); let selected = file_tree.selected_index(); let _file_items = convert_tree_to_items(file_tree); let list_items: Vec = items .iter() .enumerate() .map(|(idx, (display, _))| { let style = if idx == selected { Style::default() .bg(highlight) .fg(Color::Black) .add_modifier(Modifier::BOLD) } else { Style::default().fg(text) }; ListItem::new(Line::from(Span::styled(display.clone(), style))) }) .collect(); let is_active = self.active_panel == ActivePanel::FileTree; let border_color = if is_active { border_focused } else { border_dim }; let title_style = if is_active { Style::default() .fg(header_text_color) .bg(header_bg_color) .add_modifier(Modifier::BOLD) } else { Style::default().fg(header_text_color).bg(header_bg_color) }; let block = Block::default() .title(Span::styled(" FILE EXPLORER ", title_style)) .borders(Borders::ALL) .border_style(Style::default().fg(border_color)) .style(Style::default().bg(bg)); let list = List::new(list_items).block(block); f.render_widget(list, area); } } fn render_status( &mut self, f: &mut Frame, area: Rect, bg: Color, text: Color, border_focused: Color, border_dim: Color, _highlight: Color, header_bg_color: Color, header_text_color: Color, ) { let selected_bot_opt = self.file_tree.as_ref().and_then(|ft| ft.get_selected_bot()); let status_text = if let Some(status_panel) = &mut self.status_panel { match selected_bot_opt { Some(bot) => status_panel.render(Some(bot)), None => status_panel.render(None), } } else { "Waiting for initialization...".to_string() }; let is_active = self.active_panel == ActivePanel::Status; let border_color = if is_active { border_focused } else { border_dim }; let title_style = if is_active { Style::default() .fg(header_text_color) .bg(header_bg_color) .add_modifier(Modifier::BOLD) } else { Style::default().fg(header_text_color).bg(header_bg_color) }; let block = Block::default() .title(Span::styled(" SYSTEM STATUS ", title_style)) .borders(Borders::ALL) .border_style(Style::default().fg(border_color)) .style(Style::default().bg(bg)); let paragraph = Paragraph::new(status_text) .block(block) .style(Style::default().fg(text)) .wrap(Wrap { trim: false }); f.render_widget(paragraph, area); } fn render_chat( &self, f: &mut Frame, area: Rect, bg: Color, text: Color, border_focused: Color, border_dim: Color, _highlight: Color, header_bg_color: Color, header_text_color: Color, ) { if let Some(chat_panel) = &self.chat_panel { let is_active = self.active_panel == ActivePanel::Chat; let border_color = if is_active { border_focused } else { border_dim }; let title_style = if is_active { Style::default() .fg(header_text_color) .bg(header_bg_color) .add_modifier(Modifier::BOLD) } else { Style::default().fg(header_text_color).bg(header_bg_color) }; let selected_bot = if let Some(file_tree) = &self.file_tree { file_tree .get_selected_bot() .unwrap_or_else(|| "No bot selected".to_string()) } else { "No bot selected".to_string() }; let title_text = format!(" CHAT: {selected_bot} "); let block = Block::default() .title(Span::styled(title_text, title_style)) .borders(Borders::ALL) .border_style(Style::default().fg(border_color)) .style(Style::default().bg(bg)); let content = chat_panel.render(); let paragraph = Paragraph::new(content) .block(block) .style(Style::default().fg(text)) .wrap(Wrap { trim: false }); f.render_widget(paragraph, area); } } fn render_logs( &self, f: &mut Frame, area: Rect, bg: Color, text: Color, border_focused: Color, border_dim: Color, _highlight: Color, header_bg_color: Color, header_text_color: Color, ) { let visible_lines = area.height.saturating_sub(2) as usize; let (log_lines, can_scroll_up, can_scroll_down, logs_count, auto_scroll) = { let log_panel = self.log_panel.try_lock(); if let Ok(panel) = log_panel { let (content, up, down) = panel.render_with_scroll_indicator(visible_lines); ( content, up, down, panel.logs_count(), panel.is_auto_scroll(), ) } else { ("Loading logs...".to_string(), false, false, 0, true) } }; let is_active = self.active_panel == ActivePanel::Logs; let border_color = if is_active { border_focused } else { border_dim }; let title_style = if is_active { Style::default() .fg(header_text_color) .bg(header_bg_color) .add_modifier(Modifier::BOLD) } else { Style::default().fg(header_text_color).bg(header_bg_color) }; let scroll_indicator = if can_scroll_up && can_scroll_down { " [^v] " } else if can_scroll_up { " [^] " } else if can_scroll_down { " [v] " } else { "" }; let auto_indicator = if auto_scroll { "" } else { " [SCROLL] " }; let title_text = format!(" SYSTEM LOGS ({logs_count}) {scroll_indicator}{auto_indicator}"); let block = Block::default() .title(Span::styled(title_text, title_style)) .borders(Borders::ALL) .border_style(Style::default().fg(border_color)) .style(Style::default().bg(bg)); let paragraph = Paragraph::new(log_lines) .block(block) .style(Style::default().fg(text)) .wrap(Wrap { trim: false }); f.render_widget(paragraph, area); } async fn handle_input(&mut self, key: KeyCode, modifiers: KeyModifiers) -> Result<()> { if modifiers.contains(KeyModifiers::CONTROL) { match key { KeyCode::Char('c' | 'q') => { self.should_quit = true; return Ok(()); } KeyCode::Char('s') => { if let Some(editor) = &mut self.editor { if let Some(app_state) = &self.app_state { if let Err(e) = editor.save(app_state).await { if let Ok(mut log_panel) = self.log_panel.lock() { log_panel.add_log(&format!("Save failed: {e}")); } } else if let Ok(mut log_panel) = self.log_panel.lock() { log_panel.add_log(&format!("Saved: {}", editor.file_path())); } } } return Ok(()); } KeyCode::Char('w') => { if self.editor.is_some() { self.editor = None; self.active_panel = ActivePanel::FileTree; if let Ok(mut log_panel) = self.log_panel.lock() { log_panel.add_log("Closed editor"); } } return Ok(()); } _ => {} } } if self.app_state.is_none() { return Ok(()); } match self.active_panel { ActivePanel::FileTree => match key { KeyCode::Up => { if let Some(file_tree) = &mut self.file_tree { file_tree.move_up(); } } KeyCode::Down => { if let Some(file_tree) = &mut self.file_tree { file_tree.move_down(); } } KeyCode::Enter => { if let Err(e) = self.handle_tree_enter().await { if let Ok(mut log_panel) = self.log_panel.lock() { log_panel.add_log(&format!("Enter error: {e}")); } } } KeyCode::Backspace => { if let Some(file_tree) = &mut self.file_tree { if file_tree.go_up() { if let Err(e) = file_tree.refresh_current().await { if let Ok(mut log_panel) = self.log_panel.lock() { log_panel.add_log(&format!("Navigation error: {e}")); } } } } } KeyCode::Tab => { if self.editor.is_some() { self.active_panel = ActivePanel::Editor; } else { self.active_panel = ActivePanel::Logs; } } KeyCode::Char('q') => { self.should_quit = true; } KeyCode::F(5) => { if let Some(file_tree) = &mut self.file_tree { if let Err(e) = file_tree.refresh_current().await { if let Ok(mut log_panel) = self.log_panel.lock() { log_panel.add_log(&format!("Refresh failed: {e}")); } } else if let Ok(mut log_panel) = self.log_panel.lock() { log_panel.add_log("Refreshed"); } } } _ => {} }, ActivePanel::Editor => { if let Some(editor) = &mut self.editor { match key { KeyCode::Up => editor.move_up(), KeyCode::Down => editor.move_down(), KeyCode::Left => editor.move_left(), KeyCode::Right => editor.move_right(), KeyCode::PageUp => editor.page_up(), KeyCode::PageDown => editor.page_down(), KeyCode::Home => { if modifiers.contains(KeyModifiers::CONTROL) { editor.goto_line(1); } } KeyCode::End => { if modifiers.contains(KeyModifiers::CONTROL) { let line_count = editor.file_path().lines().count().max(1); editor.goto_line(line_count); } } KeyCode::Char(c) => editor.insert_char(c), KeyCode::Backspace => editor.backspace(), KeyCode::Enter => editor.insert_newline(), KeyCode::Tab => { self.active_panel = ActivePanel::Chat; } KeyCode::Esc => { self.editor = None; self.active_panel = ActivePanel::FileTree; if let Ok(mut log_panel) = self.log_panel.lock() { log_panel.add_log("Closed editor"); } } _ => {} } } } ActivePanel::Logs => match key { KeyCode::Up | KeyCode::Char('k') => { if let Ok(mut panel) = self.log_panel.lock() { panel.scroll_up(1); } } KeyCode::Down | KeyCode::Char('j') => { if let Ok(mut panel) = self.log_panel.lock() { panel.scroll_down(1, 10); } } KeyCode::PageUp => { if let Ok(mut panel) = self.log_panel.lock() { panel.page_up(10); } } KeyCode::PageDown => { if let Ok(mut panel) = self.log_panel.lock() { panel.page_down(10); } } KeyCode::Home => { if let Ok(mut panel) = self.log_panel.lock() { panel.scroll_to_top(); } } KeyCode::End => { if let Ok(mut panel) = self.log_panel.lock() { panel.scroll_to_bottom(); } } KeyCode::Tab => { self.active_panel = ActivePanel::Chat; } KeyCode::Char('q') => { self.should_quit = true; } _ => {} }, ActivePanel::Chat => match key { KeyCode::Tab => { if self.editor.is_some() { self.active_panel = ActivePanel::Editor; } else { self.active_panel = ActivePanel::FileTree; } } KeyCode::Enter => { if let (Some(chat_panel), Some(file_tree), Some(app_state)) = (&mut self.chat_panel, &self.file_tree, &self.app_state) { if let Some(bot_name) = file_tree.get_selected_bot() { if let Err(e) = chat_panel.send_message(&bot_name, app_state).await { if let Ok(mut log_panel) = self.log_panel.lock() { log_panel.add_log(&format!("Chat error: {e}")); } } } } } KeyCode::Char(c) => { if let Some(chat_panel) = &mut self.chat_panel { chat_panel.add_char(c); } } KeyCode::Backspace => { if let Some(chat_panel) = &mut self.chat_panel { chat_panel.backspace(); } } _ => {} }, ActivePanel::Status => { if key == KeyCode::Tab { self.active_panel = ActivePanel::Chat; } } } Ok(()) } async fn handle_tree_enter(&mut self) -> Result<()> { if let (Some(file_tree), Some(app_state)) = (&mut self.file_tree, &self.app_state) { if let Some(node) = file_tree.get_selected_node().cloned() { match node { TreeNode::Bucket { name, .. } => { file_tree.enter_bucket(name.clone()).await?; if let Ok(mut log_panel) = self.log_panel.lock() { log_panel.add_log(&format!("Opened bucket: {name}")); } } TreeNode::Folder { bucket, path, .. } => { file_tree.enter_folder(bucket.clone(), path.clone()).await?; if let Ok(mut log_panel) = self.log_panel.lock() { log_panel.add_log(&format!("Opened folder: {path}")); } } TreeNode::File { bucket, path, .. } => { match Editor::load(app_state, &bucket, &path).await { Ok(editor) => { self.editor = Some(editor); self.active_panel = ActivePanel::Editor; if let Ok(mut log_panel) = self.log_panel.lock() { log_panel.add_log(&format!("Editing: {path}")); } } Err(e) => { if let Ok(mut log_panel) = self.log_panel.lock() { log_panel.add_log(&format!("Failed to load file: {e}")); } } } } } } } Ok(()) } async fn update_data(&mut self) -> Result<()> { if let Some(status_panel) = &mut self.status_panel { status_panel.update()?; } if let Some(file_tree) = &self.file_tree { if file_tree.render_items().is_empty() { if let Some(file_tree) = &mut self.file_tree { file_tree.load_root().await?; } } } if let (Some(chat_panel), Some(file_tree)) = (&mut self.chat_panel, &self.file_tree) { if let Some(bot_name) = file_tree.get_selected_bot() { chat_panel.poll_response(&bot_name)?; } } Ok(()) } }