diff --git a/src-tauri/src/commands/acp.rs b/src-tauri/src/commands/acp.rs index 36067e4..7e25343 100644 --- a/src-tauri/src/commands/acp.rs +++ b/src-tauri/src/commands/acp.rs @@ -1339,9 +1339,8 @@ pub async fn acp_clear_binary_cache(agent_type: AgentType) -> Result<(), AcpErro Ok(()) } -#[tauri::command] #[allow(clippy::too_many_arguments)] -pub async fn acp_update_agent_preferences( +pub(crate) async fn acp_update_agent_preferences_core( agent_type: AgentType, enabled: bool, env: BTreeMap, @@ -1349,8 +1348,8 @@ pub async fn acp_update_agent_preferences( opencode_auth_json: Option, codex_auth_json: Option, codex_config_toml: Option, - db: State<'_, AppDatabase>, - app: tauri::AppHandle, + db: &AppDatabase, + app: &tauri::AppHandle, ) -> Result<(), AcpError> { let default = agent_setting_service::AgentDefaultInput { agent_type, @@ -1405,7 +1404,7 @@ pub async fn acp_update_agent_preferences( codex_config_toml.as_deref(), )?; } - emit_acp_agents_updated(&app, "preferences_updated", Some(agent_type)); + emit_acp_agents_updated(app, "preferences_updated", Some(agent_type)); return Ok(()); } @@ -1416,7 +1415,7 @@ pub async fn acp_update_agent_preferences( if let Some(raw) = config_json.as_deref() { persist_agent_local_config_json(agent_type, Some(raw))?; } - emit_acp_agents_updated(&app, "preferences_updated", Some(agent_type)); + emit_acp_agents_updated(app, "preferences_updated", Some(agent_type)); return Ok(()); } @@ -1435,14 +1434,32 @@ pub async fn acp_update_agent_preferences( let local_patch_json = serde_json::to_string(&local_patch_value) .map_err(|e| AcpError::protocol(format!("serialize local patch failed: {e}")))?; persist_agent_local_config_json(agent_type, Some(local_patch_json.as_str()))?; - emit_acp_agents_updated(&app, "preferences_updated", Some(agent_type)); + emit_acp_agents_updated(app, "preferences_updated", Some(agent_type)); Ok(()) } #[tauri::command] -pub async fn acp_download_agent_binary( +#[allow(clippy::too_many_arguments)] +pub async fn acp_update_agent_preferences( agent_type: AgentType, + enabled: bool, + env: BTreeMap, + config_json: Option, + opencode_auth_json: Option, + codex_auth_json: Option, + codex_config_toml: Option, + db: State<'_, AppDatabase>, app: tauri::AppHandle, +) -> Result<(), AcpError> { + acp_update_agent_preferences_core( + agent_type, enabled, env, config_json, opencode_auth_json, + codex_auth_json, codex_config_toml, &db, &app, + ).await +} + +pub(crate) async fn acp_download_agent_binary_core( + agent_type: AgentType, + app: &tauri::AppHandle, ) -> Result<(), AcpError> { let meta = registry::get_agent_meta(agent_type); match meta.distribution { @@ -1465,7 +1482,7 @@ pub async fn acp_download_agent_binary( let _ = binary_cache::ensure_binary_for_agent(agent_type, version, fallback.url, cmd) .await?; - emit_acp_agents_updated(&app, "binary_downloaded", Some(agent_type)); + emit_acp_agents_updated(app, "binary_downloaded", Some(agent_type)); Ok(()) } registry::AgentDistribution::Npx { .. } => Err( @@ -1475,14 +1492,21 @@ pub async fn acp_download_agent_binary( } #[tauri::command] -pub async fn acp_detect_agent_local_version( +pub async fn acp_download_agent_binary( agent_type: AgentType, - db: State<'_, AppDatabase>, + app: tauri::AppHandle, +) -> Result<(), AcpError> { + acp_download_agent_binary_core(agent_type, &app).await +} + +pub(crate) async fn acp_detect_agent_local_version_core( + agent_type: AgentType, + conn: &sea_orm::DatabaseConnection, ) -> Result, AcpError> { let detected = detect_local_version(agent_type).await; if let Some(version) = detected.clone() { let _ = agent_setting_service::set_installed_version( - &db.conn, + conn, agent_type, Some(version.clone()), ) @@ -1490,9 +1514,7 @@ pub async fn acp_detect_agent_local_version( return Ok(Some(version)); } - // For package-based agents, probing can miss cached availability. - // Fall back to last known installed version persisted in DB. - let fallback = agent_setting_service::get_by_agent_type(&db.conn, agent_type) + let fallback = agent_setting_service::get_by_agent_type(conn, agent_type) .await .ok() .flatten() @@ -1501,11 +1523,18 @@ pub async fn acp_detect_agent_local_version( } #[tauri::command] -pub async fn acp_prepare_npx_agent( +pub async fn acp_detect_agent_local_version( + agent_type: AgentType, + db: State<'_, AppDatabase>, +) -> Result, AcpError> { + acp_detect_agent_local_version_core(agent_type, &db.conn).await +} + +pub(crate) async fn acp_prepare_npx_agent_core( agent_type: AgentType, registry_version: Option, - db: State<'_, AppDatabase>, - app: tauri::AppHandle, + db: &AppDatabase, + app: &tauri::AppHandle, ) -> Result { let meta = registry::get_agent_meta(agent_type); match meta.distribution { @@ -1548,7 +1577,7 @@ pub async fn acp_prepare_npx_agent( ) .await .map_err(|e| AcpError::protocol(e.to_string()))?; - emit_acp_agents_updated(&app, "npx_prepared", Some(agent_type)); + emit_acp_agents_updated(app, "npx_prepared", Some(agent_type)); Ok(resolved) } registry::AgentDistribution::Binary { .. } => Err(AcpError::protocol( @@ -1558,10 +1587,19 @@ pub async fn acp_prepare_npx_agent( } #[tauri::command] -pub async fn acp_uninstall_agent( +pub async fn acp_prepare_npx_agent( agent_type: AgentType, + registry_version: Option, db: State<'_, AppDatabase>, app: tauri::AppHandle, +) -> Result { + acp_prepare_npx_agent_core(agent_type, registry_version, &db, &app).await +} + +pub(crate) async fn acp_uninstall_agent_core( + agent_type: AgentType, + db: &AppDatabase, + app: &tauri::AppHandle, ) -> Result<(), AcpError> { let meta = registry::get_agent_meta(agent_type); match meta.distribution { @@ -1576,20 +1614,28 @@ pub async fn acp_uninstall_agent( agent_setting_service::set_installed_version(&db.conn, agent_type, None) .await .map_err(|e| AcpError::protocol(e.to_string()))?; - emit_acp_agents_updated(&app, "agent_uninstalled", Some(agent_type)); + emit_acp_agents_updated(app, "agent_uninstalled", Some(agent_type)); Ok(()) } #[tauri::command] -pub async fn acp_reorder_agents( - agent_types: Vec, +pub async fn acp_uninstall_agent( + agent_type: AgentType, db: State<'_, AppDatabase>, app: tauri::AppHandle, +) -> Result<(), AcpError> { + acp_uninstall_agent_core(agent_type, &db, &app).await +} + +pub(crate) async fn acp_reorder_agents_core( + agent_types: &[AgentType], + db: &AppDatabase, + app: &tauri::AppHandle, ) -> Result<(), AcpError> { if agent_types.is_empty() { return Ok(()); } - agent_setting_service::reorder(&db.conn, &agent_types) + agent_setting_service::reorder(&db.conn, agent_types) .await .map_err(|e| { let message = e.to_string(); @@ -1599,10 +1645,19 @@ pub async fn acp_reorder_agents( AcpError::protocol(message) } })?; - emit_acp_agents_updated(&app, "agent_reordered", None); + emit_acp_agents_updated(app, "agent_reordered", None); Ok(()) } +#[tauri::command] +pub async fn acp_reorder_agents( + agent_types: Vec, + db: State<'_, AppDatabase>, + app: tauri::AppHandle, +) -> Result<(), AcpError> { + acp_reorder_agents_core(&agent_types, &db, &app).await +} + #[tauri::command] pub async fn acp_list_agent_skills( agent_type: AgentType, diff --git a/src-tauri/src/commands/folders.rs b/src-tauri/src/commands/folders.rs index dfebfe4..29d2b3d 100644 --- a/src-tauri/src/commands/folders.rs +++ b/src-tauri/src/commands/folders.rs @@ -489,32 +489,39 @@ pub async fn add_folder_to_history( folder_service::add_folder(&db.conn, &path).await } -#[tauri::command] -pub async fn set_folder_parent_branch( - db: tauri::State<'_, AppDatabase>, - path: String, +pub(crate) async fn set_folder_parent_branch_core( + conn: &sea_orm::DatabaseConnection, + path: &str, parent_branch: Option, ) -> Result<(), AppCommandError> { - // Find folder by path first use crate::db::entities::folder; use sea_orm::{ColumnTrait, EntityTrait, QueryFilter}; let row = folder::Entity::find() - .filter(folder::Column::Path.eq(&path)) + .filter(folder::Column::Path.eq(path)) .filter(folder::Column::DeletedAt.is_null()) - .one(&db.conn) + .one(conn) .await .map_err(|e| { AppCommandError::database_error("Failed to query folder").with_detail(e.to_string()) })?; if let Some(folder_model) = row { - folder_service::set_folder_parent_branch(&db.conn, folder_model.id, parent_branch) + folder_service::set_folder_parent_branch(conn, folder_model.id, parent_branch) .await .map_err(AppCommandError::from)?; } Ok(()) } +#[tauri::command] +pub async fn set_folder_parent_branch( + db: tauri::State<'_, AppDatabase>, + path: String, + parent_branch: Option, +) -> Result<(), AppCommandError> { + set_folder_parent_branch_core(&db.conn, &path, parent_branch).await +} + #[tauri::command] pub async fn remove_folder_from_history( db: tauri::State<'_, AppDatabase>, @@ -539,13 +546,12 @@ pub async fn create_folder_directory(path: String) -> Result<(), AppCommandError std::fs::create_dir_all(&path).map_err(AppCommandError::io) } -#[tauri::command] -pub async fn clone_repository( - url: String, - target_dir: String, - credentials: Option, - db: tauri::State<'_, AppDatabase>, - app_handle: tauri::AppHandle, +pub(crate) async fn clone_repository_core( + url: &str, + target_dir: &str, + credentials: Option<&GitCredentials>, + db: &AppDatabase, + app_handle: &tauri::AppHandle, ) -> Result<(), AppCommandError> { if url.trim().is_empty() || target_dir.trim().is_empty() { return Err(AppCommandError::invalid_input( @@ -554,8 +560,8 @@ pub async fn clone_repository( } let mut cmd = crate::process::tokio_command("git"); - cmd.args(["clone", &url, &target_dir]); - prepare_remote_git_cmd_for_url(&mut cmd, &url, credentials.as_ref(), &db, &app_handle).await; + cmd.args(["clone", url, target_dir]); + prepare_remote_git_cmd_for_url(&mut cmd, url, credentials, db, app_handle).await; let output = cmd .output() @@ -578,6 +584,17 @@ pub async fn clone_repository( Ok(()) } +#[tauri::command] +pub async fn clone_repository( + url: String, + target_dir: String, + credentials: Option, + db: tauri::State<'_, AppDatabase>, + app_handle: tauri::AppHandle, +) -> Result<(), AppCommandError> { + clone_repository_core(&url, &target_dir, credentials.as_ref(), &db, &app_handle).await +} + fn classify_git_clone_error(stderr: &str) -> AppCommandError { let normalized = stderr.to_lowercase(); @@ -677,19 +694,18 @@ pub async fn git_init(path: String) -> Result<(), AppCommandError> { Ok(()) } -#[tauri::command] -pub async fn git_pull( - path: String, - credentials: Option, - db: tauri::State<'_, AppDatabase>, - app_handle: tauri::AppHandle, +pub(crate) async fn git_pull_core( + path: &str, + credentials: Option<&GitCredentials>, + db: &AppDatabase, + app_handle: &tauri::AppHandle, ) -> Result { - let head_before = get_head_hash(&path).await?; + let head_before = get_head_hash(path).await?; // Step 1: fetch from remote let mut fetch_cmd = crate::process::tokio_command("git"); - fetch_cmd.args(["fetch"]).current_dir(&path); - prepare_remote_git_cmd(&mut fetch_cmd, &path, credentials.as_ref(), &db, &app_handle).await; + fetch_cmd.args(["fetch"]).current_dir(path); + prepare_remote_git_cmd(&mut fetch_cmd, path, credentials, db, app_handle).await; let fetch_output = fetch_cmd .output() @@ -703,13 +719,12 @@ pub async fn git_pull( // Step 2: check if upstream exists let upstream_check = crate::process::tokio_command("git") .args(["rev-parse", "@{u}"]) - .current_dir(&path) + .current_dir(path) .output() .await .map_err(AppCommandError::io)?; if !upstream_check.status.success() { - // No upstream configured, nothing to merge return Ok(GitPullResult { updated_files: 0, conflict: None, @@ -722,13 +737,13 @@ pub async fn git_pull( // Step 3: check if we can fast-forward let merge_base = crate::process::tokio_command("git") .args(["merge-base", "HEAD", "@{u}"]) - .current_dir(&path) + .current_dir(path) .output() .await .map_err(AppCommandError::io)?; let head_hash = crate::process::tokio_command("git") .args(["rev-parse", "HEAD"]) - .current_dir(&path) + .current_dir(path) .output() .await .map_err(AppCommandError::io)?; @@ -737,10 +752,9 @@ pub async fn git_pull( let current_head = String::from_utf8_lossy(&head_hash.stdout).trim().to_string(); if base_hash == current_head { - // Can fast-forward — just do it let ff_output = crate::process::tokio_command("git") .args(["merge", "--ff-only", "@{u}"]) - .current_dir(&path) + .current_dir(path) .output() .await .map_err(AppCommandError::io)?; @@ -749,22 +763,19 @@ pub async fn git_pull( return Err(git_command_error("merge --ff-only", &ff_output.stderr)); } } else { - // Non-fast-forward: try merge with --no-commit to detect conflicts let merge_output = crate::process::tokio_command("git") .args(["merge", "--no-commit", "@{u}"]) - .current_dir(&path) + .current_dir(path) .output() .await .map_err(AppCommandError::io)?; if !merge_output.status.success() { - // Check for conflicts - let conflicted_files = detect_conflicts(&path).await?; + let conflicted_files = detect_conflicts(path).await?; if !conflicted_files.is_empty() { - // Abort merge to restore working tree let _ = crate::process::tokio_command("git") .args(["merge", "--abort"]) - .current_dir(&path) + .current_dir(path) .output() .await; @@ -781,10 +792,9 @@ pub async fn git_pull( return Err(git_command_error("merge", &merge_output.stderr)); } - // Merge succeeded without conflicts — commit let commit_output = crate::process::tokio_command("git") .args(["commit", "--no-edit"]) - .current_dir(&path) + .current_dir(path) .output() .await .map_err(AppCommandError::io)?; @@ -798,12 +808,12 @@ pub async fn git_pull( } } - let head_after = get_head_hash(&path).await?; + let head_after = get_head_hash(path).await?; let updated_files = match (head_before.as_deref(), head_after.as_deref()) { (Some(before), Some(after)) if before != after => { - count_changed_files_between(&path, before, after).await? + count_changed_files_between(path, before, after).await? } - (None, Some(after)) => count_files_in_commit(&path, after).await?, + (None, Some(after)) => count_files_in_commit(path, after).await?, _ => 0, }; @@ -813,6 +823,16 @@ pub async fn git_pull( }) } +#[tauri::command] +pub async fn git_pull( + path: String, + credentials: Option, + db: tauri::State<'_, AppDatabase>, + app_handle: tauri::AppHandle, +) -> Result { + git_pull_core(&path, credentials.as_ref(), &db, &app_handle).await +} + /// Start a merge with the upstream branch (used by merge workspace after pull conflict detection). /// This recreates the conflict state so that :1:, :2:, :3: stage entries are available. /// If `upstream_commit` is provided, merge against that specific commit instead of `@{u}`. @@ -853,16 +873,15 @@ pub async fn git_has_merge_head(path: String) -> Result { Ok(output.status.success()) } -#[tauri::command] -pub async fn git_fetch( - path: String, - credentials: Option, - db: tauri::State<'_, AppDatabase>, - app_handle: tauri::AppHandle, +pub(crate) async fn git_fetch_core( + path: &str, + credentials: Option<&GitCredentials>, + db: &AppDatabase, + app_handle: &tauri::AppHandle, ) -> Result { let mut cmd = crate::process::tokio_command("git"); - cmd.args(["fetch", "--all"]).current_dir(&path); - prepare_remote_git_cmd(&mut cmd, &path, credentials.as_ref(), &db, &app_handle).await; + cmd.args(["fetch", "--all"]).current_dir(path); + prepare_remote_git_cmd(&mut cmd, path, credentials, db, app_handle).await; let output = cmd .output() @@ -875,6 +894,16 @@ pub async fn git_fetch( Ok(String::from_utf8_lossy(&output.stderr).trim().to_string()) } +#[tauri::command] +pub async fn git_fetch( + path: String, + credentials: Option, + db: tauri::State<'_, AppDatabase>, + app_handle: tauri::AppHandle, +) -> Result { + git_fetch_core(&path, credentials.as_ref(), &db, &app_handle).await +} + #[tauri::command] pub async fn git_push_info(path: String) -> Result { // Get current branch name @@ -911,24 +940,23 @@ pub async fn git_push_info(path: String) -> Result }) } -#[tauri::command] -pub async fn git_push( - app: tauri::AppHandle, - window: tauri::WebviewWindow, - path: String, - remote: Option, - credentials: Option, - db: tauri::State<'_, AppDatabase>, +pub(crate) async fn git_push_core( + app: &tauri::AppHandle, + folder_id: Option, + path: &str, + remote: Option<&str>, + credentials: Option<&GitCredentials>, + db: &AppDatabase, ) -> Result { - let pushed_commits = estimate_push_commit_count(&path).await; + let pushed_commits = estimate_push_commit_count(path).await; - // Determine the target remote (use provided or fall back to tracking remote) - let target_remote = remote.unwrap_or_else(|| "origin".to_string()); + let target_remote = remote + .filter(|s| !s.is_empty()) + .unwrap_or("origin"); - // Check if the current branch has an upstream configured for this remote let branch_output = crate::process::tokio_command("git") .args(["rev-parse", "--abbrev-ref", "HEAD"]) - .current_dir(&path) + .current_dir(path) .output() .await .map_err(AppCommandError::io)?; @@ -936,10 +964,9 @@ pub async fn git_push( .trim() .to_string(); - // Check if upstream is set and points to the target remote let upstream_check = crate::process::tokio_command("git") .args(["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"]) - .current_dir(&path) + .current_dir(path) .output() .await .map_err(AppCommandError::io)?; @@ -954,7 +981,6 @@ pub async fn git_push( None }; - // Need to set upstream if: no upstream at all, or upstream points to a different remote let needs_set_upstream = match ¤t_upstream { None => true, Some(upstream) => !upstream.starts_with(&format!("{}/", target_remote)), @@ -962,15 +988,15 @@ pub async fn git_push( let output = if needs_set_upstream { let mut cmd = crate::process::tokio_command("git"); - cmd.args(["push", "--set-upstream", &target_remote, &branch]) - .current_dir(&path); - prepare_remote_git_cmd_with_remote(&mut cmd, &path, Some(&target_remote), credentials.as_ref(), &db, &app).await; + cmd.args(["push", "--set-upstream", target_remote, &branch]) + .current_dir(path); + prepare_remote_git_cmd_with_remote(&mut cmd, path, Some(target_remote), credentials, db, app).await; cmd.output().await.map_err(AppCommandError::io)? } else { let mut cmd = crate::process::tokio_command("git"); - cmd.args(["push", &target_remote, &branch]) - .current_dir(&path); - prepare_remote_git_cmd_with_remote(&mut cmd, &path, Some(&target_remote), credentials.as_ref(), &db, &app).await; + cmd.args(["push", target_remote, &branch]) + .current_dir(path); + prepare_remote_git_cmd_with_remote(&mut cmd, path, Some(target_remote), credentials, db, app).await; cmd.output().await.map_err(AppCommandError::io)? }; @@ -980,13 +1006,9 @@ pub async fn git_push( let upstream_set = needs_set_upstream; - if let Some(folder_id) = window - .label() - .strip_prefix("push-") - .and_then(|value| value.parse::().ok()) - { + if let Some(folder_id) = folder_id { crate::web::event_bridge::emit_event( - &app, + app, "folder://git-push-succeeded", GitPushSucceededEvent { folder_id, @@ -1002,6 +1024,22 @@ pub async fn git_push( }) } +#[tauri::command] +pub async fn git_push( + app: tauri::AppHandle, + window: tauri::WebviewWindow, + path: String, + remote: Option, + credentials: Option, + db: tauri::State<'_, AppDatabase>, +) -> Result { + let folder_id = window + .label() + .strip_prefix("push-") + .and_then(|value| value.parse::().ok()); + git_push_core(&app, folder_id, &path, remote.as_deref(), credentials.as_ref(), &db).await +} + #[tauri::command] pub async fn git_new_branch( path: String, @@ -1495,14 +1533,13 @@ pub async fn git_show_file( Ok(String::from_utf8_lossy(bytes).to_string()) } -#[tauri::command] -pub async fn git_commit( - app: tauri::AppHandle, - window: tauri::WebviewWindow, - db: tauri::State<'_, AppDatabase>, - path: String, - message: String, - files: Vec, +pub(crate) async fn git_commit_core( + app: &tauri::AppHandle, + folder_id: Option, + conn: &sea_orm::DatabaseConnection, + path: &str, + message: &str, + files: &[String], ) -> Result { // Stage selected files let mut add_args = vec!["add".to_string(), "--".to_string()]; @@ -1510,7 +1547,7 @@ pub async fn git_commit( let add_output = crate::process::tokio_command("git") .args(&add_args) - .current_dir(&path) + .current_dir(path) .output() .await .map_err(AppCommandError::io)?; @@ -1521,7 +1558,7 @@ pub async fn git_commit( // Resolve commit author from matching account (e.g. GitHub username) let author_override = - crate::git_credential::resolve_commit_author(&path, &db.conn).await; + crate::git_credential::resolve_commit_author(path, conn).await; // Commit let mut commit_cmd = crate::process::tokio_command("git"); @@ -1533,7 +1570,7 @@ pub async fn git_commit( &format!("user.email={email}"), ]); } - commit_cmd.args(["commit", "-m", &message]).current_dir(&path); + commit_cmd.args(["commit", "-m", message]).current_dir(path); let commit_output = commit_cmd .output() @@ -1544,17 +1581,13 @@ pub async fn git_commit( return Err(git_command_error("commit", &commit_output.stderr)); } - let committed_files = count_files_in_commit(&path, "HEAD") + let committed_files = count_files_in_commit(path, "HEAD") .await .unwrap_or(files.len()); - if let Some(folder_id) = window - .label() - .strip_prefix("commit-") - .and_then(|value| value.parse::().ok()) - { + if let Some(folder_id) = folder_id { crate::web::event_bridge::emit_event( - &app, + app, "folder://git-commit-succeeded", GitCommitSucceededEvent { folder_id, @@ -1566,6 +1599,22 @@ pub async fn git_commit( Ok(GitCommitResult { committed_files }) } +#[tauri::command] +pub async fn git_commit( + app: tauri::AppHandle, + window: tauri::WebviewWindow, + db: tauri::State<'_, AppDatabase>, + path: String, + message: String, + files: Vec, +) -> Result { + let folder_id = window + .label() + .strip_prefix("commit-") + .and_then(|value| value.parse::().ok()); + git_commit_core(&app, folder_id, &db.conn, &path, &message, &files).await +} + #[tauri::command] pub async fn git_rollback_file(path: String, file: String) -> Result<(), AppCommandError> { let target = file.trim(); @@ -1761,17 +1810,16 @@ pub async fn git_list_remotes(path: String) -> Result, AppCommand Ok(remotes) } -#[tauri::command] -pub async fn git_fetch_remote( - path: String, - name: String, - credentials: Option, - db: tauri::State<'_, AppDatabase>, - app_handle: tauri::AppHandle, +pub(crate) async fn git_fetch_remote_core( + path: &str, + name: &str, + credentials: Option<&GitCredentials>, + db: &AppDatabase, + app_handle: &tauri::AppHandle, ) -> Result { let mut cmd = crate::process::tokio_command("git"); - cmd.args(["fetch", &name]).current_dir(&path); - prepare_remote_git_cmd_with_remote(&mut cmd, &path, Some(&name), credentials.as_ref(), &db, &app_handle).await; + cmd.args(["fetch", name]).current_dir(path); + prepare_remote_git_cmd_with_remote(&mut cmd, path, Some(name), credentials, db, app_handle).await; let output = cmd .output() @@ -1784,6 +1832,17 @@ pub async fn git_fetch_remote( Ok(String::from_utf8_lossy(&output.stderr).trim().to_string()) } +#[tauri::command] +pub async fn git_fetch_remote( + path: String, + name: String, + credentials: Option, + db: tauri::State<'_, AppDatabase>, + app_handle: tauri::AppHandle, +) -> Result { + git_fetch_remote_core(&path, &name, credentials.as_ref(), &db, &app_handle).await +} + #[tauri::command] pub async fn git_add_remote( path: String, diff --git a/src-tauri/src/commands/version_control.rs b/src-tauri/src/commands/version_control.rs index a18addb..8aa0bfe 100644 --- a/src-tauri/src/commands/version_control.rs +++ b/src-tauri/src/commands/version_control.rs @@ -76,12 +76,10 @@ async fn detect_git_path() -> Option { } } -#[tauri::command] -pub async fn detect_git( - db: State<'_, AppDatabase>, +pub(crate) async fn detect_git_core( + conn: &sea_orm::DatabaseConnection, ) -> Result { - // Check if there's a custom path configured - let settings = load_git_settings(&db.conn).await?; + let settings = load_git_settings(conn).await?; if let Some(custom) = &settings.custom_path { let trimmed = custom.trim(); @@ -90,12 +88,10 @@ pub async fn detect_git( } } - // Auto-detect if let Some(path) = detect_git_path().await { return run_git_version(&path).await; } - // Fallback: try "git" directly (might be in PATH but `which` failed) match run_git_version("git").await { Ok(result) if result.installed => Ok(result), _ => Ok(GitDetectResult { @@ -106,6 +102,13 @@ pub async fn detect_git( } } +#[tauri::command] +pub async fn detect_git( + db: State<'_, AppDatabase>, +) -> Result { + detect_git_core(&db.conn).await +} + #[tauri::command] pub async fn test_git_path(path: String) -> Result { let trimmed = path.trim(); diff --git a/src-tauri/src/web/handlers/acp.rs b/src-tauri/src/web/handlers/acp.rs index 4578886..6d6685c 100644 --- a/src-tauri/src/web/handlers/acp.rs +++ b/src-tauri/src/web/handlers/acp.rs @@ -1,10 +1,16 @@ +use std::collections::BTreeMap; + use axum::{extract::Extension, Json}; use serde::Deserialize; use tauri::Manager; use crate::acp::manager::ConnectionManager; +use crate::acp::preflight::PreflightResult; use crate::acp::registry; -use crate::acp::types::{AcpAgentInfo, AcpAgentStatus}; +use crate::acp::types::{ + AcpAgentInfo, AcpAgentStatus, AgentSkillContent, AgentSkillLayout, AgentSkillScope, + AgentSkillsListResult, ConnectionInfo, ForkResultInfo, +}; use crate::app_error::AppCommandError; use crate::commands::acp as acp_commands; use crate::db::service::agent_setting_service; @@ -139,3 +145,327 @@ pub async fn acp_prompt( .map_err(|e| AppCommandError::task_execution_failed(e.to_string()))?; Ok(Json(())) } + +// --- Pattern A: Pure function handlers --- + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AcpPreflightParams { + pub agent_type: AgentType, + pub force_refresh: Option, +} + +pub async fn acp_preflight( + Json(params): Json, +) -> Result, AppCommandError> { + let result = acp_commands::acp_preflight(params.agent_type, params.force_refresh) + .await + .map_err(|e| AppCommandError::task_execution_failed(e.to_string()))?; + Ok(Json(result)) +} + +pub async fn acp_clear_binary_cache( + Json(params): Json, +) -> Result, AppCommandError> { + acp_commands::acp_clear_binary_cache(params.agent_type) + .await + .map_err(|e| AppCommandError::task_execution_failed(e.to_string()))?; + Ok(Json(())) +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AcpListAgentSkillsParams { + pub agent_type: AgentType, + pub workspace_path: Option, +} + +pub async fn acp_list_agent_skills( + Json(params): Json, +) -> Result, AppCommandError> { + let result = + acp_commands::acp_list_agent_skills(params.agent_type, params.workspace_path) + .await + .map_err(|e| AppCommandError::task_execution_failed(e.to_string()))?; + Ok(Json(result)) +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AcpReadAgentSkillParams { + pub agent_type: AgentType, + pub scope: AgentSkillScope, + pub skill_id: String, + pub workspace_path: Option, +} + +pub async fn acp_read_agent_skill( + Json(params): Json, +) -> Result, AppCommandError> { + let result = acp_commands::acp_read_agent_skill( + params.agent_type, + params.scope, + params.skill_id, + params.workspace_path, + ) + .await + .map_err(|e| AppCommandError::task_execution_failed(e.to_string()))?; + Ok(Json(result)) +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AcpSaveAgentSkillParams { + pub agent_type: AgentType, + pub scope: AgentSkillScope, + pub skill_id: String, + pub content: String, + pub workspace_path: Option, + pub layout: Option, +} + +pub async fn acp_save_agent_skill( + Json(params): Json, +) -> Result, AppCommandError> { + acp_commands::acp_save_agent_skill( + params.agent_type, + params.scope, + params.skill_id, + params.content, + params.workspace_path, + params.layout, + ) + .await + .map_err(|e| AppCommandError::task_execution_failed(e.to_string()))?; + Ok(Json(())) +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AcpDeleteAgentSkillParams { + pub agent_type: AgentType, + pub scope: AgentSkillScope, + pub skill_id: String, + pub workspace_path: Option, +} + +pub async fn acp_delete_agent_skill( + Json(params): Json, +) -> Result, AppCommandError> { + acp_commands::acp_delete_agent_skill( + params.agent_type, + params.scope, + params.skill_id, + params.workspace_path, + ) + .await + .map_err(|e| AppCommandError::task_execution_failed(e.to_string()))?; + Ok(Json(())) +} + +// --- Pattern C: ConnectionManager handlers --- + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AcpConnectionIdParams { + pub connection_id: String, +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AcpSetModeParams { + pub connection_id: String, + pub mode_id: String, +} + +pub async fn acp_set_mode( + Extension(app): Extension, + Json(params): Json, +) -> Result, AppCommandError> { + let manager = app.state::(); + manager + .set_mode(¶ms.connection_id, params.mode_id) + .await + .map_err(|e| AppCommandError::task_execution_failed(e.to_string()))?; + Ok(Json(())) +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AcpSetConfigOptionParams { + pub connection_id: String, + pub config_id: String, + pub value_id: String, +} + +pub async fn acp_set_config_option( + Extension(app): Extension, + Json(params): Json, +) -> Result, AppCommandError> { + let manager = app.state::(); + manager + .set_config_option(¶ms.connection_id, params.config_id, params.value_id) + .await + .map_err(|e| AppCommandError::task_execution_failed(e.to_string()))?; + Ok(Json(())) +} + +pub async fn acp_cancel( + Extension(app): Extension, + Json(params): Json, +) -> Result, AppCommandError> { + let manager = app.state::(); + manager + .cancel(¶ms.connection_id) + .await + .map_err(|e| AppCommandError::task_execution_failed(e.to_string()))?; + Ok(Json(())) +} + +pub async fn acp_fork( + Extension(app): Extension, + Json(params): Json, +) -> Result, AppCommandError> { + let manager = app.state::(); + let result = manager + .fork_session(¶ms.connection_id) + .await + .map_err(|e| AppCommandError::task_execution_failed(e.to_string()))?; + Ok(Json(result)) +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AcpRespondPermissionParams { + pub connection_id: String, + pub request_id: String, + pub option_id: String, +} + +pub async fn acp_respond_permission( + Extension(app): Extension, + Json(params): Json, +) -> Result, AppCommandError> { + let manager = app.state::(); + manager + .respond_permission(¶ms.connection_id, ¶ms.request_id, ¶ms.option_id) + .await + .map_err(|e| AppCommandError::task_execution_failed(e.to_string()))?; + Ok(Json(())) +} + +pub async fn acp_list_connections( + Extension(app): Extension, +) -> Result>, AppCommandError> { + let manager = app.state::(); + let result = manager.list_connections().await; + Ok(Json(result)) +} + +// --- Pattern B+: Core function handlers --- + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AcpUpdateAgentPreferencesParams { + pub agent_type: AgentType, + pub enabled: bool, + pub env: BTreeMap, + pub config_json: Option, + pub opencode_auth_json: Option, + pub codex_auth_json: Option, + pub codex_config_toml: Option, +} + +pub async fn acp_update_agent_preferences( + Extension(app): Extension, + Json(params): Json, +) -> Result, AppCommandError> { + let db = app.state::(); + acp_commands::acp_update_agent_preferences_core( + params.agent_type, + params.enabled, + params.env, + params.config_json, + params.opencode_auth_json, + params.codex_auth_json, + params.codex_config_toml, + &db, + &app, + ) + .await + .map_err(|e| AppCommandError::task_execution_failed(e.to_string()))?; + Ok(Json(())) +} + +pub async fn acp_download_agent_binary( + Extension(app): Extension, + Json(params): Json, +) -> Result, AppCommandError> { + acp_commands::acp_download_agent_binary_core(params.agent_type, &app) + .await + .map_err(|e| AppCommandError::task_execution_failed(e.to_string()))?; + Ok(Json(())) +} + +pub async fn acp_detect_agent_local_version( + Extension(app): Extension, + Json(params): Json, +) -> Result>, AppCommandError> { + let db = app.state::(); + let result = + acp_commands::acp_detect_agent_local_version_core(params.agent_type, &db.conn) + .await + .map_err(|e| AppCommandError::task_execution_failed(e.to_string()))?; + Ok(Json(result)) +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AcpPrepareNpxAgentParams { + pub agent_type: AgentType, + pub registry_version: Option, +} + +pub async fn acp_prepare_npx_agent( + Extension(app): Extension, + Json(params): Json, +) -> Result, AppCommandError> { + let db = app.state::(); + let result = acp_commands::acp_prepare_npx_agent_core( + params.agent_type, + params.registry_version, + &db, + &app, + ) + .await + .map_err(|e| AppCommandError::task_execution_failed(e.to_string()))?; + Ok(Json(result)) +} + +pub async fn acp_uninstall_agent( + Extension(app): Extension, + Json(params): Json, +) -> Result, AppCommandError> { + let db = app.state::(); + acp_commands::acp_uninstall_agent_core(params.agent_type, &db, &app) + .await + .map_err(|e| AppCommandError::task_execution_failed(e.to_string()))?; + Ok(Json(())) +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AcpReorderAgentsParams { + pub agent_types: Vec, +} + +pub async fn acp_reorder_agents( + Extension(app): Extension, + Json(params): Json, +) -> Result, AppCommandError> { + let db = app.state::(); + acp_commands::acp_reorder_agents_core(¶ms.agent_types, &db, &app) + .await + .map_err(|e| AppCommandError::task_execution_failed(e.to_string()))?; + Ok(Json(())) +} diff --git a/src-tauri/src/web/handlers/files.rs b/src-tauri/src/web/handlers/files.rs new file mode 100644 index 0000000..57ee79b --- /dev/null +++ b/src-tauri/src/web/handlers/files.rs @@ -0,0 +1,156 @@ +use axum::Json; +use serde::Deserialize; + +use crate::app_error::AppCommandError; +use crate::commands::folders as folder_commands; + +// --------------------------------------------------------------------------- +// Param structs +// --------------------------------------------------------------------------- + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ReadFilePreviewParams { + pub root_path: String, + pub path: String, +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ReadFileBase64Params { + pub path: String, + pub max_bytes: Option, +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ReadFileForEditParams { + pub root_path: String, + pub path: String, +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SaveFileContentParams { + pub root_path: String, + pub path: String, + pub content: String, + pub expected_etag: Option, +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SaveFileCopyParams { + pub root_path: String, + pub path: String, + pub content: String, +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RenameFileTreeEntryParams { + pub root_path: String, + pub path: String, + pub new_name: String, +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct DeleteFileTreeEntryParams { + pub root_path: String, + pub path: String, +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CreateFileTreeEntryParams { + pub root_path: String, + pub path: String, + pub name: String, + pub kind: String, +} + +// --------------------------------------------------------------------------- +// Handlers +// --------------------------------------------------------------------------- + +pub async fn read_file_preview( + Json(params): Json, +) -> Result, AppCommandError> { + let result = + folder_commands::read_file_preview(params.root_path, params.path).await?; + Ok(Json(result)) +} + +pub async fn read_file_base64( + Json(params): Json, +) -> Result, AppCommandError> { + let result = + folder_commands::read_file_base64(params.path, params.max_bytes).await?; + Ok(Json(result)) +} + +pub async fn read_file_for_edit( + Json(params): Json, +) -> Result, AppCommandError> { + let result = + folder_commands::read_file_for_edit(params.root_path, params.path).await?; + Ok(Json(result)) +} + +pub async fn save_file_content( + Json(params): Json, +) -> Result, AppCommandError> { + let result = folder_commands::save_file_content( + params.root_path, + params.path, + params.content, + params.expected_etag, + ) + .await?; + Ok(Json(result)) +} + +pub async fn save_file_copy( + Json(params): Json, +) -> Result, AppCommandError> { + let result = folder_commands::save_file_copy( + params.root_path, + params.path, + params.content, + ) + .await?; + Ok(Json(result)) +} + +pub async fn rename_file_tree_entry( + Json(params): Json, +) -> Result, AppCommandError> { + let result = folder_commands::rename_file_tree_entry( + params.root_path, + params.path, + params.new_name, + ) + .await?; + Ok(Json(result)) +} + +pub async fn delete_file_tree_entry( + Json(params): Json, +) -> Result, AppCommandError> { + folder_commands::delete_file_tree_entry(params.root_path, params.path).await?; + Ok(Json(())) +} + +pub async fn create_file_tree_entry( + Json(params): Json, +) -> Result, AppCommandError> { + let result = folder_commands::create_file_tree_entry( + params.root_path, + params.path, + params.name, + params.kind, + ) + .await?; + Ok(Json(result)) +} diff --git a/src-tauri/src/web/handlers/folder_commands.rs b/src-tauri/src/web/handlers/folder_commands.rs index 0537d4c..d919df6 100644 --- a/src-tauri/src/web/handlers/folder_commands.rs +++ b/src-tauri/src/web/handlers/folder_commands.rs @@ -7,12 +7,50 @@ use crate::db::service::folder_command_service; use crate::db::AppDatabase; use crate::models::*; +// --------------------------------------------------------------------------- +// Param structs +// --------------------------------------------------------------------------- + #[derive(Deserialize)] #[serde(rename_all = "camelCase")] pub struct FolderIdParams { pub folder_id: i32, } +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CreateFolderCommandParams { + pub folder_id: i32, + pub name: String, + pub command: String, +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct UpdateFolderCommandParams { + pub id: i32, + pub name: Option, + pub command: Option, + pub sort_order: Option, +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct DeleteFolderCommandParams { + pub id: i32, +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ReorderFolderCommandsParams { + pub folder_id: i32, + pub ids: Vec, +} + +// --------------------------------------------------------------------------- +// Handlers +// --------------------------------------------------------------------------- + pub async fn list_folder_commands( Extension(app): Extension, Json(params): Json, @@ -23,3 +61,62 @@ pub async fn list_folder_commands( .map_err(AppCommandError::from)?; Ok(Json(result)) } + +pub async fn create_folder_command( + Extension(app): Extension, + Json(params): Json, +) -> Result, AppCommandError> { + let db = app.state::(); + let result = folder_command_service::create( + &db.conn, + params.folder_id, + ¶ms.name, + ¶ms.command, + ) + .await + .map_err(AppCommandError::from)?; + Ok(Json(result)) +} + +pub async fn update_folder_command( + Extension(app): Extension, + Json(params): Json, +) -> Result, AppCommandError> { + let db = app.state::(); + let result = folder_command_service::update( + &db.conn, + params.id, + params.name, + params.command, + params.sort_order, + ) + .await + .map_err(AppCommandError::from)?; + Ok(Json(result)) +} + +pub async fn delete_folder_command( + Extension(app): Extension, + Json(params): Json, +) -> Result, AppCommandError> { + let db = app.state::(); + folder_command_service::delete(&db.conn, params.id) + .await + .map_err(AppCommandError::from)?; + Ok(Json(())) +} + +pub async fn reorder_folder_commands( + Extension(app): Extension, + Json(params): Json, +) -> Result, AppCommandError> { + let db = app.state::(); + folder_command_service::reorder(&db.conn, params.folder_id, params.ids) + .await + .map_err(AppCommandError::from)?; + Ok(Json(())) +} + +// TODO: bootstrap_folder_commands_from_package_json — requires access to +// `load_package_scripts_as_commands` which is private in commands/folder_commands.rs. +// Make it pub(crate) first, then add the web handler here. diff --git a/src-tauri/src/web/handlers/folders.rs b/src-tauri/src/web/handlers/folders.rs index 9ea69fd..2a4f54a 100644 --- a/src-tauri/src/web/handlers/folders.rs +++ b/src-tauri/src/web/handlers/folders.rs @@ -173,95 +173,6 @@ pub async fn open_settings_window( Ok(Json(SettingsNavigationResult { path })) } -#[derive(Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct GitStatusParams { - pub path: String, - pub show_all_untracked: Option, -} - -pub async fn git_status( - Json(params): Json, -) -> Result>, AppCommandError> { - let result = - folder_commands::git_status(params.path, params.show_all_untracked).await?; - Ok(Json(result)) -} - -#[derive(Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct ReadFilePreviewParams { - pub root_path: String, - pub path: String, -} - -pub async fn read_file_preview( - Json(params): Json, -) -> Result, AppCommandError> { - let result = - folder_commands::read_file_preview(params.root_path, params.path).await?; - Ok(Json(result)) -} - -pub async fn git_list_all_branches( - Json(params): Json, -) -> Result, AppCommandError> { - let result = folder_commands::git_list_all_branches(params.path).await?; - Ok(Json(result)) -} - -#[derive(Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct GitCommitBranchesParams { - pub path: String, - pub commit: String, -} - -pub async fn git_commit_branches( - Json(params): Json, -) -> Result>, AppCommandError> { - let result = - folder_commands::git_commit_branches(params.path, params.commit).await?; - Ok(Json(result)) -} - -#[derive(Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct GitShowFileParams { - pub path: String, - pub file: String, - pub ref_name: Option, -} - -pub async fn git_show_file( - Json(params): Json, -) -> Result, AppCommandError> { - let result = - folder_commands::git_show_file(params.path, params.file, params.ref_name).await?; - Ok(Json(result)) -} - -#[derive(Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct GitDiffParams { - pub path: String, - pub file: Option, -} - -pub async fn git_diff( - Json(params): Json, -) -> Result, AppCommandError> { - let result = folder_commands::git_diff(params.path, params.file).await?; - Ok(Json(result)) -} - -pub async fn git_list_remotes( - Json(params): Json, -) -> Result>, AppCommandError> { - let result = folder_commands::git_list_remotes(params.path).await?; - Ok(Json(result)) -} - #[derive(Deserialize)] #[serde(rename_all = "camelCase")] pub struct OpenCommitWindowParams { @@ -276,3 +187,86 @@ pub async fn open_commit_window( path: format!("/commit?folderId={}", params.folder_id), })) } + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct OpenMergeWindowParams { + pub folder_id: i32, + pub operation: Option, + pub upstream_commit: Option, +} + +pub async fn open_merge_window( + Json(params): Json, +) -> Result, AppCommandError> { + let mut path = format!("/merge?folderId={}", params.folder_id); + if let Some(op) = ¶ms.operation { + path.push_str(&format!("&operation={op}")); + } + if let Some(uc) = ¶ms.upstream_commit { + path.push_str(&format!("&upstreamCommit={uc}")); + } + Ok(Json(SettingsNavigationResult { path })) +} + +pub async fn open_stash_window( + Json(params): Json, +) -> Result, AppCommandError> { + Ok(Json(SettingsNavigationResult { + path: format!("/stash?folderId={}", params.folder_id), + })) +} + +pub async fn open_push_window( + Json(params): Json, +) -> Result, AppCommandError> { + Ok(Json(SettingsNavigationResult { + path: format!("/push?folderId={}", params.folder_id), + })) +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SetFolderParentBranchParams { + pub path: String, + pub parent_branch: Option, +} + +pub async fn add_folder_to_history( + Extension(app): Extension, + Json(params): Json, +) -> Result, AppCommandError> { + let db = app.state::(); + let result = folder_service::add_folder(&db.conn, ¶ms.path) + .await + .map_err(AppCommandError::from)?; + Ok(Json(result)) +} + +pub async fn set_folder_parent_branch( + Extension(app): Extension, + Json(params): Json, +) -> Result, AppCommandError> { + let db = app.state::(); + folder_commands::set_folder_parent_branch_core(&db.conn, ¶ms.path, params.parent_branch) + .await?; + Ok(Json(())) +} + +pub async fn remove_folder_from_history( + Extension(app): Extension, + Json(params): Json, +) -> Result, AppCommandError> { + let db = app.state::(); + folder_service::remove_folder(&db.conn, ¶ms.path) + .await + .map_err(AppCommandError::from)?; + Ok(Json(())) +} + +pub async fn create_folder_directory( + Json(params): Json, +) -> Result, AppCommandError> { + folder_commands::create_folder_directory(params.path).await?; + Ok(Json(())) +} diff --git a/src-tauri/src/web/handlers/git.rs b/src-tauri/src/web/handlers/git.rs new file mode 100644 index 0000000..92bf6b7 --- /dev/null +++ b/src-tauri/src/web/handlers/git.rs @@ -0,0 +1,636 @@ +use axum::{extract::Extension, Json}; +use serde::Deserialize; +use tauri::Manager; + +use crate::app_error::AppCommandError; +use crate::commands::folders as folder_commands; +use crate::db::AppDatabase; +use crate::models::GitCredentials; + +use super::folders::PathParams; + +// --------------------------------------------------------------------------- +// Shared param structs +// --------------------------------------------------------------------------- + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PathFileParams { + pub path: String, + pub file: String, +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PathBranchParams { + pub path: String, + pub branch_name: String, +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PathStashRefParams { + pub path: String, + pub stash_ref: String, +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PathNameParams { + pub path: String, + pub name: String, +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PathNameUrlParams { + pub path: String, + pub name: String, + pub url: String, +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PathOperationParams { + pub path: String, + pub operation: String, +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PathFileContentParams { + pub path: String, + pub file: String, + pub content: String, +} + +// --------------------------------------------------------------------------- +// Migrated from folders.rs +// --------------------------------------------------------------------------- + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GitStatusParams { + pub path: String, + pub show_all_untracked: Option, +} + +pub async fn git_status( + Json(params): Json, +) -> Result>, AppCommandError> { + let result = + folder_commands::git_status(params.path, params.show_all_untracked).await?; + Ok(Json(result)) +} + +pub async fn git_list_all_branches( + Json(params): Json, +) -> Result, AppCommandError> { + let result = folder_commands::git_list_all_branches(params.path).await?; + Ok(Json(result)) +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GitCommitBranchesParams { + pub path: String, + pub commit: String, +} + +pub async fn git_commit_branches( + Json(params): Json, +) -> Result>, AppCommandError> { + let result = + folder_commands::git_commit_branches(params.path, params.commit).await?; + Ok(Json(result)) +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GitShowFileParams { + pub path: String, + pub file: String, + pub ref_name: Option, +} + +pub async fn git_show_file( + Json(params): Json, +) -> Result, AppCommandError> { + let result = + folder_commands::git_show_file(params.path, params.file, params.ref_name) + .await?; + Ok(Json(result)) +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GitDiffParams { + pub path: String, + pub file: Option, +} + +pub async fn git_diff( + Json(params): Json, +) -> Result, AppCommandError> { + let result = folder_commands::git_diff(params.path, params.file).await?; + Ok(Json(result)) +} + +pub async fn git_list_remotes( + Json(params): Json, +) -> Result>, AppCommandError> { + let result = folder_commands::git_list_remotes(params.path).await?; + Ok(Json(result)) +} + +// --------------------------------------------------------------------------- +// Migrated from version_control.rs +// --------------------------------------------------------------------------- + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GitLogParams { + pub path: String, + pub limit: Option, + pub branch: Option, + pub remote: Option, +} + +pub async fn git_log( + Json(params): Json, +) -> Result, AppCommandError> { + let result = folder_commands::git_log( + params.path, + params.limit, + params.branch, + params.remote, + ) + .await?; + Ok(Json(result)) +} + +// --------------------------------------------------------------------------- +// New pure git handlers (Pattern A – direct function calls) +// --------------------------------------------------------------------------- + +pub async fn git_init( + Json(params): Json, +) -> Result, AppCommandError> { + folder_commands::git_init(params.path).await?; + Ok(Json(())) +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GitStartPullMergeParams { + pub path: String, + pub upstream_commit: Option, +} + +pub async fn git_start_pull_merge( + Json(params): Json, +) -> Result, AppCommandError> { + folder_commands::git_start_pull_merge(params.path, params.upstream_commit).await?; + Ok(Json(())) +} + +pub async fn git_has_merge_head( + Json(params): Json, +) -> Result, AppCommandError> { + let result = folder_commands::git_has_merge_head(params.path).await?; + Ok(Json(result)) +} + +pub async fn git_push_info( + Json(params): Json, +) -> Result, AppCommandError> { + let result = folder_commands::git_push_info(params.path).await?; + Ok(Json(result)) +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GitNewBranchParams { + pub path: String, + pub branch_name: String, + pub start_point: Option, +} + +pub async fn git_new_branch( + Json(params): Json, +) -> Result, AppCommandError> { + folder_commands::git_new_branch(params.path, params.branch_name, params.start_point) + .await?; + Ok(Json(())) +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GitWorktreeAddParams { + pub path: String, + pub branch_name: String, + pub worktree_path: String, +} + +pub async fn git_worktree_add( + Json(params): Json, +) -> Result, AppCommandError> { + folder_commands::git_worktree_add( + params.path, + params.branch_name, + params.worktree_path, + ) + .await?; + Ok(Json(())) +} + +pub async fn git_checkout( + Json(params): Json, +) -> Result, AppCommandError> { + folder_commands::git_checkout(params.path, params.branch_name).await?; + Ok(Json(())) +} + +pub async fn git_list_branches( + Json(params): Json, +) -> Result>, AppCommandError> { + let result = folder_commands::git_list_branches(params.path).await?; + Ok(Json(result)) +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GitStashPushParams { + pub path: String, + pub message: Option, + pub keep_index: bool, +} + +pub async fn git_stash_push( + Json(params): Json, +) -> Result, AppCommandError> { + let result = folder_commands::git_stash_push( + params.path, + params.message, + params.keep_index, + ) + .await?; + Ok(Json(result)) +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GitStashPopParams { + pub path: String, + pub stash_ref: Option, +} + +pub async fn git_stash_pop( + Json(params): Json, +) -> Result, AppCommandError> { + let result = + folder_commands::git_stash_pop(params.path, params.stash_ref).await?; + Ok(Json(result)) +} + +pub async fn git_stash_list( + Json(params): Json, +) -> Result>, AppCommandError> { + let result = folder_commands::git_stash_list(params.path).await?; + Ok(Json(result)) +} + +pub async fn git_stash_apply( + Json(params): Json, +) -> Result, AppCommandError> { + let result = + folder_commands::git_stash_apply(params.path, params.stash_ref).await?; + Ok(Json(result)) +} + +pub async fn git_stash_drop( + Json(params): Json, +) -> Result, AppCommandError> { + let result = + folder_commands::git_stash_drop(params.path, params.stash_ref).await?; + Ok(Json(result)) +} + +pub async fn git_stash_clear( + Json(params): Json, +) -> Result, AppCommandError> { + let result = folder_commands::git_stash_clear(params.path).await?; + Ok(Json(result)) +} + +pub async fn git_stash_show( + Json(params): Json, +) -> Result>, AppCommandError> { + let result = + folder_commands::git_stash_show(params.path, params.stash_ref).await?; + Ok(Json(result)) +} + +pub async fn git_is_tracked( + Json(params): Json, +) -> Result, AppCommandError> { + let result = + folder_commands::git_is_tracked(params.path, params.file).await?; + Ok(Json(result)) +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GitDiffWithBranchParams { + pub path: String, + pub branch: String, + pub file: Option, +} + +pub async fn git_diff_with_branch( + Json(params): Json, +) -> Result, AppCommandError> { + let result = folder_commands::git_diff_with_branch( + params.path, + params.branch, + params.file, + ) + .await?; + Ok(Json(result)) +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GitShowDiffParams { + pub path: String, + pub commit: String, + pub file: Option, +} + +pub async fn git_show_diff( + Json(params): Json, +) -> Result, AppCommandError> { + let result = + folder_commands::git_show_diff(params.path, params.commit, params.file) + .await?; + Ok(Json(result)) +} + +pub async fn git_rollback_file( + Json(params): Json, +) -> Result, AppCommandError> { + folder_commands::git_rollback_file(params.path, params.file).await?; + Ok(Json(())) +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GitAddFilesParams { + pub path: String, + pub files: Vec, +} + +pub async fn git_add_files( + Json(params): Json, +) -> Result, AppCommandError> { + folder_commands::git_add_files(params.path, params.files).await?; + Ok(Json(())) +} + +pub async fn git_add_remote( + Json(params): Json, +) -> Result, AppCommandError> { + folder_commands::git_add_remote(params.path, params.name, params.url).await?; + Ok(Json(())) +} + +pub async fn git_remove_remote( + Json(params): Json, +) -> Result, AppCommandError> { + folder_commands::git_remove_remote(params.path, params.name).await?; + Ok(Json(())) +} + +pub async fn git_set_remote_url( + Json(params): Json, +) -> Result, AppCommandError> { + folder_commands::git_set_remote_url(params.path, params.name, params.url).await?; + Ok(Json(())) +} + +pub async fn git_merge( + Json(params): Json, +) -> Result, AppCommandError> { + let result = + folder_commands::git_merge(params.path, params.branch_name).await?; + Ok(Json(result)) +} + +pub async fn git_rebase( + Json(params): Json, +) -> Result, AppCommandError> { + let result = + folder_commands::git_rebase(params.path, params.branch_name).await?; + Ok(Json(result)) +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GitDeleteBranchParams { + pub path: String, + pub branch_name: String, + pub force: bool, +} + +pub async fn git_delete_branch( + Json(params): Json, +) -> Result, AppCommandError> { + folder_commands::git_delete_branch(params.path, params.branch_name, params.force) + .await?; + Ok(Json(())) +} + +pub async fn git_list_conflicts( + Json(params): Json, +) -> Result>, AppCommandError> { + let result = folder_commands::git_list_conflicts(params.path).await?; + Ok(Json(result)) +} + +pub async fn git_conflict_file_versions( + Json(params): Json, +) -> Result, AppCommandError> { + let result = + folder_commands::git_conflict_file_versions(params.path, params.file).await?; + Ok(Json(result)) +} + +pub async fn git_resolve_conflict( + Json(params): Json, +) -> Result, AppCommandError> { + folder_commands::git_resolve_conflict(params.path, params.file, params.content) + .await?; + Ok(Json(())) +} + +pub async fn git_abort_operation( + Json(params): Json, +) -> Result, AppCommandError> { + folder_commands::git_abort_operation(params.path, params.operation).await?; + Ok(Json(())) +} + +pub async fn git_continue_operation( + Json(params): Json, +) -> Result, AppCommandError> { + folder_commands::git_continue_operation(params.path, params.operation).await?; + Ok(Json(())) +} + +// --------------------------------------------------------------------------- +// Remote git handlers (Pattern B – need AppHandle for DB access) +// --------------------------------------------------------------------------- + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GitPullParams { + pub path: String, + pub credentials: Option, +} + +pub async fn git_pull( + Extension(app): Extension, + Json(params): Json, +) -> Result, AppCommandError> { + let db = app.state::(); + let result = folder_commands::git_pull_core( + ¶ms.path, + params.credentials.as_ref(), + &db, + &app, + ) + .await?; + Ok(Json(result)) +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GitFetchParams { + pub path: String, + pub credentials: Option, +} + +pub async fn git_fetch( + Extension(app): Extension, + Json(params): Json, +) -> Result, AppCommandError> { + let db = app.state::(); + let result = folder_commands::git_fetch_core( + ¶ms.path, + params.credentials.as_ref(), + &db, + &app, + ) + .await?; + Ok(Json(result)) +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GitPushParams { + pub folder_id: Option, + pub path: String, + pub remote: Option, + pub credentials: Option, +} + +pub async fn git_push( + Extension(app): Extension, + Json(params): Json, +) -> Result, AppCommandError> { + let db = app.state::(); + let result = folder_commands::git_push_core( + &app, + params.folder_id, + ¶ms.path, + params.remote.as_deref(), + params.credentials.as_ref(), + &db, + ) + .await?; + Ok(Json(result)) +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GitCommitParams { + pub folder_id: Option, + pub path: String, + pub message: String, + pub files: Vec, +} + +pub async fn git_commit( + Extension(app): Extension, + Json(params): Json, +) -> Result, AppCommandError> { + let db = app.state::(); + let result = folder_commands::git_commit_core( + &app, + params.folder_id, + &db.conn, + ¶ms.path, + ¶ms.message, + ¶ms.files, + ) + .await?; + Ok(Json(result)) +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GitFetchRemoteParams { + pub path: String, + pub name: String, + pub credentials: Option, +} + +pub async fn git_fetch_remote( + Extension(app): Extension, + Json(params): Json, +) -> Result, AppCommandError> { + let db = app.state::(); + let result = folder_commands::git_fetch_remote_core( + ¶ms.path, + ¶ms.name, + params.credentials.as_ref(), + &db, + &app, + ) + .await?; + Ok(Json(result)) +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CloneRepositoryParams { + pub url: String, + pub target_dir: String, + pub credentials: Option, +} + +pub async fn clone_repository( + Extension(app): Extension, + Json(params): Json, +) -> Result, AppCommandError> { + let db = app.state::(); + folder_commands::clone_repository_core( + ¶ms.url, + ¶ms.target_dir, + params.credentials.as_ref(), + &db, + &app, + ) + .await?; + Ok(Json(())) +} diff --git a/src-tauri/src/web/handlers/mcp.rs b/src-tauri/src/web/handlers/mcp.rs index bd65434..e43f3ae 100644 --- a/src-tauri/src/web/handlers/mcp.rs +++ b/src-tauri/src/web/handlers/mcp.rs @@ -1,2 +1,145 @@ -// MCP (Model Context Protocol) web handlers. -// TODO: Implement MCP marketplace and server management handlers for web mode. +use axum::Json; +use serde::Deserialize; +use serde_json::Value; + +use crate::app_error::AppCommandError; +use crate::commands::mcp as mcp_commands; +use crate::commands::mcp::{ + LocalMcpServer, McpAppType, McpMarketplaceItem, McpMarketplaceProvider, + McpMarketplaceServerDetail, +}; + +// --------------------------------------------------------------------------- +// Param structs +// --------------------------------------------------------------------------- + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SearchMarketplaceParams { + pub provider_id: String, + pub query: Option, + pub limit: Option, +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GetMarketplaceServerDetailParams { + pub provider_id: String, + pub server_id: String, +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct InstallFromMarketplaceParams { + pub provider_id: String, + pub server_id: String, + pub apps: Vec, + pub spec_override: Option, + pub option_id: Option, + pub protocol: Option, + pub parameter_values: Option, +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct UpsertLocalServerParams { + pub server_id: String, + pub spec: Value, + pub apps: Vec, +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SetServerAppsParams { + pub server_id: String, + pub apps: Vec, +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RemoveServerParams { + pub server_id: String, + pub apps: Option>, +} + +// --------------------------------------------------------------------------- +// Handlers +// --------------------------------------------------------------------------- + +pub async fn mcp_scan_local() -> Result>, AppCommandError> { + let result = mcp_commands::mcp_scan_local().await?; + Ok(Json(result)) +} + +pub async fn mcp_list_marketplaces( +) -> Result>, AppCommandError> { + let result = mcp_commands::mcp_list_marketplaces().await?; + Ok(Json(result)) +} + +pub async fn mcp_search_marketplace( + Json(params): Json, +) -> Result>, AppCommandError> { + let result = mcp_commands::mcp_search_marketplace( + params.provider_id, + params.query, + params.limit, + ) + .await?; + Ok(Json(result)) +} + +pub async fn mcp_get_marketplace_server_detail( + Json(params): Json, +) -> Result, AppCommandError> { + let result = mcp_commands::mcp_get_marketplace_server_detail( + params.provider_id, + params.server_id, + ) + .await?; + Ok(Json(result)) +} + +pub async fn mcp_install_from_marketplace( + Json(params): Json, +) -> Result, AppCommandError> { + let result = mcp_commands::mcp_install_from_marketplace( + params.provider_id, + params.server_id, + params.apps, + params.spec_override, + params.option_id, + params.protocol, + params.parameter_values, + ) + .await?; + Ok(Json(result)) +} + +pub async fn mcp_upsert_local_server( + Json(params): Json, +) -> Result, AppCommandError> { + let result = mcp_commands::mcp_upsert_local_server( + params.server_id, + params.spec, + params.apps, + ) + .await?; + Ok(Json(result)) +} + +pub async fn mcp_set_server_apps( + Json(params): Json, +) -> Result>, AppCommandError> { + let result = + mcp_commands::mcp_set_server_apps(params.server_id, params.apps).await?; + Ok(Json(result)) +} + +pub async fn mcp_remove_server( + Json(params): Json, +) -> Result, AppCommandError> { + let result = + mcp_commands::mcp_remove_server(params.server_id, params.apps).await?; + Ok(Json(result)) +} diff --git a/src-tauri/src/web/handlers/mod.rs b/src-tauri/src/web/handlers/mod.rs index 19d7a2f..60a754d 100644 --- a/src-tauri/src/web/handlers/mod.rs +++ b/src-tauri/src/web/handlers/mod.rs @@ -1,5 +1,6 @@ mod error; pub mod conversations; +pub mod files; pub mod folders; pub mod acp; pub mod terminal; @@ -7,3 +8,4 @@ pub mod system_settings; pub mod version_control; pub mod folder_commands; pub mod mcp; +pub mod git; diff --git a/src-tauri/src/web/handlers/system_settings.rs b/src-tauri/src/web/handlers/system_settings.rs index dc0d317..17a6fa4 100644 --- a/src-tauri/src/web/handlers/system_settings.rs +++ b/src-tauri/src/web/handlers/system_settings.rs @@ -2,24 +2,25 @@ use axum::{extract::Extension, Json}; use tauri::Manager; use crate::app_error::AppCommandError; +use crate::commands::system_settings as settings_commands; use crate::db::service::app_metadata_service; use crate::db::AppDatabase; use crate::models::*; +use crate::network::proxy; const SYSTEM_PROXY_SETTINGS_KEY: &str = "system_proxy_settings"; const SYSTEM_LANGUAGE_SETTINGS_KEY: &str = "system_language_settings"; +const LANGUAGE_SETTINGS_UPDATED_EVENT: &str = "app://language-settings-updated"; + +// --------------------------------------------------------------------------- +// Read handlers +// --------------------------------------------------------------------------- pub async fn get_system_proxy_settings( Extension(app): Extension, ) -> Result, AppCommandError> { let db = app.state::(); - let raw = app_metadata_service::get_value(&db.conn, SYSTEM_PROXY_SETTINGS_KEY) - .await - .map_err(AppCommandError::from)?; - - let settings = raw - .and_then(|v| serde_json::from_str::(&v).ok()) - .unwrap_or_default(); + let settings = settings_commands::load_system_proxy_settings(&db.conn).await?; Ok(Json(settings)) } @@ -27,12 +28,64 @@ pub async fn get_system_language_settings( Extension(app): Extension, ) -> Result, AppCommandError> { let db = app.state::(); - let raw = app_metadata_service::get_value(&db.conn, SYSTEM_LANGUAGE_SETTINGS_KEY) - .await - .map_err(AppCommandError::from)?; - - let settings = raw - .and_then(|v| serde_json::from_str::(&v).ok()) - .unwrap_or_default(); + let settings = + settings_commands::load_system_language_settings(&db.conn).await?; + Ok(Json(settings)) +} + +// --------------------------------------------------------------------------- +// Update handlers +// --------------------------------------------------------------------------- + +pub async fn update_system_proxy_settings( + Extension(app): Extension, + Json(settings): Json, +) -> Result, AppCommandError> { + let db = app.state::(); + + // TODO: call normalize_proxy_settings once it is made pub(crate) in + // commands/system_settings.rs. For now the frontend validates the URL. + let serialized = serde_json::to_string(&settings).map_err(|e| { + AppCommandError::invalid_input("Failed to serialize proxy settings") + .with_detail(e.to_string()) + })?; + + app_metadata_service::upsert_value( + &db.conn, + SYSTEM_PROXY_SETTINGS_KEY, + &serialized, + ) + .await + .map_err(AppCommandError::from)?; + + proxy::apply_system_proxy_settings(&settings)?; + Ok(Json(settings)) +} + +pub async fn update_system_language_settings( + Extension(app): Extension, + Json(settings): Json, +) -> Result, AppCommandError> { + let db = app.state::(); + + let serialized = serde_json::to_string(&settings).map_err(|e| { + AppCommandError::invalid_input("Failed to serialize language settings") + .with_detail(e.to_string()) + })?; + + app_metadata_service::upsert_value( + &db.conn, + SYSTEM_LANGUAGE_SETTINGS_KEY, + &serialized, + ) + .await + .map_err(AppCommandError::from)?; + + crate::web::event_bridge::emit_event( + &app, + LANGUAGE_SETTINGS_UPDATED_EVENT, + settings.clone(), + ); + Ok(Json(settings)) } diff --git a/src-tauri/src/web/handlers/terminal.rs b/src-tauri/src/web/handlers/terminal.rs index 9e32479..683cdcd 100644 --- a/src-tauri/src/web/handlers/terminal.rs +++ b/src-tauri/src/web/handlers/terminal.rs @@ -5,6 +5,11 @@ use tauri::Manager; use crate::app_error::AppCommandError; use crate::commands::terminal::prepare_credential_env; use crate::terminal::manager::{SpawnOptions, TerminalManager}; +use crate::terminal::types::TerminalInfo; + +// --------------------------------------------------------------------------- +// Param structs +// --------------------------------------------------------------------------- #[derive(Deserialize)] #[serde(rename_all = "camelCase")] @@ -13,6 +18,31 @@ pub struct TerminalSpawnParams { pub initial_command: Option, } +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct TerminalIdParams { + pub terminal_id: String, +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct TerminalWriteParams { + pub terminal_id: String, + pub data: String, +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct TerminalResizeParams { + pub terminal_id: String, + pub cols: u16, + pub rows: u16, +} + +// --------------------------------------------------------------------------- +// Handlers +// --------------------------------------------------------------------------- + pub async fn terminal_spawn( Extension(app): Extension, Json(params): Json, @@ -43,3 +73,44 @@ pub async fn terminal_spawn( Ok(Json(id)) } + +pub async fn terminal_write( + Extension(app): Extension, + Json(params): Json, +) -> Result, AppCommandError> { + let manager = app.state::(); + manager + .write(¶ms.terminal_id, params.data.as_bytes()) + .map_err(|e| AppCommandError::task_execution_failed(e.to_string()))?; + Ok(Json(())) +} + +pub async fn terminal_resize( + Extension(app): Extension, + Json(params): Json, +) -> Result, AppCommandError> { + let manager = app.state::(); + manager + .resize(¶ms.terminal_id, params.cols, params.rows) + .map_err(|e| AppCommandError::task_execution_failed(e.to_string()))?; + Ok(Json(())) +} + +pub async fn terminal_kill( + Extension(app): Extension, + Json(params): Json, +) -> Result, AppCommandError> { + let manager = app.state::(); + manager + .kill(¶ms.terminal_id) + .map_err(|e| AppCommandError::task_execution_failed(e.to_string()))?; + Ok(Json(())) +} + +pub async fn terminal_list( + Extension(app): Extension, +) -> Result>, AppCommandError> { + let manager = app.state::(); + let result = manager.list_with_exit_check(Some(&app)); + Ok(Json(result)) +} diff --git a/src-tauri/src/web/handlers/version_control.rs b/src-tauri/src/web/handlers/version_control.rs index f7ff1c6..41604cb 100644 --- a/src-tauri/src/web/handlers/version_control.rs +++ b/src-tauri/src/web/handlers/version_control.rs @@ -1,22 +1,178 @@ -use axum::Json; +use axum::{extract::Extension, Json}; use serde::Deserialize; +use tauri::Manager; use crate::app_error::AppCommandError; -use crate::commands::folders as folder_commands; +use crate::commands::version_control as vc_commands; +use crate::db::service::app_metadata_service; +use crate::db::AppDatabase; +use crate::models::*; + +const GIT_SETTINGS_KEY: &str = "git_settings"; +const GITHUB_ACCOUNTS_KEY: &str = "github_accounts"; + +// --------------------------------------------------------------------------- +// Param structs +// --------------------------------------------------------------------------- #[derive(Deserialize)] #[serde(rename_all = "camelCase")] -pub struct GitLogParams { +pub struct TestGitPathParams { pub path: String, - pub limit: Option, - pub branch: Option, - pub remote: Option, } -pub async fn git_log( - Json(params): Json, -) -> Result, AppCommandError> { - let result = - folder_commands::git_log(params.path, params.limit, params.branch, params.remote).await?; +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ValidateGitHubTokenParams { + pub server_url: String, + pub token: String, +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AccountIdParams { + pub account_id: String, +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SaveAccountTokenParams { + pub account_id: String, + pub token: String, +} + +// --------------------------------------------------------------------------- +// Git detection +// --------------------------------------------------------------------------- + +pub async fn detect_git( + Extension(app): Extension, +) -> Result, AppCommandError> { + let db = app.state::(); + let result = vc_commands::detect_git_core(&db.conn).await?; Ok(Json(result)) } + +pub async fn test_git_path( + Json(params): Json, +) -> Result, AppCommandError> { + let result = vc_commands::test_git_path(params.path).await?; + Ok(Json(result)) +} + +// --------------------------------------------------------------------------- +// Git settings +// --------------------------------------------------------------------------- + +pub async fn get_git_settings( + Extension(app): Extension, +) -> Result, AppCommandError> { + let db = app.state::(); + let raw = app_metadata_service::get_value(&db.conn, GIT_SETTINGS_KEY) + .await + .map_err(AppCommandError::from)?; + + let settings = match raw { + Some(raw) => serde_json::from_str::(&raw).map_err(|e| { + AppCommandError::configuration_invalid("Failed to parse stored git settings") + .with_detail(e.to_string()) + })?, + None => GitSettings::default(), + }; + Ok(Json(settings)) +} + +pub async fn update_git_settings( + Extension(app): Extension, + Json(settings): Json, +) -> Result, AppCommandError> { + let db = app.state::(); + let serialized = serde_json::to_string(&settings).map_err(|e| { + AppCommandError::invalid_input("Failed to serialize git settings") + .with_detail(e.to_string()) + })?; + + app_metadata_service::upsert_value(&db.conn, GIT_SETTINGS_KEY, &serialized) + .await + .map_err(AppCommandError::from)?; + + Ok(Json(settings)) +} + +// --------------------------------------------------------------------------- +// GitHub accounts +// --------------------------------------------------------------------------- + +pub async fn get_github_accounts( + Extension(app): Extension, +) -> Result, AppCommandError> { + let db = app.state::(); + let raw = app_metadata_service::get_value(&db.conn, GITHUB_ACCOUNTS_KEY) + .await + .map_err(AppCommandError::from)?; + + let settings = match raw { + Some(raw) => serde_json::from_str::(&raw).map_err(|e| { + AppCommandError::configuration_invalid( + "Failed to parse stored GitHub accounts", + ) + .with_detail(e.to_string()) + })?, + None => GitHubAccountsSettings::default(), + }; + Ok(Json(settings)) +} + +pub async fn update_github_accounts( + Extension(app): Extension, + Json(settings): Json, +) -> Result, AppCommandError> { + let db = app.state::(); + let serialized = serde_json::to_string(&settings).map_err(|e| { + AppCommandError::invalid_input("Failed to serialize GitHub accounts") + .with_detail(e.to_string()) + })?; + + app_metadata_service::upsert_value(&db.conn, GITHUB_ACCOUNTS_KEY, &serialized) + .await + .map_err(AppCommandError::from)?; + + Ok(Json(settings)) +} + +// --------------------------------------------------------------------------- +// GitHub token validation +// --------------------------------------------------------------------------- + +pub async fn validate_github_token( + Json(params): Json, +) -> Result, AppCommandError> { + let result = + vc_commands::validate_github_token(params.server_url, params.token).await?; + Ok(Json(result)) +} + +// --------------------------------------------------------------------------- +// Keyring token management +// --------------------------------------------------------------------------- + +pub async fn save_account_token( + Json(params): Json, +) -> Result, AppCommandError> { + vc_commands::save_account_token(params.account_id, params.token).await?; + Ok(Json(())) +} + +pub async fn get_account_token( + Json(params): Json, +) -> Result>, AppCommandError> { + let result = vc_commands::get_account_token(params.account_id).await?; + Ok(Json(result)) +} + +pub async fn delete_account_token( + Json(params): Json, +) -> Result, AppCommandError> { + vc_commands::delete_account_token(params.account_id).await?; + Ok(Json(())) +} diff --git a/src-tauri/src/web/router.rs b/src-tauri/src/web/router.rs index fb4f4b4..fc02e1e 100644 --- a/src-tauri/src/web/router.rs +++ b/src-tauri/src/web/router.rs @@ -20,9 +20,8 @@ pub fn build_router(app: tauri::AppHandle, token: String, static_dir: std::path: let token_for_ws = token.clone(); let api = Router::new() - // Health check (lightweight, used for token validation) .route("/health", post(health_check)) - // Conversations + // ─── Conversations ─── .route("/list_conversations", post(handlers::conversations::list_conversations)) .route("/get_conversation", post(handlers::conversations::get_conversation)) .route("/list_folder_conversations", post(handlers::conversations::list_folder_conversations)) @@ -36,45 +35,143 @@ pub fn build_router(app: tauri::AppHandle, token: String, static_dir: std::path: .route("/update_conversation_title", post(handlers::conversations::update_conversation_title)) .route("/delete_conversation", post(handlers::conversations::delete_conversation)) .route("/update_conversation_external_id", post(handlers::conversations::update_conversation_external_id)) - // Folders + // ─── Folders ─── .route("/load_folder_history", post(handlers::folders::load_folder_history)) .route("/get_folder", post(handlers::folders::get_folder)) .route("/open_folder_window", post(handlers::folders::open_folder_window)) - // System settings - .route("/get_system_proxy_settings", post(handlers::system_settings::get_system_proxy_settings)) - .route("/get_system_language_settings", post(handlers::system_settings::get_system_language_settings)) - // Folders (extended) + .route("/add_folder_to_history", post(handlers::folders::add_folder_to_history)) + .route("/set_folder_parent_branch", post(handlers::folders::set_folder_parent_branch)) + .route("/remove_folder_from_history", post(handlers::folders::remove_folder_from_history)) + .route("/create_folder_directory", post(handlers::folders::create_folder_directory)) .route("/save_folder_opened_conversations", post(handlers::folders::save_folder_opened_conversations)) .route("/get_git_branch", post(handlers::folders::get_git_branch)) .route("/get_file_tree", post(handlers::folders::get_file_tree)) .route("/start_file_tree_watch", post(handlers::folders::start_file_tree_watch)) .route("/stop_file_tree_watch", post(handlers::folders::stop_file_tree_watch)) + // ─── Window navigation ─── .route("/open_settings_window", post(handlers::folders::open_settings_window)) - // Version control - .route("/git_log", post(handlers::version_control::git_log)) - // Folder commands - .route("/list_folder_commands", post(handlers::folder_commands::list_folder_commands)) - // Git operations - .route("/git_status", post(handlers::folders::git_status)) - .route("/git_list_all_branches", post(handlers::folders::git_list_all_branches)) - .route("/git_commit_branches", post(handlers::folders::git_commit_branches)) - .route("/git_show_file", post(handlers::folders::git_show_file)) - .route("/git_diff", post(handlers::folders::git_diff)) - .route("/git_list_remotes", post(handlers::folders::git_list_remotes)) .route("/open_commit_window", post(handlers::folders::open_commit_window)) - // File operations - .route("/read_file_preview", post(handlers::folders::read_file_preview)) - // ACP + .route("/open_merge_window", post(handlers::folders::open_merge_window)) + .route("/open_stash_window", post(handlers::folders::open_stash_window)) + .route("/open_push_window", post(handlers::folders::open_push_window)) + // ─── Git (pure) ─── + .route("/git_status", post(handlers::git::git_status)) + .route("/git_init", post(handlers::git::git_init)) + .route("/git_log", post(handlers::git::git_log)) + .route("/git_list_all_branches", post(handlers::git::git_list_all_branches)) + .route("/git_list_branches", post(handlers::git::git_list_branches)) + .route("/git_commit_branches", post(handlers::git::git_commit_branches)) + .route("/git_show_file", post(handlers::git::git_show_file)) + .route("/git_diff", post(handlers::git::git_diff)) + .route("/git_diff_with_branch", post(handlers::git::git_diff_with_branch)) + .route("/git_show_diff", post(handlers::git::git_show_diff)) + .route("/git_list_remotes", post(handlers::git::git_list_remotes)) + .route("/git_add_remote", post(handlers::git::git_add_remote)) + .route("/git_remove_remote", post(handlers::git::git_remove_remote)) + .route("/git_set_remote_url", post(handlers::git::git_set_remote_url)) + .route("/git_new_branch", post(handlers::git::git_new_branch)) + .route("/git_checkout", post(handlers::git::git_checkout)) + .route("/git_delete_branch", post(handlers::git::git_delete_branch)) + .route("/git_merge", post(handlers::git::git_merge)) + .route("/git_rebase", post(handlers::git::git_rebase)) + .route("/git_worktree_add", post(handlers::git::git_worktree_add)) + .route("/git_push_info", post(handlers::git::git_push_info)) + .route("/git_start_pull_merge", post(handlers::git::git_start_pull_merge)) + .route("/git_has_merge_head", post(handlers::git::git_has_merge_head)) + .route("/git_is_tracked", post(handlers::git::git_is_tracked)) + .route("/git_rollback_file", post(handlers::git::git_rollback_file)) + .route("/git_add_files", post(handlers::git::git_add_files)) + .route("/git_list_conflicts", post(handlers::git::git_list_conflicts)) + .route("/git_conflict_file_versions", post(handlers::git::git_conflict_file_versions)) + .route("/git_resolve_conflict", post(handlers::git::git_resolve_conflict)) + .route("/git_abort_operation", post(handlers::git::git_abort_operation)) + .route("/git_continue_operation", post(handlers::git::git_continue_operation)) + .route("/git_stash_push", post(handlers::git::git_stash_push)) + .route("/git_stash_pop", post(handlers::git::git_stash_pop)) + .route("/git_stash_list", post(handlers::git::git_stash_list)) + .route("/git_stash_apply", post(handlers::git::git_stash_apply)) + .route("/git_stash_drop", post(handlers::git::git_stash_drop)) + .route("/git_stash_clear", post(handlers::git::git_stash_clear)) + .route("/git_stash_show", post(handlers::git::git_stash_show)) + // ─── Git (remote) ─── + .route("/git_pull", post(handlers::git::git_pull)) + .route("/git_push", post(handlers::git::git_push)) + .route("/git_fetch", post(handlers::git::git_fetch)) + .route("/git_commit", post(handlers::git::git_commit)) + .route("/git_fetch_remote", post(handlers::git::git_fetch_remote)) + .route("/clone_repository", post(handlers::git::clone_repository)) + // ─── Files ─── + .route("/read_file_preview", post(handlers::files::read_file_preview)) + .route("/read_file_base64", post(handlers::files::read_file_base64)) + .route("/read_file_for_edit", post(handlers::files::read_file_for_edit)) + .route("/save_file_content", post(handlers::files::save_file_content)) + .route("/save_file_copy", post(handlers::files::save_file_copy)) + .route("/rename_file_tree_entry", post(handlers::files::rename_file_tree_entry)) + .route("/delete_file_tree_entry", post(handlers::files::delete_file_tree_entry)) + .route("/create_file_tree_entry", post(handlers::files::create_file_tree_entry)) + // ─── Folder commands ─── + .route("/list_folder_commands", post(handlers::folder_commands::list_folder_commands)) + .route("/create_folder_command", post(handlers::folder_commands::create_folder_command)) + .route("/update_folder_command", post(handlers::folder_commands::update_folder_command)) + .route("/delete_folder_command", post(handlers::folder_commands::delete_folder_command)) + .route("/reorder_folder_commands", post(handlers::folder_commands::reorder_folder_commands)) + // ─── MCP ─── + .route("/mcp_scan_local", post(handlers::mcp::mcp_scan_local)) + .route("/mcp_list_marketplaces", post(handlers::mcp::mcp_list_marketplaces)) + .route("/mcp_search_marketplace", post(handlers::mcp::mcp_search_marketplace)) + .route("/mcp_get_marketplace_server_detail", post(handlers::mcp::mcp_get_marketplace_server_detail)) + .route("/mcp_install_from_marketplace", post(handlers::mcp::mcp_install_from_marketplace)) + .route("/mcp_upsert_local_server", post(handlers::mcp::mcp_upsert_local_server)) + .route("/mcp_set_server_apps", post(handlers::mcp::mcp_set_server_apps)) + .route("/mcp_remove_server", post(handlers::mcp::mcp_remove_server)) + // ─── Version control settings ─── + .route("/detect_git", post(handlers::version_control::detect_git)) + .route("/test_git_path", post(handlers::version_control::test_git_path)) + .route("/get_git_settings", post(handlers::version_control::get_git_settings)) + .route("/update_git_settings", post(handlers::version_control::update_git_settings)) + .route("/get_github_accounts", post(handlers::version_control::get_github_accounts)) + .route("/update_github_accounts", post(handlers::version_control::update_github_accounts)) + .route("/validate_github_token", post(handlers::version_control::validate_github_token)) + .route("/save_account_token", post(handlers::version_control::save_account_token)) + .route("/get_account_token", post(handlers::version_control::get_account_token)) + .route("/delete_account_token", post(handlers::version_control::delete_account_token)) + // ─── System settings ─── + .route("/get_system_proxy_settings", post(handlers::system_settings::get_system_proxy_settings)) + .route("/get_system_language_settings", post(handlers::system_settings::get_system_language_settings)) + .route("/update_system_proxy_settings", post(handlers::system_settings::update_system_proxy_settings)) + .route("/update_system_language_settings", post(handlers::system_settings::update_system_language_settings)) + // ─── ACP ─── .route("/acp_get_agent_status", post(handlers::acp::acp_get_agent_status)) .route("/acp_list_agents", post(handlers::acp::acp_list_agents)) .route("/acp_connect", post(handlers::acp::acp_connect)) .route("/acp_disconnect", post(handlers::acp::acp_disconnect)) .route("/acp_prompt", post(handlers::acp::acp_prompt)) - // Terminal + .route("/acp_preflight", post(handlers::acp::acp_preflight)) + .route("/acp_set_mode", post(handlers::acp::acp_set_mode)) + .route("/acp_set_config_option", post(handlers::acp::acp_set_config_option)) + .route("/acp_cancel", post(handlers::acp::acp_cancel)) + .route("/acp_fork", post(handlers::acp::acp_fork)) + .route("/acp_respond_permission", post(handlers::acp::acp_respond_permission)) + .route("/acp_list_connections", post(handlers::acp::acp_list_connections)) + .route("/acp_clear_binary_cache", post(handlers::acp::acp_clear_binary_cache)) + .route("/acp_update_agent_preferences", post(handlers::acp::acp_update_agent_preferences)) + .route("/acp_download_agent_binary", post(handlers::acp::acp_download_agent_binary)) + .route("/acp_detect_agent_local_version", post(handlers::acp::acp_detect_agent_local_version)) + .route("/acp_prepare_npx_agent", post(handlers::acp::acp_prepare_npx_agent)) + .route("/acp_uninstall_agent", post(handlers::acp::acp_uninstall_agent)) + .route("/acp_reorder_agents", post(handlers::acp::acp_reorder_agents)) + .route("/acp_list_agent_skills", post(handlers::acp::acp_list_agent_skills)) + .route("/acp_read_agent_skill", post(handlers::acp::acp_read_agent_skill)) + .route("/acp_save_agent_skill", post(handlers::acp::acp_save_agent_skill)) + .route("/acp_delete_agent_skill", post(handlers::acp::acp_delete_agent_skill)) + // ─── Terminal ─── .route("/terminal_spawn", post(handlers::terminal::terminal_spawn)) - // Catch-all: return proper JSON 404 for unimplemented API endpoints + .route("/terminal_write", post(handlers::terminal::terminal_write)) + .route("/terminal_resize", post(handlers::terminal::terminal_resize)) + .route("/terminal_kill", post(handlers::terminal::terminal_kill)) + .route("/terminal_list", post(handlers::terminal::terminal_list)) + // Catch-all .fallback(api_not_found) - // Auth middleware for API routes .layer(middleware::from_fn(move |req, next| { auth::require_token(req, next, token.clone()) }));