mod acp; mod app_error; mod commands; mod db; mod models; mod network; mod parsers; mod process; mod terminal; use std::sync::atomic::{AtomicBool, Ordering}; use acp::manager::ConnectionManager; use commands::{ acp as acp_commands, conversations, folder_commands, folders, mcp as mcp_commands, system_settings, terminal as terminal_commands, windows, }; use tauri::Manager; use terminal::manager::TerminalManager; static APP_QUITTING: AtomicBool = AtomicBool::new(false); fn get_folder_id_from_url(window: &tauri::Window) -> Option { let webview = window.get_webview_window(window.label())?; let url = webview.url().ok()?; url.query_pairs() .find(|(key, _)| key == "id") .and_then(|(_, value)| value.parse::().ok()) } #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { let _ = fix_path_env::fix(); tauri::Builder::default() .plugin(tauri_plugin_window_state::Builder::new().build()) .plugin(tauri_plugin_opener::init()) .plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_updater::Builder::new().build()) .plugin(tauri_plugin_process::init()) .manage(ConnectionManager::new()) .manage(TerminalManager::new()) .manage(windows::SettingsWindowState::new()) .manage(windows::CommitWindowState::new()) .setup(|app| { let app_data_dir = app.path().app_data_dir()?; let app_version = env!("CARGO_PKG_VERSION"); let database = tauri::async_runtime::block_on(db::init_database(&app_data_dir, app_version)) .map_err(|e| e.to_string())?; app.manage(database); // Restore and apply saved system proxy settings before any network operation. let db = app.state::(); match tauri::async_runtime::block_on(system_settings::load_system_proxy_settings( &db.conn, )) { Ok(settings) => { let _ = network::proxy::apply_system_proxy_settings(&settings); } Err(err) => { eprintln!("[Settings] failed to load system proxy settings: {err}"); } } // Restore previously open folders or show welcome let db = app.state::(); let open_folders = tauri::async_runtime::block_on( db::service::folder_service::list_open_folders(&db.conn), ) .unwrap_or_default(); if open_folders.is_empty() { let _ = windows::open_welcome_window(app.handle()); } else { for entry in &open_folders { let label = format!("folder-{}", uuid::Uuid::new_v4()); let url = tauri::WebviewUrl::App(format!("folder?id={}", entry.id).into()); let builder = tauri::WebviewWindowBuilder::new(app, &label, url) .title(&entry.name) .inner_size(1260.0, 860.0) .min_inner_size(900.0, 600.0); let _ = windows::apply_platform_window_style(builder).build(); } } Ok(()) }) .on_window_event(|window, event| { let label = window.label().to_string(); if label == "settings" && matches!( event, tauri::WindowEvent::CloseRequested { .. } | tauri::WindowEvent::Destroyed ) { let app = window.app_handle(); if let Some(state) = app.try_state::() { windows::restore_windows_after_settings(&app, &state); } } if label.starts_with("commit-") && matches!( event, tauri::WindowEvent::CloseRequested { .. } | tauri::WindowEvent::Destroyed ) { let app = window.app_handle(); if let Some(state) = app.try_state::() { windows::restore_window_after_commit(&app, &state, &label); } } if let tauri::WindowEvent::CloseRequested { .. } = event { if label.starts_with("folder-") { let app = window.app_handle(); if let Some(cm) = app.try_state::() { let disconnected = tauri::async_runtime::block_on(cm.disconnect_by_owner_window(&label)); eprintln!( "[ACP] folder window closing label={} disconnected_connections={}", label, disconnected ); } // Only mark folder as closed if user is closing individual window, // not when the entire app is quitting (so folders reopen on next launch) if !APP_QUITTING.load(Ordering::Relaxed) { if let Some(folder_id) = get_folder_id_from_url(window) { if let Some(db) = app.try_state::() { let _ = tauri::async_runtime::block_on( db::service::folder_service::set_folder_open( &db.conn, folder_id, false, ), ); } } } // Kill terminal sessions owned by this folder window. if let Some(tm) = app.try_state::() { let killed = tm.kill_by_owner_window(&label); eprintln!( "[TERM] folder window closing label={} killed_terminals={}", label, killed ); } let has_other_folder = app .webview_windows() .keys() .any(|l| l.starts_with("folder-") && *l != label); if !has_other_folder && !APP_QUITTING.load(Ordering::Relaxed) { let _ = windows::open_welcome_window(app); } } } }) .invoke_handler(tauri::generate_handler![ conversations::list_conversations, conversations::get_conversation, conversations::list_folder_conversations, conversations::import_local_conversations, conversations::get_folder_conversation, conversations::list_folders, conversations::get_stats, conversations::get_sidebar_data, conversations::create_conversation, conversations::update_conversation_status, conversations::update_conversation_title, conversations::update_conversation_external_id, conversations::delete_conversation, folders::load_folder_history, folders::get_folder, folders::add_folder_to_history, folders::set_folder_parent_branch, folders::remove_folder_from_history, folders::create_folder_directory, folders::clone_repository, folders::get_git_branch, folders::git_init, folders::git_pull, folders::git_fetch, folders::git_push, folders::git_new_branch, folders::git_worktree_add, folders::git_checkout, folders::git_list_branches, folders::git_stash, folders::git_stash_pop, folders::git_status, folders::git_is_tracked, folders::git_diff, folders::git_diff_with_branch, folders::git_show_diff, folders::git_show_file, folders::git_commit, folders::git_rollback_file, folders::git_add_files, folders::git_list_all_branches, folders::git_merge, folders::git_rebase, folders::git_delete_branch, folders::save_folder_opened_conversations, folders::start_file_tree_watch, folders::stop_file_tree_watch, folders::get_file_tree, folders::read_file_preview, folders::read_file_for_edit, folders::save_file_content, folders::save_file_copy, folders::rename_file_tree_entry, folders::delete_file_tree_entry, folders::git_log, folders::git_commit_branches, windows::open_folder_window, windows::open_commit_window, windows::open_settings_window, windows::list_open_folders, windows::focus_folder_window, system_settings::get_system_proxy_settings, system_settings::update_system_proxy_settings, system_settings::get_system_language_settings, system_settings::update_system_language_settings, acp_commands::acp_preflight, acp_commands::acp_connect, acp_commands::acp_prompt, acp_commands::acp_set_mode, acp_commands::acp_set_config_option, acp_commands::acp_cancel, acp_commands::acp_respond_permission, acp_commands::acp_disconnect, acp_commands::acp_list_connections, acp_commands::acp_list_agents, acp_commands::acp_clear_binary_cache, acp_commands::acp_download_agent_binary, acp_commands::acp_detect_agent_local_version, acp_commands::acp_prepare_npx_agent, acp_commands::acp_prepare_uvx_agent, acp_commands::acp_uninstall_agent, acp_commands::acp_update_agent_preferences, acp_commands::acp_reorder_agents, acp_commands::acp_list_agent_skills, acp_commands::acp_read_agent_skill, acp_commands::acp_save_agent_skill, acp_commands::acp_delete_agent_skill, folder_commands::list_folder_commands, folder_commands::create_folder_command, folder_commands::update_folder_command, folder_commands::delete_folder_command, folder_commands::reorder_folder_commands, folder_commands::bootstrap_folder_commands_from_package_json, terminal_commands::terminal_spawn, terminal_commands::terminal_write, terminal_commands::terminal_resize, terminal_commands::terminal_kill, terminal_commands::terminal_list, mcp_commands::mcp_scan_local, mcp_commands::mcp_list_marketplaces, mcp_commands::mcp_search_marketplace, mcp_commands::mcp_get_marketplace_server_detail, mcp_commands::mcp_install_from_marketplace, mcp_commands::mcp_upsert_local_server, mcp_commands::mcp_set_server_apps, mcp_commands::mcp_remove_server, ]) .build(tauri::generate_context!()) .expect("error while building tauri application") .run(|_app, event| { if let tauri::RunEvent::ExitRequested { .. } = event { APP_QUITTING.store(true, Ordering::Relaxed); } }); }