pub mod ai_agents; pub mod ai_models; mod app_icon; pub mod app_updater; pub mod claude_cli; mod claude_invocation; mod cli_agent_runtime; pub mod codex_cli; mod commands; pub mod frontmatter; pub mod gemini_cli; mod gemini_config; mod gemini_discovery; pub mod git; pub mod kiro_cli; mod kiro_discovery; #[cfg(any(test, all(desktop, target_os = "linux")))] mod linux_appimage; pub mod mcp; #[cfg(desktop)] pub mod menu; pub mod opencode_cli; mod opencode_config; mod opencode_discovery; mod opencode_events; pub mod pi_cli; mod pi_config; mod pi_discovery; mod pi_events; pub mod search; pub mod settings; pub mod telemetry; pub mod vault; pub mod vault_list; pub mod vault_watcher; #[cfg(desktop)] mod window_state; use std::ffi::OsStr; use std::process::Command; #[cfg(desktop)] use std::path::{Path, PathBuf}; #[cfg(desktop)] use std::process::Child; #[cfg(desktop)] use std::sync::Mutex; #[cfg(windows)] const CREATE_NO_WINDOW: u32 = 0x08000000; pub(crate) fn hidden_command(program: impl AsRef) -> Command { let mut command = Command::new(program); suppress_windows_console(&mut command); command } #[cfg(windows)] fn suppress_windows_console(command: &mut Command) { use std::os::windows::process::CommandExt; command.creation_flags(CREATE_NO_WINDOW); } #[cfg(not(windows))] fn suppress_windows_console(_command: &mut Command) {} #[cfg(desktop)] struct WsBridgeChild(Mutex>); #[cfg(desktop)] struct AllowedAssetScopeRoots(Mutex>); #[cfg(desktop)] fn log_startup_result(label: &str, result: Result) { match result { Ok(n) if n > 0 => log::info!("{}: {} files", label, n), Err(e) => log::warn!("{}: {}", label, e), _ => {} } } #[cfg(desktop)] fn selected_mcp_bridge_vault_paths(vault_list: &vault_list::VaultList) -> Vec { let mut paths = Vec::new(); if let Some(active_vault) = vault_list .active_vault .as_deref() .map(str::trim) .filter(|path| !path.is_empty()) { push_unique_mcp_bridge_vault_path(&mut paths, active_vault); } for vault in &vault_list.vaults { if vault.mounted == Some(false) { continue; } push_unique_mcp_bridge_vault_path(&mut paths, &vault.path); } paths } #[cfg(desktop)] fn push_unique_mcp_bridge_vault_path(paths: &mut Vec, path: &str) { let trimmed = path.trim(); if trimmed.is_empty() { return; } let path = PathBuf::from(trimmed); if paths.iter().any(|existing| existing == &path) { return; } paths.push(path); } #[cfg(desktop)] fn validate_mcp_bridge_vault_path(vault_path: &Path) -> Result { let resolved = std::fs::canonicalize(vault_path).map_err(|e| { format!( "MCP bridge vault is not available: {} ({e})", vault_path.display() ) })?; if !resolved.is_dir() { return Err(format!( "MCP bridge vault is not available: {} is not a directory", vault_path.display() )); } Ok(resolved) } #[cfg(desktop)] fn stop_ws_bridge_child(active_child: &mut Option) { if let Some(mut child) = active_child.take() { let _ = child.kill(); let _ = child.wait(); log::info!("ws-bridge child process stopped"); } } #[cfg(desktop)] pub(crate) fn sync_ws_bridge_for_vault( app_handle: &tauri::AppHandle, vault_path: Option<&Path>, active_vault_paths: &[PathBuf], ) -> Result<&'static str, String> { use tauri::Manager; let state: tauri::State<'_, WsBridgeChild> = app_handle.state(); let mut active_child = state .0 .lock() .map_err(|_| "Failed to lock ws-bridge state".to_string())?; let Some(vault_path) = vault_path else { stop_ws_bridge_child(&mut active_child); return Ok("stopped"); }; let resolved_vault_path = match validate_mcp_bridge_vault_path(vault_path) { Ok(path) => path, Err(e) => { stop_ws_bridge_child(&mut active_child); return Err(e); } }; stop_ws_bridge_child(&mut active_child); let resolved_active_vault_paths = active_vault_paths .iter() .filter_map(|path| validate_mcp_bridge_vault_path(path).ok()) .collect::>(); let child = mcp::spawn_ws_bridge_with_paths(&resolved_vault_path, &resolved_active_vault_paths)?; *active_child = Some(child); Ok("started") } fn spawn_background_task(thread_name: &'static str, task: F) where F: FnOnce() + Send + 'static, { if let Err(e) = std::thread::Builder::new() .name(thread_name.into()) .spawn(task) { log::warn!("Failed to start {thread_name}: {e}"); } } /// Run startup housekeeping on the legacy default vault (migrate legacy frontmatter, seed configs). #[cfg(desktop)] fn run_startup_tasks_for_vault(vault_path: &Path) { let vp_str = vault_path.to_str().unwrap_or_default(); log_startup_result( "Migrated is_a to type on startup", vault::migrate_is_a_to_type(vp_str), ); // Migrate legacy config/agents.md -> root AGENTS.md (one-time, idempotent) vault::migrate_agents_md(vp_str); // Seed AGENTS.md and starter type definitions at vault root if missing vault::seed_config_files(vp_str); } #[cfg(desktop)] fn spawn_startup_tasks_for_vault_with(vault_path: PathBuf, task: F) -> bool where F: FnOnce(PathBuf) + Send + 'static, { if !vault_path.is_dir() { return false; } spawn_background_task("tolaria-startup-tasks", move || task(vault_path)); true } #[cfg(desktop)] fn spawn_startup_tasks() { let Some(vault_path) = dirs::home_dir().map(|h| h.join("Laputa")) else { return; }; spawn_startup_tasks_for_vault_with(vault_path, |path| run_startup_tasks_for_vault(&path)); } #[cfg(desktop)] fn sync_ws_bridge_for_selected_vault(app_handle: &tauri::AppHandle) { let vault_paths = match vault_list::load_vault_list() { Ok(vault_list) => selected_mcp_bridge_vault_paths(&vault_list), Err(e) => { log::warn!("Failed to load active vault for ws-bridge startup: {}", e); Vec::new() } }; let Some(vault_path) = vault_paths.first() else { log::info!("ws-bridge not started: no active vault selected"); return; }; if let Err(e) = sync_ws_bridge_for_vault(app_handle, Some(vault_path), &vault_paths) { log::warn!("Failed to start ws-bridge: {}", e); } } #[cfg(desktop)] fn spawn_initial_ws_bridge_sync(app: &tauri::App) { let app_handle = app.handle().clone(); spawn_background_task("tolaria-ws-bridge-startup", move || { #[cfg(all(desktop, target_os = "linux"))] if linux_appimage::is_running() { let app_version = app_handle.package_info().version.to_string(); if let Err(e) = mcp::extract_mcp_server_to_stable_dir(&app_version) { log::warn!("Failed to extract MCP server to stable path: {e}"); } } sync_ws_bridge_for_selected_vault(&app_handle); }); } fn setup_common_plugins(app: &mut tauri::App) -> Result<(), Box> { if cfg!(debug_assertions) { app.handle().plugin( tauri_plugin_log::Builder::default() .level(log::LevelFilter::Info) .build(), )?; } app.handle().plugin(tauri_plugin_dialog::init())?; Ok(()) } #[cfg(desktop)] fn focus_main_window(app_handle: &tauri::AppHandle) { use tauri::Manager; if let Some(window) = app_handle.get_webview_window("main") { let _ = window.unminimize(); let _ = window.show(); let _ = window.set_focus(); } } #[cfg(desktop)] fn with_desktop_entry_plugins(builder: tauri::Builder) -> tauri::Builder { builder .plugin(tauri_plugin_single_instance::init(|app, _argv, _cwd| { focus_main_window(app); })) .plugin(tauri_plugin_deep_link::init()) } #[cfg(desktop)] fn setup_deep_link_runtime_registration( _app: &mut tauri::App, ) -> Result<(), Box> { #[cfg(any(target_os = "windows", target_os = "linux"))] { use tauri_plugin_deep_link::DeepLinkExt; _app.deep_link().register_all()?; } Ok(()) } #[cfg(desktop)] fn setup_desktop_plugins(app: &mut tauri::App) -> Result<(), Box> { setup_macos_webview_shortcut_prevention(app)?; setup_deep_link_runtime_registration(app)?; app.handle() .plugin(tauri_plugin_updater::Builder::new().build())?; app.handle().plugin(tauri_plugin_process::init())?; app.handle().plugin(tauri_plugin_opener::init())?; if should_use_native_desktop_menu(std::env::consts::OS) { menu::setup_menu(app)?; } setup_custom_window_chrome(app)?; window_state::restore_main_window_state(app); show_debug_main_window(app); Ok(()) } #[cfg(debug_assertions)] fn show_debug_main_window(app: &mut tauri::App) { use tauri::Manager; if let Some(window) = app.get_webview_window("main") { let _ = window.unminimize(); let _ = window.show(); let _ = window.center(); let _ = window.set_focus(); } } #[cfg(not(debug_assertions))] fn show_debug_main_window(_app: &mut tauri::App) {} fn should_use_native_desktop_menu(target_os: &str) -> bool { target_os == "macos" } #[cfg(all(desktop, any(target_os = "linux", target_os = "windows")))] fn setup_custom_window_chrome(app: &mut tauri::App) -> Result<(), Box> { use tauri::Manager; if let Some(window) = app.get_webview_window("main") { let _ = window.set_decorations(false); } Ok(()) } #[cfg(not(all(desktop, any(target_os = "linux", target_os = "windows"))))] fn setup_custom_window_chrome(_app: &mut tauri::App) -> Result<(), Box> { Ok(()) } #[cfg(any(test, all(desktop, target_os = "macos")))] const MACOS_WEBVIEW_RESERVED_COMMAND_KEYS: &[&str] = &["O", "F"]; #[cfg(any(test, all(desktop, target_os = "macos")))] const MACOS_WEBVIEW_RESERVED_COMMAND_SHIFT_KEYS: &[&str] = &["L"]; #[cfg(all(desktop, target_os = "macos"))] fn setup_macos_webview_shortcut_prevention( app: &mut tauri::App, ) -> Result<(), Box> { use tauri_plugin_prevent_default::ModifierKey::{MetaKey, ShiftKey}; use tauri_plugin_prevent_default::{Flags, KeyboardShortcut}; let mut builder = tauri_plugin_prevent_default::Builder::new().with_flags(Flags::empty()); // WKWebView can swallow some browser-reserved chords before our shared // renderer shortcut handler sees them. Keep this list narrow and verify // every addition with native QA. for key in MACOS_WEBVIEW_RESERVED_COMMAND_KEYS { builder = builder.shortcut(KeyboardShortcut::with_modifiers(key, &[MetaKey])); } for key in MACOS_WEBVIEW_RESERVED_COMMAND_SHIFT_KEYS { builder = builder.shortcut(KeyboardShortcut::with_modifiers(key, &[MetaKey, ShiftKey])); } app.handle().plugin(builder.build())?; Ok(()) } #[cfg(not(all(desktop, target_os = "macos")))] fn setup_macos_webview_shortcut_prevention( _app: &mut tauri::App, ) -> Result<(), Box> { Ok(()) } fn setup_app(app: &mut tauri::App) -> Result<(), Box> { setup_common_plugins(app)?; #[cfg(desktop)] setup_desktop_plugins(app)?; if telemetry::init_sentry_from_settings() { log::info!("Sentry initialized (crash reporting enabled)"); } #[cfg(desktop)] { spawn_startup_tasks(); spawn_initial_ws_bridge_sync(app); } Ok(()) } #[cfg(desktop)] fn vault_asset_scope_roots(vault_path: &Path) -> Result, String> { let canonical_vault_path = std::fs::canonicalize(vault_path).map_err(|e| { format!( "Failed to resolve asset scope for {}: {e}", vault_path.display() ) })?; let mut roots = vec![canonical_vault_path.clone()]; let requested_vault_path = vault_path.to_path_buf(); if requested_vault_path != canonical_vault_path { roots.push(requested_vault_path); } Ok(roots) } #[cfg(desktop)] fn missing_asset_scope_roots( allowed_roots: &[PathBuf], requested_roots: &[PathBuf], ) -> Vec { requested_roots .iter() .filter(|root| !allowed_roots.contains(root)) .cloned() .collect() } #[cfg(desktop)] pub(crate) fn sync_vault_asset_scope( app_handle: &tauri::AppHandle, vault_path: &Path, ) -> Result<(), String> { use tauri::Manager; let requested_roots = vault_asset_scope_roots(vault_path)?; let scope = app_handle.asset_protocol_scope(); let state: tauri::State<'_, AllowedAssetScopeRoots> = app_handle.state(); let mut allowed_roots = state .0 .lock() .map_err(|_| "Failed to lock asset scope state".to_string())?; let roots_to_allow = missing_asset_scope_roots(&allowed_roots, &requested_roots); for root in &roots_to_allow { scope .allow_directory(root, true) .map_err(|e| format!("Failed to allow asset access for {}: {e}", root.display()))?; } allowed_roots.extend(roots_to_allow); Ok(()) } macro_rules! app_invoke_handler { () => { tauri::generate_handler![ commands::list_vault, commands::list_vault_folders, commands::get_note_content, commands::validate_note_content, commands::create_note_content, commands::save_note_content, commands::update_frontmatter, commands::delete_frontmatter_property, commands::rename_note, commands::rename_note_filename, commands::move_note_to_folder, commands::move_note_to_workspace, commands::auto_rename_untitled, commands::detect_renames, commands::update_wikilinks_for_renames, commands::get_file_history, commands::get_modified_files, commands::get_file_diff, commands::get_file_diff_at_commit, commands::get_vault_pulse, commands::git_commit, commands::get_build_number, commands::get_last_commit_info, commands::git_pull, commands::git_push, commands::git_remote_status, commands::git_add_remote, commands::get_conflict_files, commands::get_conflict_mode, commands::git_resolve_conflict, commands::git_commit_conflict_resolution, commands::git_discard_file, commands::is_git_repo, commands::init_git_repo, commands::check_claude_cli, commands::get_ai_agents_status, commands::get_agent_docs_path, commands::get_vault_ai_guidance_status, commands::restore_vault_ai_guidance, commands::stream_claude_chat, commands::stream_ai_agent, commands::stream_ai_model, commands::save_ai_model_provider_api_key, commands::delete_ai_model_provider_api_key, commands::test_ai_model_provider, commands::reload_vault, commands::reload_vault_entry, commands::sync_vault_asset_scope_for_window, commands::open_vault_file_external, commands::sync_note_title, commands::save_image, commands::copy_image_to_vault, commands::delete_note, commands::batch_delete_notes, commands::batch_delete_notes_async, commands::migrate_is_a_to_type, commands::create_vault_folder, commands::rename_vault_folder, commands::delete_vault_folder, commands::batch_archive_notes, commands::get_settings, commands::check_for_app_update, commands::update_menu_state, commands::update_app_icon, commands::trigger_menu_command, commands::update_current_window_min_size, commands::perform_current_window_titlebar_double_click, commands::save_settings, commands::download_and_install_app_update, commands::load_vault_list, commands::save_vault_list, commands::git_clone::clone_git_repo, commands::search_vault, commands::create_empty_vault, commands::create_getting_started_vault, commands::check_vault_exists, commands::get_default_vault_path, commands::register_mcp_tools, commands::remove_mcp_tools, commands::check_mcp_status, commands::get_mcp_config_snippet, commands::copy_text_to_clipboard, commands::read_text_from_clipboard, commands::sync_mcp_bridge_vault, commands::get_process_memory_snapshot, commands::repair_vault, commands::reinit_telemetry, commands::should_use_external_media_preview, commands::list_views, commands::save_view_cmd, commands::delete_view_cmd, vault_watcher::start_vault_watcher, vault_watcher::stop_vault_watcher ] }; } fn with_invoke_handler(builder: tauri::Builder) -> tauri::Builder { builder.invoke_handler(app_invoke_handler!()) } #[cfg(desktop)] fn handle_run_event(app_handle: &tauri::AppHandle, event: &tauri::RunEvent) { use tauri::Manager; window_state::handle_run_event(app_handle, event); if let tauri::RunEvent::Exit = event { let state: tauri::State<'_, WsBridgeChild> = app_handle.state(); let mut guard = state.0.lock().unwrap(); stop_ws_bridge_child(&mut guard); } } #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { #[cfg(all(desktop, target_os = "linux"))] linux_appimage::apply_startup_env_overrides(); let builder = tauri::Builder::default(); #[cfg(desktop)] let builder = with_desktop_entry_plugins(builder); #[cfg(desktop)] let builder = builder .manage(WsBridgeChild(Mutex::new(None))) .manage(AllowedAssetScopeRoots(Mutex::new(Vec::new()))) .manage(window_state::MainWindowFrameState::default()) .manage(vault_watcher::VaultWatcherState::new()); with_invoke_handler(builder) .setup(setup_app) .build(tauri::generate_context!()) .expect("error while building tauri application") .run(|app_handle, event| { #[cfg(desktop)] handle_run_event(app_handle, &event); }); } #[cfg(test)] mod tests { use super::should_use_native_desktop_menu; use super::MACOS_WEBVIEW_RESERVED_COMMAND_KEYS; use super::MACOS_WEBVIEW_RESERVED_COMMAND_SHIFT_KEYS; #[cfg(desktop)] use super::{ missing_asset_scope_roots, selected_mcp_bridge_vault_paths, spawn_startup_tasks_for_vault_with, validate_mcp_bridge_vault_path, }; #[cfg(desktop)] use crate::vault_list::{VaultEntry, VaultList}; #[cfg(desktop)] use std::path::PathBuf; #[cfg(all(desktop, unix))] use super::vault_asset_scope_roots; #[test] fn macos_webview_shortcut_prevention_includes_ai_panel_shortcut() { assert_eq!(MACOS_WEBVIEW_RESERVED_COMMAND_KEYS, ["O", "F"]); assert_eq!(MACOS_WEBVIEW_RESERVED_COMMAND_SHIFT_KEYS, ["L"]); } #[cfg(desktop)] #[test] fn selected_mcp_bridge_vault_paths_puts_persisted_active_vault_first() { let list = VaultList { vaults: vec![ VaultEntry { label: "Secondary".to_string(), path: "/tmp/Secondary Vault".to_string(), mounted: Some(true), ..VaultEntry::default() }, VaultEntry { label: "Hidden".to_string(), path: "/tmp/Hidden Vault".to_string(), mounted: Some(false), ..VaultEntry::default() }, VaultEntry { label: "Selected".to_string(), path: "/tmp/Selected Vault".to_string(), mounted: Some(true), ..VaultEntry::default() }, ], active_vault: Some("/tmp/Selected Vault".to_string()), default_workspace_path: None, hidden_defaults: Vec::new(), }; assert_eq!( selected_mcp_bridge_vault_paths(&list), vec![ PathBuf::from("/tmp/Selected Vault"), PathBuf::from("/tmp/Secondary Vault"), ] ); } #[cfg(desktop)] #[test] fn selected_mcp_bridge_vault_paths_ignores_blank_active_vault() { let list = VaultList { vaults: Vec::new(), active_vault: Some(" ".to_string()), default_workspace_path: None, hidden_defaults: Vec::new(), }; assert!(selected_mcp_bridge_vault_paths(&list).is_empty()); } #[cfg(desktop)] #[test] fn startup_tasks_skip_missing_legacy_vault() { let missing_vault = tempfile::tempdir().unwrap().path().join("missing"); let called = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)); let called_from_task = called.clone(); let spawned = spawn_startup_tasks_for_vault_with(missing_vault, move |_| { called_from_task.store(true, std::sync::atomic::Ordering::SeqCst); }); assert!(!spawned); assert!(!called.load(std::sync::atomic::Ordering::SeqCst)); } #[cfg(desktop)] #[test] fn startup_tasks_run_in_background() { let dir = tempfile::tempdir().unwrap(); let (entered_tx, entered_rx) = std::sync::mpsc::channel(); let (release_tx, release_rx) = std::sync::mpsc::channel(); let spawned = spawn_startup_tasks_for_vault_with(dir.path().to_path_buf(), move |_| { entered_tx.send(()).unwrap(); release_rx .recv_timeout(std::time::Duration::from_secs(1)) .unwrap(); }); assert!(spawned); entered_rx .recv_timeout(std::time::Duration::from_secs(1)) .unwrap(); release_tx.send(()).unwrap(); } #[cfg(desktop)] #[test] fn validate_mcp_bridge_vault_path_requires_existing_directory() { let dir = tempfile::tempdir().unwrap(); let vault = dir.path().join("Vault With Spaces"); std::fs::create_dir(&vault).unwrap(); let resolved = validate_mcp_bridge_vault_path(&vault).unwrap(); assert_eq!(resolved, vault.canonicalize().unwrap()); let missing = dir.path().join("Missing Vault"); let err = validate_mcp_bridge_vault_path(&missing).unwrap_err(); assert!(err.contains("MCP bridge vault is not available")); } #[cfg(all(desktop, unix))] #[test] fn vault_asset_scope_roots_include_requested_symlink_path() { let dir = tempfile::tempdir().unwrap(); let canonical_vault = dir.path().join("Getting Started"); let symlinked_vault = dir.path().join("Symlinked Getting Started"); std::fs::create_dir(&canonical_vault).unwrap(); std::os::unix::fs::symlink(&canonical_vault, &symlinked_vault).unwrap(); let roots = vault_asset_scope_roots(&symlinked_vault).unwrap(); assert_eq!(roots[0], canonical_vault.canonicalize().unwrap()); assert!(roots.contains(&symlinked_vault)); } #[cfg(desktop)] #[test] fn missing_asset_scope_roots_keeps_previously_allowed_vaults() { let vault_a = PathBuf::from("/vault-a"); let vault_b = PathBuf::from("/vault-b"); let allowed_roots = vec![vault_a.clone()]; assert_eq!( missing_asset_scope_roots(&allowed_roots, std::slice::from_ref(&vault_b)), vec![vault_b] ); assert!( missing_asset_scope_roots(&allowed_roots, std::slice::from_ref(&vault_a)).is_empty() ); } #[test] fn native_desktop_menu_is_macos_only() { assert!(should_use_native_desktop_menu("macos")); assert!(!should_use_native_desktop_menu("windows")); assert!(!should_use_native_desktop_menu("linux")); } }