1828 lines · 68.7 KB · Rust
RawDownload
use anyhow::Result;use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};use serde::{Deserialize, Serialize};use std::sync::mpsc::{channel, Receiver, Sender};use std::sync::Arc;use std::time::Duration; use crate::ai::Summarizer;use crate::browser::{Browser, Page, RenderMode};use crate::chat::ChatMessage;use crate::config::Config;use crate::mascot::{Mascot, MascotState};use crate::search::{self, QueryTarget, SearchManager};use crate::scrape;use crate::storage::{Bookmark, Database, HistoryEntry};use crate::tabs::TabManager; #[derive(Debug, Clone, Copy, PartialEq, Eq)]pub enum Focus {    Content,    Sidebar,    URLBar,    TabBar,    Bookmarks,    History,} impl Focus {    pub fn next(&self) -> Self {        match self {            Focus::Content => Focus::Sidebar,            Focus::Sidebar => Focus::URLBar,            Focus::URLBar => Focus::Content,            Focus::TabBar => Focus::Content,            Focus::Bookmarks => Focus::Content,            Focus::History => Focus::Content,        }    }     pub fn previous(&self) -> Self {        match self {            Focus::Content => Focus::URLBar,            Focus::Sidebar => Focus::Content,            Focus::URLBar => Focus::Sidebar,            Focus::TabBar => Focus::Content,            Focus::Bookmarks => Focus::Content,            Focus::History => Focus::Content,        }    }} pub enum AppMessage {    PageLoaded(usize, Page), // tab_id, page    LoadError(usize, String),    AiSummary(String),    AiError(String),    ChatResponse(String),    ToolAction(ToolAction),    RagResponse(Vec<String>),    RagError(String),} /// Actions that can be triggered by AI tool calls#[derive(Debug, Clone)]pub enum ToolAction {    Navigate(String),    FollowLink(usize),    Scroll { direction: String, amount: usize },} /// Panel mode for overlays#[derive(Debug, Clone, Copy, PartialEq, Eq)]pub enum PanelMode {    None,    Bookmarks,    History,    Help,    Chat,    Settings,    Rag,    Memory,} pub struct App {    pub config: Config,    pub focus: Focus,    pub should_quit: bool,    pub animation_tick: usize,    pub status_message: String,    pub view_mode: ViewMode,    pub format_text: bool,    pub ai_summary: Option<String>,    pub panel_mode: PanelMode,     // Tabs    pub tabs: TabManager,    pub url_input: String,     // Mascot    pub mascot: Mascot,     // Storage    pub db: Option<Database>,    pub bookmarks_list: Vec<Bookmark>,    pub history_list: Vec<HistoryEntry>,    pub bookmarks_selected: usize,    pub history_selected: usize,     // Chat    pub chat_session: Option<crate::chat::ChatSession>,    pub chat_input: String,    pub chat_selected_model: usize,    pub chat_scroll: usize,    pub chat_focused: bool,     // Settings    pub settings_selected: usize,    pub settings_model_input: String,    pub settings_editing_model: bool,     // RAG & Memory    pub rag_client: Option<crate::rag::RagClient>,    pub rag_results: Vec<String>,    pub rag_query: String,    pub rag_scroll: usize,    pub rag_loading: bool,    pub memory_client: Option<crate::memory::MemoryClient>,    pub memory_nodes: Vec<String>,     // Message passing    page_rx: Receiver<AppMessage>,    page_tx: Sender<AppMessage>,    summarizer: Option<Arc<Summarizer>>,} #[derive(Debug, Clone, Copy, PartialEq, Eq)]pub enum ViewMode {    Rendered,    Raw,} impl App {    pub fn new() -> Result<Self> {        let config = Config::load()?;        let (page_tx, page_rx) = channel();         // Try to open database, but don't fail if it errors        let db = Database::open().ok();         // Initialize chat session with available models        let chat_session = if let Some(api_key) = config.get_api_key() {            if !api_key.is_empty() {                let model = config.get_ai_model().unwrap_or("qwen/qwen3-235b-a22b:free").to_string();                let mut models = vec![model.clone()];                if let Some(fallback) = config.get_fallback_models() {                    models.extend(fallback);                }                Some(crate::chat::ChatSession::new(model, models))            } else {                None            }        } else {            None        };         // Initialize RAG and memory clients        let rag_client = crate::rag::RagClient::from_config(&config);        let memory_client = crate::memory::MemoryClient::from_config(&config);         let mut app = Self {            config,            focus: Focus::Content,            should_quit: false,            animation_tick: 0,            status_message: "Ready - / search | t new tab | b bookmarks | c chat | r rag | m memory | ? help".to_string(),            view_mode: ViewMode::Rendered,            format_text: true,            ai_summary: None,            panel_mode: PanelMode::None,  // Chat is always visible, not a panel mode            tabs: TabManager::new(),            url_input: String::new(),            mascot: Mascot::new(),            db,            bookmarks_list: Vec::new(),            history_list: Vec::new(),            bookmarks_selected: 0,            history_selected: 0,            chat_session,            chat_input: String::new(),            chat_selected_model: 0,            chat_scroll: 0,            chat_focused: false,  // Start in content mode, press 'c' or '3' for chat            settings_selected: 0,            settings_model_input: String::new(),            settings_editing_model: false,            rag_client,            rag_results: Vec::new(),            rag_query: String::new(),            rag_scroll: 0,            rag_loading: false,            memory_client,            memory_nodes: Vec::new(),            page_rx,            page_tx,            summarizer: Summarizer::from_env().map(Arc::new),        };         // Load bookmarks and history        app.refresh_bookmarks();        app.refresh_history();         Ok(app)    }     // ═══════════════════════════════════════════════════════════════════════════    // CONVENIENCE ACCESSORS    // ═══════════════════════════════════════════════════════════════════════════     /// Get current page from active tab    pub fn current_page(&self) -> Option<&Page> {        self.tabs.active_tab().and_then(|t| t.page.as_ref())    }     /// Get current scroll offset from active tab    pub fn scroll_offset(&self) -> usize {        self.tabs.active_tab().map(|t| t.scroll_offset).unwrap_or(0)    }     /// Set scroll offset on active tab    fn set_scroll_offset(&mut self, offset: usize) {        if let Some(tab) = self.tabs.active_tab_mut() {            tab.scroll_offset = offset;        }    }     /// Get sidebar selected from active tab    pub fn sidebar_selected(&self) -> usize {        self.tabs.active_tab().map(|t| t.sidebar_selected).unwrap_or(0)    }     /// Set sidebar selected on active tab    fn set_sidebar_selected(&mut self, idx: usize) {        if let Some(tab) = self.tabs.active_tab_mut() {            tab.sidebar_selected = idx;        }    }     /// Check if current tab is loading    pub fn loading(&self) -> bool {        self.tabs.active_tab().map(|t| t.loading).unwrap_or(false)    }     /// Get current render mode from active tab    pub fn render_mode(&self) -> RenderMode {        self.tabs.active_tab().map(|t| t.render_mode).unwrap_or(RenderMode::Auto)    }     // ═══════════════════════════════════════════════════════════════════════════    // UPDATE LOOP    // ═══════════════════════════════════════════════════════════════════════════     pub fn update(&mut self) {        self.animation_tick = self.animation_tick.wrapping_add(1);        self.mascot.tick();         // Check for loaded pages        while let Ok(msg) = self.page_rx.try_recv() {            match msg {                AppMessage::PageLoaded(tab_id, page) => {                    // Find and update the tab                    for i in 0..self.tabs.count() {                        if let Some(tab) = self.tabs.get_tab_mut(i) {                            if tab.id == tab_id {                                // Add to history                                if let Some(db) = &self.db {                                    let _ = db.add_history(&page.url, &page.title);                                }                                tab.set_page(page);                                self.mascot.set_state(MascotState::Success);                                self.status_message = format!("Loaded: {}", tab.url);                                break;                            }                        }                    }                    self.ai_summary = None;                }                AppMessage::LoadError(tab_id, err) => {                    for i in 0..self.tabs.count() {                        if let Some(tab) = self.tabs.get_tab_mut(i) {                            if tab.id == tab_id {                                tab.set_error(err.clone());                                self.mascot.set_state(MascotState::Error);                                self.status_message = format!("Error: {}", err);                                break;                            }                        }                    }                }                AppMessage::AiSummary(summary) => {                    self.ai_summary = Some(summary);                    self.status_message = "AI summary ready".to_string();                    self.mascot.set_state(MascotState::Success);                }                AppMessage::AiError(err) => {                    self.status_message = format!("AI error: {}", err);                    self.mascot.set_state(MascotState::Error);                    // Also show error in chat so it's visible                    if let Some(session) = &mut self.chat_session {                        session.add_assistant_message(format!("❌ Error: {}", err));                        self.chat_scroll = 0;                    }                }                AppMessage::ChatResponse(response) => {                    if let Some(session) = &mut self.chat_session {                        session.add_assistant_message(response);                        self.chat_scroll = 0; // Auto-scroll to bottom (newest messages)                        self.mascot.set_state(MascotState::Success);                        self.status_message = "Response received".to_string();                    }                }                AppMessage::ToolAction(action) => {                    match action {                        ToolAction::Navigate(url) => {                            self.url_input = url;                            self.navigate();                            self.status_message = "AI navigated to URL".to_string();                        }                        ToolAction::FollowLink(idx) => {                            if let Some(page) = self.current_page() {                                if let Some(link) = page.links.get(idx) {                                    self.url_input = link.url.clone();                                    self.navigate();                                    self.status_message = format!("AI followed link {}", idx + 1);                                }                            }                        }                        ToolAction::Scroll { direction, amount } => {                            if let Some(tab) = self.tabs.active_tab_mut() {                                match direction.as_str() {                                    "up" => tab.scroll_offset = tab.scroll_offset.saturating_sub(amount),                                    "down" => tab.scroll_offset += amount,                                    "top" => tab.scroll_offset = 0,                                    "bottom" => tab.scroll_offset = usize::MAX / 2,                                    _ => {}                                }                                self.status_message = format!("AI scrolled {}", direction);                            }                        }                    }                }                AppMessage::RagResponse(results) => {                    self.rag_loading = false;                    self.rag_results = results;                    self.rag_scroll = 0;                    self.mascot.set_state(MascotState::Success);                    self.status_message = format!("RAG: {} results", self.rag_results.len());                }                AppMessage::RagError(err) => {                    self.rag_loading = false;                    self.rag_results = vec![format!("Error: {}", err)];                    self.mascot.set_state(MascotState::Error);                    self.status_message = format!("RAG error: {}", err);                }            }        }    }     pub fn quit(&mut self) {        self.should_quit = true;    }     pub fn focus_next(&mut self) {        self.focus = self.focus.next();        self.update_status();    }     pub fn focus_previous(&mut self) {        self.focus = self.focus.previous();        self.update_status();    }     // ═══════════════════════════════════════════════════════════════════════════    // KEY HANDLING    // ═══════════════════════════════════════════════════════════════════════════     pub fn handle_key(&mut self, key: KeyEvent) -> Result<()> {        // Overlay panel handling first (these take full focus)        match self.panel_mode {            PanelMode::Bookmarks => return self.handle_bookmarks_keys(key),            PanelMode::History => return self.handle_history_keys(key),            PanelMode::Help => {                // Any key closes help                self.panel_mode = PanelMode::None;                return Ok(());            }            PanelMode::Settings => return self.handle_settings_keys(key),            PanelMode::Rag => return self.handle_rag_keys(key),            PanelMode::Memory => return self.handle_memory_keys(key),            PanelMode::Chat | PanelMode::None => {}  // Chat is always visible, handled below        }         // Chat input handling when chat is focused        if self.chat_focused {            match (key.code, key.modifiers) {                // Esc unfocuses chat                (KeyCode::Esc, _) => {                    self.chat_focused = false;                    self.status_message = "Content mode - 'c' for chat, '/' for URL".to_string();                    return Ok(());                }                // These pass through to global shortcuts                (KeyCode::Tab, _) | (KeyCode::BackTab, _) => {}                // Handle chat input                _ => return self.handle_chat_keys(key),            }        }         // Global shortcuts        match (key.code, key.modifiers) {            (KeyCode::Char('q'), KeyModifiers::NONE) | (KeyCode::Char('Q'), KeyModifiers::NONE) => {                if self.focus != Focus::URLBar {                    self.quit();                    return Ok(());                }            }            (KeyCode::Char('c'), KeyModifiers::CONTROL) => {                self.quit();                return Ok(());            }            (KeyCode::Char('?'), KeyModifiers::NONE) => {                self.panel_mode = PanelMode::Help;                return Ok(());            }            (KeyCode::Tab, KeyModifiers::NONE) => {                self.focus_next();                return Ok(());            }            (KeyCode::BackTab, _) => {                self.focus_previous();                return Ok(());            }            (KeyCode::Char('/'), KeyModifiers::NONE) => {                if self.focus != Focus::URLBar {                    self.focus = Focus::URLBar;                    self.url_input.clear();                    self.update_status();                    return Ok(());                }            }            // Tab management            (KeyCode::Char('t'), KeyModifiers::NONE) if self.focus != Focus::URLBar => {                if self.tabs.new_tab(None).is_some() {                    self.status_message = format!("New tab {} created", self.tabs.count());                } else {                    self.status_message = "Max tabs reached (9)".to_string();                }                return Ok(());            }            (KeyCode::Char('w'), KeyModifiers::CONTROL) => {                if self.tabs.count() > 1 {                    self.tabs.close_current_tab();                    self.status_message = "Tab closed".to_string();                } else {                    self.status_message = "Can't close last tab".to_string();                }                return Ok(());            }            (KeyCode::Char(n), KeyModifiers::ALT) if n.is_ascii_digit() => {                let num = n.to_digit(10).unwrap_or(0) as usize;                if num > 0 {                    self.tabs.go_to_tab(num);                    self.status_message = format!("Switched to tab {}", num);                }                return Ok(());            }            (KeyCode::Char('['), KeyModifiers::CONTROL) => {                self.tabs.prev_tab();                self.status_message = format!("Tab {}/{}", self.tabs.active_index() + 1, self.tabs.count());                return Ok(());            }            (KeyCode::Char(']'), KeyModifiers::CONTROL) => {                self.tabs.next_tab();                self.status_message = format!("Tab {}/{}", self.tabs.active_index() + 1, self.tabs.count());                return Ok(());            }            // Bookmarks & History            (KeyCode::Char('b'), KeyModifiers::NONE) if self.focus != Focus::URLBar => {                self.panel_mode = PanelMode::Bookmarks;                self.refresh_bookmarks();                self.bookmarks_selected = 0;                return Ok(());            }            (KeyCode::Char('H'), KeyModifiers::NONE) => {                self.panel_mode = PanelMode::History;                self.refresh_history();                self.history_selected = 0;                return Ok(());            }            (KeyCode::Char('B'), KeyModifiers::NONE) => {                self.toggle_bookmark();

Lines 1–500 of 1828.