Files
codeg/src-tauri/src/lib.rs
2026-03-25 14:26:26 +08:00

352 lines
15 KiB
Rust

mod acp;
mod app_error;
mod commands;
mod db;
pub mod git_credential;
pub mod keyring_store;
mod models;
mod network;
mod parsers;
mod process;
mod terminal;
mod web;
use std::sync::atomic::{AtomicBool, Ordering};
use acp::manager::ConnectionManager;
use commands::{
acp as acp_commands, conversations, folder_commands, folders, mcp as mcp_commands,
notification, system_settings, terminal as terminal_commands, version_control, 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<i32> {
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::<i32>().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())
.plugin(tauri_plugin_notification::init())
.manage(ConnectionManager::new())
.manage(TerminalManager::new())
.manage(windows::SettingsWindowState::new())
.manage(windows::CommitWindowState::new())
.manage(windows::MergeWindowState::new())
.manage(web::WebServerState::new())
.manage(web::event_bridge::WebEventBroadcaster::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::<db::AppDatabase>();
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::<db::AppDatabase>();
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 = windows::folder_window_label(entry.id);
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::SettingsWindowState>() {
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::CommitWindowState>() {
windows::restore_window_after_commit(app, &state, &label);
}
}
if label.starts_with("merge-")
&& matches!(
event,
tauri::WindowEvent::CloseRequested { .. } | tauri::WindowEvent::Destroyed
)
{
let app = window.app_handle();
if let Some(state) = app.try_state::<windows::MergeWindowState>() {
windows::restore_window_after_merge(app, &state, &label);
}
// Clean up dangling merge state (MERGE_HEAD) if window closed
// without completing or aborting the merge
let app_clone = window.app_handle().clone();
let label_clone = label.clone();
tauri::async_runtime::spawn(async move {
windows::cleanup_dangling_merge(&app_clone, &label_clone).await;
});
}
if let tauri::WindowEvent::CloseRequested { .. } = event {
if label.starts_with("folder-") {
let app = window.app_handle();
if let Some(cm) = app.try_state::<ConnectionManager>() {
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::<db::AppDatabase>() {
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::<TerminalManager>() {
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_start_pull_merge,
folders::git_has_merge_head,
folders::git_fetch,
folders::git_push_info,
folders::git_push,
folders::git_new_branch,
folders::git_worktree_add,
folders::git_checkout,
folders::git_list_branches,
folders::git_stash_push,
folders::git_stash_pop,
folders::git_stash_list,
folders::git_stash_apply,
folders::git_stash_drop,
folders::git_stash_clear,
folders::git_stash_show,
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_list_remotes,
folders::git_fetch_remote,
folders::git_add_remote,
folders::git_remove_remote,
folders::git_set_remote_url,
folders::git_merge,
folders::git_rebase,
folders::git_delete_branch,
folders::git_list_conflicts,
folders::git_conflict_file_versions,
folders::git_resolve_conflict,
folders::git_abort_operation,
folders::git_continue_operation,
folders::save_folder_opened_conversations,
folders::start_file_tree_watch,
folders::stop_file_tree_watch,
folders::get_file_tree,
folders::read_file_base64,
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::create_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,
windows::open_merge_window,
windows::open_stash_window,
windows::open_push_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,
version_control::detect_git,
version_control::test_git_path,
version_control::get_git_settings,
version_control::update_git_settings,
version_control::get_github_accounts,
version_control::validate_github_token,
version_control::update_github_accounts,
version_control::save_account_token,
version_control::get_account_token,
version_control::delete_account_token,
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_fork,
acp_commands::acp_respond_permission,
acp_commands::acp_disconnect,
acp_commands::acp_list_connections,
acp_commands::acp_list_agents,
acp_commands::acp_get_agent_status,
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_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,
notification::send_notification,
web::start_web_server,
web::stop_web_server,
web::get_web_server_status,
])
.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);
// Stop the embedded web server if running.
if let Some(ws) = app.try_state::<web::WebServerState>() {
let _ = tauri::async_runtime::block_on(web::stop_web_server(ws));
}
// Kill all terminal sessions to prevent orphaned processes.
if let Some(tm) = app.try_state::<TerminalManager>() {
tm.kill_all();
}
// Disconnect all ACP agent connections (kills agent process trees).
if let Some(cm) = app.try_state::<ConnectionManager>() {
tauri::async_runtime::block_on(cm.disconnect_all());
}
}
});
}