完善web缺失的端点

This commit is contained in:
xintaofei
2026-03-25 17:13:24 +08:00
parent 8c67bb60f4
commit b330a4f936
14 changed files with 2131 additions and 279 deletions

View File

@@ -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<String, String>,
@@ -1349,8 +1348,8 @@ pub async fn acp_update_agent_preferences(
opencode_auth_json: Option<String>,
codex_auth_json: Option<String>,
codex_config_toml: Option<String>,
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<String, String>,
config_json: Option<String>,
opencode_auth_json: Option<String>,
codex_auth_json: Option<String>,
codex_config_toml: Option<String>,
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<Option<String>, 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<Option<String>, 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<String>,
db: State<'_, AppDatabase>,
app: tauri::AppHandle,
db: &AppDatabase,
app: &tauri::AppHandle,
) -> Result<String, AcpError> {
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<String>,
db: State<'_, AppDatabase>,
app: tauri::AppHandle,
) -> Result<String, AcpError> {
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<AgentType>,
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<AgentType>,
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,

View File

@@ -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<String>,
) -> 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<String>,
) -> 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<GitCredentials>,
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<GitCredentials>,
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<GitCredentials>,
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<GitPullResult, AppCommandError> {
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<GitCredentials>,
db: tauri::State<'_, AppDatabase>,
app_handle: tauri::AppHandle,
) -> Result<GitPullResult, AppCommandError> {
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<bool, AppCommandError> {
Ok(output.status.success())
}
#[tauri::command]
pub async fn git_fetch(
path: String,
credentials: Option<GitCredentials>,
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<String, AppCommandError> {
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<GitCredentials>,
db: tauri::State<'_, AppDatabase>,
app_handle: tauri::AppHandle,
) -> Result<String, AppCommandError> {
git_fetch_core(&path, credentials.as_ref(), &db, &app_handle).await
}
#[tauri::command]
pub async fn git_push_info(path: String) -> Result<GitPushInfo, AppCommandError> {
// Get current branch name
@@ -911,24 +940,23 @@ pub async fn git_push_info(path: String) -> Result<GitPushInfo, AppCommandError>
})
}
#[tauri::command]
pub async fn git_push(
app: tauri::AppHandle,
window: tauri::WebviewWindow,
path: String,
remote: Option<String>,
credentials: Option<GitCredentials>,
db: tauri::State<'_, AppDatabase>,
pub(crate) async fn git_push_core(
app: &tauri::AppHandle,
folder_id: Option<i32>,
path: &str,
remote: Option<&str>,
credentials: Option<&GitCredentials>,
db: &AppDatabase,
) -> Result<GitPushResult, AppCommandError> {
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 &current_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::<i32>().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<String>,
credentials: Option<GitCredentials>,
db: tauri::State<'_, AppDatabase>,
) -> Result<GitPushResult, AppCommandError> {
let folder_id = window
.label()
.strip_prefix("push-")
.and_then(|value| value.parse::<i32>().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<String>,
pub(crate) async fn git_commit_core(
app: &tauri::AppHandle,
folder_id: Option<i32>,
conn: &sea_orm::DatabaseConnection,
path: &str,
message: &str,
files: &[String],
) -> Result<GitCommitResult, AppCommandError> {
// 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::<i32>().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<String>,
) -> Result<GitCommitResult, AppCommandError> {
let folder_id = window
.label()
.strip_prefix("commit-")
.and_then(|value| value.parse::<i32>().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<Vec<GitRemote>, AppCommand
Ok(remotes)
}
#[tauri::command]
pub async fn git_fetch_remote(
path: String,
name: String,
credentials: Option<GitCredentials>,
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<String, AppCommandError> {
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<GitCredentials>,
db: tauri::State<'_, AppDatabase>,
app_handle: tauri::AppHandle,
) -> Result<String, AppCommandError> {
git_fetch_remote_core(&path, &name, credentials.as_ref(), &db, &app_handle).await
}
#[tauri::command]
pub async fn git_add_remote(
path: String,

View File

@@ -76,12 +76,10 @@ async fn detect_git_path() -> Option<String> {
}
}
#[tauri::command]
pub async fn detect_git(
db: State<'_, AppDatabase>,
pub(crate) async fn detect_git_core(
conn: &sea_orm::DatabaseConnection,
) -> Result<GitDetectResult, AppCommandError> {
// 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<GitDetectResult, AppCommandError> {
detect_git_core(&db.conn).await
}
#[tauri::command]
pub async fn test_git_path(path: String) -> Result<GitDetectResult, AppCommandError> {
let trimmed = path.trim();

View File

@@ -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<bool>,
}
pub async fn acp_preflight(
Json(params): Json<AcpPreflightParams>,
) -> Result<Json<PreflightResult>, 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<AgentTypeParams>,
) -> Result<Json<()>, 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<String>,
}
pub async fn acp_list_agent_skills(
Json(params): Json<AcpListAgentSkillsParams>,
) -> Result<Json<AgentSkillsListResult>, 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<String>,
}
pub async fn acp_read_agent_skill(
Json(params): Json<AcpReadAgentSkillParams>,
) -> Result<Json<AgentSkillContent>, 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<String>,
pub layout: Option<AgentSkillLayout>,
}
pub async fn acp_save_agent_skill(
Json(params): Json<AcpSaveAgentSkillParams>,
) -> Result<Json<()>, 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<String>,
}
pub async fn acp_delete_agent_skill(
Json(params): Json<AcpDeleteAgentSkillParams>,
) -> Result<Json<()>, 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<tauri::AppHandle>,
Json(params): Json<AcpSetModeParams>,
) -> Result<Json<()>, AppCommandError> {
let manager = app.state::<ConnectionManager>();
manager
.set_mode(&params.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<tauri::AppHandle>,
Json(params): Json<AcpSetConfigOptionParams>,
) -> Result<Json<()>, AppCommandError> {
let manager = app.state::<ConnectionManager>();
manager
.set_config_option(&params.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<tauri::AppHandle>,
Json(params): Json<AcpConnectionIdParams>,
) -> Result<Json<()>, AppCommandError> {
let manager = app.state::<ConnectionManager>();
manager
.cancel(&params.connection_id)
.await
.map_err(|e| AppCommandError::task_execution_failed(e.to_string()))?;
Ok(Json(()))
}
pub async fn acp_fork(
Extension(app): Extension<tauri::AppHandle>,
Json(params): Json<AcpConnectionIdParams>,
) -> Result<Json<ForkResultInfo>, AppCommandError> {
let manager = app.state::<ConnectionManager>();
let result = manager
.fork_session(&params.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<tauri::AppHandle>,
Json(params): Json<AcpRespondPermissionParams>,
) -> Result<Json<()>, AppCommandError> {
let manager = app.state::<ConnectionManager>();
manager
.respond_permission(&params.connection_id, &params.request_id, &params.option_id)
.await
.map_err(|e| AppCommandError::task_execution_failed(e.to_string()))?;
Ok(Json(()))
}
pub async fn acp_list_connections(
Extension(app): Extension<tauri::AppHandle>,
) -> Result<Json<Vec<ConnectionInfo>>, AppCommandError> {
let manager = app.state::<ConnectionManager>();
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<String, String>,
pub config_json: Option<String>,
pub opencode_auth_json: Option<String>,
pub codex_auth_json: Option<String>,
pub codex_config_toml: Option<String>,
}
pub async fn acp_update_agent_preferences(
Extension(app): Extension<tauri::AppHandle>,
Json(params): Json<AcpUpdateAgentPreferencesParams>,
) -> Result<Json<()>, AppCommandError> {
let db = app.state::<AppDatabase>();
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<tauri::AppHandle>,
Json(params): Json<AgentTypeParams>,
) -> Result<Json<()>, 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<tauri::AppHandle>,
Json(params): Json<AgentTypeParams>,
) -> Result<Json<Option<String>>, AppCommandError> {
let db = app.state::<AppDatabase>();
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<String>,
}
pub async fn acp_prepare_npx_agent(
Extension(app): Extension<tauri::AppHandle>,
Json(params): Json<AcpPrepareNpxAgentParams>,
) -> Result<Json<String>, AppCommandError> {
let db = app.state::<AppDatabase>();
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<tauri::AppHandle>,
Json(params): Json<AgentTypeParams>,
) -> Result<Json<()>, AppCommandError> {
let db = app.state::<AppDatabase>();
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<AgentType>,
}
pub async fn acp_reorder_agents(
Extension(app): Extension<tauri::AppHandle>,
Json(params): Json<AcpReorderAgentsParams>,
) -> Result<Json<()>, AppCommandError> {
let db = app.state::<AppDatabase>();
acp_commands::acp_reorder_agents_core(&params.agent_types, &db, &app)
.await
.map_err(|e| AppCommandError::task_execution_failed(e.to_string()))?;
Ok(Json(()))
}

View File

@@ -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<usize>,
}
#[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<String>,
}
#[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<ReadFilePreviewParams>,
) -> Result<Json<folder_commands::FilePreviewContent>, 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<ReadFileBase64Params>,
) -> Result<Json<String>, 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<ReadFileForEditParams>,
) -> Result<Json<folder_commands::FileEditContent>, 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<SaveFileContentParams>,
) -> Result<Json<folder_commands::FileSaveResult>, 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<SaveFileCopyParams>,
) -> Result<Json<folder_commands::FileSaveResult>, 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<RenameFileTreeEntryParams>,
) -> Result<Json<String>, 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<DeleteFileTreeEntryParams>,
) -> Result<Json<()>, 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<CreateFileTreeEntryParams>,
) -> Result<Json<String>, AppCommandError> {
let result = folder_commands::create_file_tree_entry(
params.root_path,
params.path,
params.name,
params.kind,
)
.await?;
Ok(Json(result))
}

View File

@@ -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<String>,
pub command: Option<String>,
pub sort_order: Option<i32>,
}
#[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<i32>,
}
// ---------------------------------------------------------------------------
// Handlers
// ---------------------------------------------------------------------------
pub async fn list_folder_commands(
Extension(app): Extension<tauri::AppHandle>,
Json(params): Json<FolderIdParams>,
@@ -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<tauri::AppHandle>,
Json(params): Json<CreateFolderCommandParams>,
) -> Result<Json<FolderCommandInfo>, AppCommandError> {
let db = app.state::<AppDatabase>();
let result = folder_command_service::create(
&db.conn,
params.folder_id,
&params.name,
&params.command,
)
.await
.map_err(AppCommandError::from)?;
Ok(Json(result))
}
pub async fn update_folder_command(
Extension(app): Extension<tauri::AppHandle>,
Json(params): Json<UpdateFolderCommandParams>,
) -> Result<Json<FolderCommandInfo>, AppCommandError> {
let db = app.state::<AppDatabase>();
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<tauri::AppHandle>,
Json(params): Json<DeleteFolderCommandParams>,
) -> Result<Json<()>, AppCommandError> {
let db = app.state::<AppDatabase>();
folder_command_service::delete(&db.conn, params.id)
.await
.map_err(AppCommandError::from)?;
Ok(Json(()))
}
pub async fn reorder_folder_commands(
Extension(app): Extension<tauri::AppHandle>,
Json(params): Json<ReorderFolderCommandsParams>,
) -> Result<Json<()>, AppCommandError> {
let db = app.state::<AppDatabase>();
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.

View File

@@ -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<bool>,
}
pub async fn git_status(
Json(params): Json<GitStatusParams>,
) -> Result<Json<Vec<folder_commands::GitStatusEntry>>, 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<ReadFilePreviewParams>,
) -> Result<Json<folder_commands::FilePreviewContent>, 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<PathParams>,
) -> Result<Json<folder_commands::GitBranchList>, 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<GitCommitBranchesParams>,
) -> Result<Json<Vec<String>>, 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<String>,
}
pub async fn git_show_file(
Json(params): Json<GitShowFileParams>,
) -> Result<Json<String>, 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<String>,
}
pub async fn git_diff(
Json(params): Json<GitDiffParams>,
) -> Result<Json<String>, AppCommandError> {
let result = folder_commands::git_diff(params.path, params.file).await?;
Ok(Json(result))
}
pub async fn git_list_remotes(
Json(params): Json<PathParams>,
) -> Result<Json<Vec<folder_commands::GitRemote>>, 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<String>,
pub upstream_commit: Option<String>,
}
pub async fn open_merge_window(
Json(params): Json<OpenMergeWindowParams>,
) -> Result<Json<SettingsNavigationResult>, AppCommandError> {
let mut path = format!("/merge?folderId={}", params.folder_id);
if let Some(op) = &params.operation {
path.push_str(&format!("&operation={op}"));
}
if let Some(uc) = &params.upstream_commit {
path.push_str(&format!("&upstreamCommit={uc}"));
}
Ok(Json(SettingsNavigationResult { path }))
}
pub async fn open_stash_window(
Json(params): Json<OpenCommitWindowParams>,
) -> Result<Json<SettingsNavigationResult>, AppCommandError> {
Ok(Json(SettingsNavigationResult {
path: format!("/stash?folderId={}", params.folder_id),
}))
}
pub async fn open_push_window(
Json(params): Json<OpenCommitWindowParams>,
) -> Result<Json<SettingsNavigationResult>, 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<String>,
}
pub async fn add_folder_to_history(
Extension(app): Extension<tauri::AppHandle>,
Json(params): Json<AddFolderParams>,
) -> Result<Json<FolderHistoryEntry>, AppCommandError> {
let db = app.state::<AppDatabase>();
let result = folder_service::add_folder(&db.conn, &params.path)
.await
.map_err(AppCommandError::from)?;
Ok(Json(result))
}
pub async fn set_folder_parent_branch(
Extension(app): Extension<tauri::AppHandle>,
Json(params): Json<SetFolderParentBranchParams>,
) -> Result<Json<()>, AppCommandError> {
let db = app.state::<AppDatabase>();
folder_commands::set_folder_parent_branch_core(&db.conn, &params.path, params.parent_branch)
.await?;
Ok(Json(()))
}
pub async fn remove_folder_from_history(
Extension(app): Extension<tauri::AppHandle>,
Json(params): Json<AddFolderParams>,
) -> Result<Json<()>, AppCommandError> {
let db = app.state::<AppDatabase>();
folder_service::remove_folder(&db.conn, &params.path)
.await
.map_err(AppCommandError::from)?;
Ok(Json(()))
}
pub async fn create_folder_directory(
Json(params): Json<AddFolderParams>,
) -> Result<Json<()>, AppCommandError> {
folder_commands::create_folder_directory(params.path).await?;
Ok(Json(()))
}

View File

@@ -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<bool>,
}
pub async fn git_status(
Json(params): Json<GitStatusParams>,
) -> Result<Json<Vec<folder_commands::GitStatusEntry>>, 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<PathParams>,
) -> Result<Json<folder_commands::GitBranchList>, 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<GitCommitBranchesParams>,
) -> Result<Json<Vec<String>>, 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<String>,
}
pub async fn git_show_file(
Json(params): Json<GitShowFileParams>,
) -> Result<Json<String>, 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<String>,
}
pub async fn git_diff(
Json(params): Json<GitDiffParams>,
) -> Result<Json<String>, AppCommandError> {
let result = folder_commands::git_diff(params.path, params.file).await?;
Ok(Json(result))
}
pub async fn git_list_remotes(
Json(params): Json<PathParams>,
) -> Result<Json<Vec<folder_commands::GitRemote>>, 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<u32>,
pub branch: Option<String>,
pub remote: Option<String>,
}
pub async fn git_log(
Json(params): Json<GitLogParams>,
) -> Result<Json<folder_commands::GitLogResult>, 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<PathParams>,
) -> Result<Json<()>, 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<String>,
}
pub async fn git_start_pull_merge(
Json(params): Json<GitStartPullMergeParams>,
) -> Result<Json<()>, 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<PathParams>,
) -> Result<Json<bool>, AppCommandError> {
let result = folder_commands::git_has_merge_head(params.path).await?;
Ok(Json(result))
}
pub async fn git_push_info(
Json(params): Json<PathParams>,
) -> Result<Json<folder_commands::GitPushInfo>, 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<String>,
}
pub async fn git_new_branch(
Json(params): Json<GitNewBranchParams>,
) -> Result<Json<()>, 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<GitWorktreeAddParams>,
) -> Result<Json<()>, 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<PathBranchParams>,
) -> Result<Json<()>, AppCommandError> {
folder_commands::git_checkout(params.path, params.branch_name).await?;
Ok(Json(()))
}
pub async fn git_list_branches(
Json(params): Json<PathParams>,
) -> Result<Json<Vec<String>>, 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<String>,
pub keep_index: bool,
}
pub async fn git_stash_push(
Json(params): Json<GitStashPushParams>,
) -> Result<Json<String>, 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<String>,
}
pub async fn git_stash_pop(
Json(params): Json<GitStashPopParams>,
) -> Result<Json<String>, 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<PathParams>,
) -> Result<Json<Vec<folder_commands::GitStashEntry>>, AppCommandError> {
let result = folder_commands::git_stash_list(params.path).await?;
Ok(Json(result))
}
pub async fn git_stash_apply(
Json(params): Json<PathStashRefParams>,
) -> Result<Json<String>, 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<PathStashRefParams>,
) -> Result<Json<String>, 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<PathParams>,
) -> Result<Json<String>, AppCommandError> {
let result = folder_commands::git_stash_clear(params.path).await?;
Ok(Json(result))
}
pub async fn git_stash_show(
Json(params): Json<PathStashRefParams>,
) -> Result<Json<Vec<folder_commands::GitStatusEntry>>, 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<PathFileParams>,
) -> Result<Json<bool>, 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<String>,
}
pub async fn git_diff_with_branch(
Json(params): Json<GitDiffWithBranchParams>,
) -> Result<Json<String>, 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<String>,
}
pub async fn git_show_diff(
Json(params): Json<GitShowDiffParams>,
) -> Result<Json<String>, 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<PathFileParams>,
) -> Result<Json<()>, 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<String>,
}
pub async fn git_add_files(
Json(params): Json<GitAddFilesParams>,
) -> Result<Json<()>, AppCommandError> {
folder_commands::git_add_files(params.path, params.files).await?;
Ok(Json(()))
}
pub async fn git_add_remote(
Json(params): Json<PathNameUrlParams>,
) -> Result<Json<()>, AppCommandError> {
folder_commands::git_add_remote(params.path, params.name, params.url).await?;
Ok(Json(()))
}
pub async fn git_remove_remote(
Json(params): Json<PathNameParams>,
) -> Result<Json<()>, AppCommandError> {
folder_commands::git_remove_remote(params.path, params.name).await?;
Ok(Json(()))
}
pub async fn git_set_remote_url(
Json(params): Json<PathNameUrlParams>,
) -> Result<Json<()>, AppCommandError> {
folder_commands::git_set_remote_url(params.path, params.name, params.url).await?;
Ok(Json(()))
}
pub async fn git_merge(
Json(params): Json<PathBranchParams>,
) -> Result<Json<folder_commands::GitMergeResult>, AppCommandError> {
let result =
folder_commands::git_merge(params.path, params.branch_name).await?;
Ok(Json(result))
}
pub async fn git_rebase(
Json(params): Json<PathBranchParams>,
) -> Result<Json<folder_commands::GitRebaseResult>, 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<GitDeleteBranchParams>,
) -> Result<Json<()>, 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<PathParams>,
) -> Result<Json<Vec<String>>, AppCommandError> {
let result = folder_commands::git_list_conflicts(params.path).await?;
Ok(Json(result))
}
pub async fn git_conflict_file_versions(
Json(params): Json<PathFileParams>,
) -> Result<Json<folder_commands::GitConflictFileVersions>, 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<PathFileContentParams>,
) -> Result<Json<()>, AppCommandError> {
folder_commands::git_resolve_conflict(params.path, params.file, params.content)
.await?;
Ok(Json(()))
}
pub async fn git_abort_operation(
Json(params): Json<PathOperationParams>,
) -> Result<Json<()>, AppCommandError> {
folder_commands::git_abort_operation(params.path, params.operation).await?;
Ok(Json(()))
}
pub async fn git_continue_operation(
Json(params): Json<PathOperationParams>,
) -> Result<Json<()>, 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<GitCredentials>,
}
pub async fn git_pull(
Extension(app): Extension<tauri::AppHandle>,
Json(params): Json<GitPullParams>,
) -> Result<Json<folder_commands::GitPullResult>, AppCommandError> {
let db = app.state::<AppDatabase>();
let result = folder_commands::git_pull_core(
&params.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<GitCredentials>,
}
pub async fn git_fetch(
Extension(app): Extension<tauri::AppHandle>,
Json(params): Json<GitFetchParams>,
) -> Result<Json<String>, AppCommandError> {
let db = app.state::<AppDatabase>();
let result = folder_commands::git_fetch_core(
&params.path,
params.credentials.as_ref(),
&db,
&app,
)
.await?;
Ok(Json(result))
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GitPushParams {
pub folder_id: Option<i32>,
pub path: String,
pub remote: Option<String>,
pub credentials: Option<GitCredentials>,
}
pub async fn git_push(
Extension(app): Extension<tauri::AppHandle>,
Json(params): Json<GitPushParams>,
) -> Result<Json<folder_commands::GitPushResult>, AppCommandError> {
let db = app.state::<AppDatabase>();
let result = folder_commands::git_push_core(
&app,
params.folder_id,
&params.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<i32>,
pub path: String,
pub message: String,
pub files: Vec<String>,
}
pub async fn git_commit(
Extension(app): Extension<tauri::AppHandle>,
Json(params): Json<GitCommitParams>,
) -> Result<Json<folder_commands::GitCommitResult>, AppCommandError> {
let db = app.state::<AppDatabase>();
let result = folder_commands::git_commit_core(
&app,
params.folder_id,
&db.conn,
&params.path,
&params.message,
&params.files,
)
.await?;
Ok(Json(result))
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GitFetchRemoteParams {
pub path: String,
pub name: String,
pub credentials: Option<GitCredentials>,
}
pub async fn git_fetch_remote(
Extension(app): Extension<tauri::AppHandle>,
Json(params): Json<GitFetchRemoteParams>,
) -> Result<Json<String>, AppCommandError> {
let db = app.state::<AppDatabase>();
let result = folder_commands::git_fetch_remote_core(
&params.path,
&params.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<GitCredentials>,
}
pub async fn clone_repository(
Extension(app): Extension<tauri::AppHandle>,
Json(params): Json<CloneRepositoryParams>,
) -> Result<Json<()>, AppCommandError> {
let db = app.state::<AppDatabase>();
folder_commands::clone_repository_core(
&params.url,
&params.target_dir,
params.credentials.as_ref(),
&db,
&app,
)
.await?;
Ok(Json(()))
}

View File

@@ -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<String>,
pub limit: Option<u32>,
}
#[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<McpAppType>,
pub spec_override: Option<Value>,
pub option_id: Option<String>,
pub protocol: Option<String>,
pub parameter_values: Option<Value>,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct UpsertLocalServerParams {
pub server_id: String,
pub spec: Value,
pub apps: Vec<McpAppType>,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SetServerAppsParams {
pub server_id: String,
pub apps: Vec<McpAppType>,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RemoveServerParams {
pub server_id: String,
pub apps: Option<Vec<McpAppType>>,
}
// ---------------------------------------------------------------------------
// Handlers
// ---------------------------------------------------------------------------
pub async fn mcp_scan_local() -> Result<Json<Vec<LocalMcpServer>>, AppCommandError> {
let result = mcp_commands::mcp_scan_local().await?;
Ok(Json(result))
}
pub async fn mcp_list_marketplaces(
) -> Result<Json<Vec<McpMarketplaceProvider>>, AppCommandError> {
let result = mcp_commands::mcp_list_marketplaces().await?;
Ok(Json(result))
}
pub async fn mcp_search_marketplace(
Json(params): Json<SearchMarketplaceParams>,
) -> Result<Json<Vec<McpMarketplaceItem>>, 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<GetMarketplaceServerDetailParams>,
) -> Result<Json<McpMarketplaceServerDetail>, 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<InstallFromMarketplaceParams>,
) -> Result<Json<LocalMcpServer>, 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<UpsertLocalServerParams>,
) -> Result<Json<LocalMcpServer>, 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<SetServerAppsParams>,
) -> Result<Json<Option<LocalMcpServer>>, 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<RemoveServerParams>,
) -> Result<Json<bool>, AppCommandError> {
let result =
mcp_commands::mcp_remove_server(params.server_id, params.apps).await?;
Ok(Json(result))
}

View File

@@ -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;

View File

@@ -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<tauri::AppHandle>,
) -> Result<Json<SystemProxySettings>, AppCommandError> {
let db = app.state::<AppDatabase>();
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::<SystemProxySettings>(&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<tauri::AppHandle>,
) -> Result<Json<SystemLanguageSettings>, AppCommandError> {
let db = app.state::<AppDatabase>();
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::<SystemLanguageSettings>(&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<tauri::AppHandle>,
Json(settings): Json<SystemProxySettings>,
) -> Result<Json<SystemProxySettings>, AppCommandError> {
let db = app.state::<AppDatabase>();
// 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<tauri::AppHandle>,
Json(settings): Json<SystemLanguageSettings>,
) -> Result<Json<SystemLanguageSettings>, AppCommandError> {
let db = app.state::<AppDatabase>();
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))
}

View File

@@ -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<String>,
}
#[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<tauri::AppHandle>,
Json(params): Json<TerminalSpawnParams>,
@@ -43,3 +73,44 @@ pub async fn terminal_spawn(
Ok(Json(id))
}
pub async fn terminal_write(
Extension(app): Extension<tauri::AppHandle>,
Json(params): Json<TerminalWriteParams>,
) -> Result<Json<()>, AppCommandError> {
let manager = app.state::<TerminalManager>();
manager
.write(&params.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<tauri::AppHandle>,
Json(params): Json<TerminalResizeParams>,
) -> Result<Json<()>, AppCommandError> {
let manager = app.state::<TerminalManager>();
manager
.resize(&params.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<tauri::AppHandle>,
Json(params): Json<TerminalIdParams>,
) -> Result<Json<()>, AppCommandError> {
let manager = app.state::<TerminalManager>();
manager
.kill(&params.terminal_id)
.map_err(|e| AppCommandError::task_execution_failed(e.to_string()))?;
Ok(Json(()))
}
pub async fn terminal_list(
Extension(app): Extension<tauri::AppHandle>,
) -> Result<Json<Vec<TerminalInfo>>, AppCommandError> {
let manager = app.state::<TerminalManager>();
let result = manager.list_with_exit_check(Some(&app));
Ok(Json(result))
}

View File

@@ -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<u32>,
pub branch: Option<String>,
pub remote: Option<String>,
}
pub async fn git_log(
Json(params): Json<GitLogParams>,
) -> Result<Json<folder_commands::GitLogResult>, 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<tauri::AppHandle>,
) -> Result<Json<GitDetectResult>, AppCommandError> {
let db = app.state::<AppDatabase>();
let result = vc_commands::detect_git_core(&db.conn).await?;
Ok(Json(result))
}
pub async fn test_git_path(
Json(params): Json<TestGitPathParams>,
) -> Result<Json<GitDetectResult>, 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<tauri::AppHandle>,
) -> Result<Json<GitSettings>, AppCommandError> {
let db = app.state::<AppDatabase>();
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::<GitSettings>(&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<tauri::AppHandle>,
Json(settings): Json<GitSettings>,
) -> Result<Json<GitSettings>, AppCommandError> {
let db = app.state::<AppDatabase>();
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<tauri::AppHandle>,
) -> Result<Json<GitHubAccountsSettings>, AppCommandError> {
let db = app.state::<AppDatabase>();
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::<GitHubAccountsSettings>(&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<tauri::AppHandle>,
Json(settings): Json<GitHubAccountsSettings>,
) -> Result<Json<GitHubAccountsSettings>, AppCommandError> {
let db = app.state::<AppDatabase>();
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<ValidateGitHubTokenParams>,
) -> Result<Json<GitHubTokenValidation>, 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<SaveAccountTokenParams>,
) -> Result<Json<()>, AppCommandError> {
vc_commands::save_account_token(params.account_id, params.token).await?;
Ok(Json(()))
}
pub async fn get_account_token(
Json(params): Json<AccountIdParams>,
) -> Result<Json<Option<String>>, AppCommandError> {
let result = vc_commands::get_account_token(params.account_id).await?;
Ok(Json(result))
}
pub async fn delete_account_token(
Json(params): Json<AccountIdParams>,
) -> Result<Json<()>, AppCommandError> {
vc_commands::delete_account_token(params.account_id).await?;
Ok(Json(()))
}

View File

@@ -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())
}));