diff --git a/src-tauri/src/commands/acp.rs b/src-tauri/src/commands/acp.rs index a2f64f9..d30bb83 100644 --- a/src-tauri/src/commands/acp.rs +++ b/src-tauri/src/commands/acp.rs @@ -2735,6 +2735,7 @@ pub async fn acp_detect_agent_local_version( pub(crate) async fn acp_prepare_npx_agent_core( agent_type: AgentType, registry_version: Option, + clean_first: bool, task_id: String, db: &AppDatabase, emitter: &EventEmitter, @@ -2759,6 +2760,30 @@ pub(crate) async fn acp_prepare_npx_agent_core( .flatten() .and_then(|m| m.installed_version); + // Best-effort uninstall before reinstall. Forces npm to re-resolve + // the dependency graph from scratch, which is required for + // platform-specific optionalDependencies (e.g. native CLI binaries + // shipped as `-darwin-x64`) to be picked up after an upgrade. + // Failures here are logged and swallowed so we still attempt the + // install — for example when nothing is currently installed. + if clean_first { + let package_name = package_name_from_spec(package); + emit_agent_install_event( + emitter, + &task_id, + AgentInstallEventKind::Log, + format!("$ npm uninstall -g {package_name} (clean reinstall)"), + ); + if let Err(e) = uninstall_npm_global_package(package).await { + emit_agent_install_event( + emitter, + &task_id, + AgentInstallEventKind::Log, + format!("(warning) uninstall step failed, continuing: {e}"), + ); + } + } + emit_agent_install_event( emitter, &task_id, @@ -2813,6 +2838,23 @@ pub(crate) async fn acp_prepare_npx_agent_core( ); } Err(e) => { + // When clean_first was true the uninstall step may already have + // succeeded by the time install failed, leaving the DB pointing at + // a version that no longer exists on disk. Resync the DB to the + // actual filesystem state so the UI doesn't mislead the user into + // thinking they can connect. + if clean_first { + let detected = detect_local_version(agent_type).await; + if let Err(sync_err) = + agent_setting_service::set_installed_version(&db.conn, agent_type, detected) + .await + { + eprintln!( + "[acp] failed to resync installed_version after clean upgrade failure: {sync_err}" + ); + } + emit_acp_agents_updated(emitter, "npx_prepare_failed", Some(agent_type)); + } emit_agent_install_event( emitter, &task_id, @@ -2829,12 +2871,21 @@ pub(crate) async fn acp_prepare_npx_agent_core( pub async fn acp_prepare_npx_agent( agent_type: AgentType, registry_version: Option, + clean_first: Option, task_id: String, db: State<'_, AppDatabase>, app: tauri::AppHandle, ) -> Result { let emitter = EventEmitter::Tauri(app); - acp_prepare_npx_agent_core(agent_type, registry_version, task_id, &db, &emitter).await + acp_prepare_npx_agent_core( + agent_type, + registry_version, + clean_first.unwrap_or(false), + task_id, + &db, + &emitter, + ) + .await } pub(crate) async fn acp_uninstall_agent_core( diff --git a/src-tauri/src/web/handlers/acp.rs b/src-tauri/src/web/handlers/acp.rs index 40bab97..80fa352 100644 --- a/src-tauri/src/web/handlers/acp.rs +++ b/src-tauri/src/web/handlers/acp.rs @@ -492,6 +492,8 @@ pub async fn acp_detect_agent_local_version( pub struct AcpPrepareNpxAgentParams { pub agent_type: AgentType, pub registry_version: Option, + #[serde(default)] + pub clean_first: bool, pub task_id: String, } @@ -504,6 +506,7 @@ pub async fn acp_prepare_npx_agent( let result = acp_commands::acp_prepare_npx_agent_core( params.agent_type, params.registry_version, + params.clean_first, params.task_id, db, &emitter, diff --git a/src/components/settings/acp-agent-settings.tsx b/src/components/settings/acp-agent-settings.tsx index 014e0e1..2bc9c59 100644 --- a/src/components/settings/acp-agent-settings.tsx +++ b/src/components/settings/acp-agent-settings.tsx @@ -3153,7 +3153,8 @@ export function AcpAgentSettings() { const installedVersion = await acpPrepareNpxAgent( agent.agent_type, agent.registry_version, - taskId + taskId, + mode === "upgrade" ) setAgents((prev) => prev.map((item) => @@ -3200,6 +3201,25 @@ export function AcpAgentSettings() { description: message, } ) + if (mode === "upgrade") { + // Clean upgrade may have removed the old install before failing — + // resync local state so the UI doesn't keep showing a phantom version. + try { + const detected = await acpDetectAgentLocalVersion(agent.agent_type) + setAgents((prev) => + prev.map((item) => + item.agent_type === agent.agent_type + ? { ...item, installed_version: detected ?? null } + : item + ) + ) + } catch (detectErr) { + console.error( + "[Settings] failed to resync installed version after upgrade failure:", + detectErr + ) + } + } throw err } finally { busyActionRef.current.delete(agent.agent_type) diff --git a/src/lib/api.ts b/src/lib/api.ts index 9298b26..ec5de4c 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -202,11 +202,13 @@ export async function acpDetectAgentLocalVersion( export async function acpPrepareNpxAgent( agentType: AgentType, registryVersion: string | null | undefined, - taskId: string + taskId: string, + cleanFirst: boolean = false ): Promise { return getTransport().call("acp_prepare_npx_agent", { agentType, registryVersion: registryVersion ?? null, + cleanFirst, taskId, }) } diff --git a/src/lib/tauri.ts b/src/lib/tauri.ts index c53be36..16c0438 100644 --- a/src/lib/tauri.ts +++ b/src/lib/tauri.ts @@ -189,11 +189,13 @@ export async function acpDetectAgentLocalVersion( export async function acpPrepareNpxAgent( agentType: AgentType, registryVersion: string | null | undefined, - taskId: string + taskId: string, + cleanFirst: boolean = false ): Promise { return invoke("acp_prepare_npx_agent", { agentType, registryVersion: registryVersion ?? null, + cleanFirst, taskId, }) }