From 84c29782b0d8268c6f252ce0295082ae08a83e12 Mon Sep 17 00:00:00 2001 From: lzt404 <2596933790@qq.com> Date: Mon, 21 Sep 2026 15:38:38 +0800 Subject: [PATCH] fix(agents): fix antigravity settings path resolution and CLI launch in PowerShell Resolve Antigravity IDE configuration directory from installed executable metadata and product.json before falling back to legacy paths. Launch Antigravity CLI via System.Diagnostics.Process and wait for process exit when running under Windows PowerShell to avoid console I/O contention. Add unit tests for executable product metadata resolution and PowerShell CLI process synchronization. --- src-tauri/src/agents/antigravity.rs | 36 +++++- src-tauri/src/agents/antigravity/tests.rs | 71 ++++++++++- src-tauri/src/agents/launch.rs | 147 ++++++++++++++++++++-- 3 files changed, 240 insertions(+), 14 deletions(-) diff --git a/src-tauri/src/agents/antigravity.rs b/src-tauri/src/agents/antigravity.rs index 0fb4262..7f39f28 100644 --- a/src-tauri/src/agents/antigravity.rs +++ b/src-tauri/src/agents/antigravity.rs @@ -28,13 +28,45 @@ pub(crate) fn antigravity_config_paths(client: AgentClient, home: &Path) -> Vec< let directory = agent_configuration_environment("XDG_CONFIG_HOME") .filter(|path| path.is_absolute()) .unwrap_or_else(|| home.join(".config")); + vec![antigravity_ide_settings_path( + &directory, + find_antigravity_ide(home).as_deref(), + )] +} + +fn antigravity_ide_settings_path(directory: &Path, executable: Option<&Path>) -> PathBuf { + if let Some(executable) = executable { + let product = antigravity_resources(executable) + .and_then(|resources| fs::read_to_string(resources.join("product.json")).ok()) + .and_then(|text| serde_json::from_str::(&text).ok()); + let name = product + .as_ref() + .and_then(|value| value.get("nameShort")) + .and_then(Value::as_str) + .filter(|name| matches!(*name, "Antigravity IDE" | "Antigravity")) + .or_else(|| { + executable + .ancestors() + .find_map(|path| match path.file_name()?.to_str()? { + "Antigravity IDE.exe" | "Antigravity IDE.app" | "antigravity-ide" => { + Some("Antigravity IDE") + } + "Antigravity.exe" | "Antigravity.app" | "antigravity" => { + Some("Antigravity") + } + _ => None, + }) + }) + .unwrap_or("Antigravity IDE"); + return directory.join(name).join("User/settings.json"); + } let current = directory.join("Antigravity IDE/User/settings.json"); let legacy = directory.join("Antigravity/User/settings.json"); - vec![if !current.exists() && legacy.exists() { + if !current.exists() && legacy.exists() { legacy } else { current - }] + } } pub(crate) fn find_antigravity_cli(home: &Path) -> Option { diff --git a/src-tauri/src/agents/antigravity/tests.rs b/src-tauri/src/agents/antigravity/tests.rs index 5f9d9e4..916297d 100644 --- a/src-tauri/src/agents/antigravity/tests.rs +++ b/src-tauri/src/agents/antigravity/tests.rs @@ -87,6 +87,63 @@ fn antigravity_clients_have_independent_paths_and_launch_targets() { )); } +#[test] +fn antigravity_ide_settings_follow_the_installed_product() { + for (name, other) in [ + ("Antigravity IDE", "Antigravity"), + ("Antigravity", "Antigravity IDE"), + ] { + let home = Home::new(); + let directory = home.0.join("configuration"); + let executable = home.0.join("installation/Contents/MacOS/Electron"); + let resources = antigravity_resources(&executable).unwrap(); + home.save(&resources.join("product.json"), &json!({"nameShort":name})); + let other_settings = directory.join(other).join("User/settings.json"); + home.save(&other_settings, &json!({"editor.fontSize":17})); + let selected = directory.join(name).join("User/settings.json"); + assert!(!selected.exists()); + assert_eq!( + antigravity_ide_settings_path(&directory, Some(&executable)), + selected + ); + home.save(&selected, &json!({})); + assert_eq!( + antigravity_ide_settings_path(&directory, Some(&executable)), + selected + ); + assert_eq!( + fs::read_to_string(other_settings).unwrap(), + r#"{"editor.fontSize":17}"# + ); + } +} + +#[test] +fn antigravity_ide_settings_use_installation_name_without_product_metadata() { + let home = Home::new(); + let directory = home.0.join("configuration"); + for (binary, name) in [ + ("Antigravity IDE.exe", "Antigravity IDE"), + ("Antigravity.exe", "Antigravity"), + ( + "Antigravity IDE.app/Contents/MacOS/Electron", + "Antigravity IDE", + ), + ("Antigravity.app/Contents/MacOS/Electron", "Antigravity"), + ("antigravity-ide", "Antigravity IDE"), + ("antigravity", "Antigravity"), + ] { + let executable = home.0.join("installation").join(binary); + assert_eq!( + antigravity_ide_settings_path(&directory, Some(&executable)), + directory.join(name).join("User/settings.json") + ); + } + let legacy = directory.join("Antigravity/User/settings.json"); + home.save(&legacy, &json!({})); + assert_eq!(antigravity_ide_settings_path(&directory, None), legacy); +} + #[test] fn antigravity_updates_and_disable_preserve_external_settings() { for client in [AgentClient::AntigravityIde, AgentClient::AntigravityCli] { @@ -270,7 +327,18 @@ fn antigravity_ide_discovery_and_template_use_native_server() { fs::write(&executable, []).unwrap(); home.save( &resources.join("product.json"), - &json!({"ideVersion":"2.5.5"}), + &json!({"ideVersion":"2.5.5", "nameShort":"Antigravity IDE"}), + ); + let legacy = home + .0 + .join("AppData/Roaming/Antigravity/User/settings.json"); + home.save(&legacy, &json!({"editor.fontSize":17})); + let expected = home + .0 + .join("AppData/Roaming/Antigravity IDE/User/settings.json"); + assert_eq!( + antigravity_config_paths(AgentClient::AntigravityIde, &home.0), + vec![expected.clone()] ); assert_eq!(find_antigravity_ide(&home.0), Some(executable.clone())); assert_eq!( @@ -295,6 +363,7 @@ fn antigravity_ide_discovery_and_template_use_native_server() { }) .unwrap(); assert_eq!(updates.len(), 1); + assert_eq!(updates[0].path, expected); let value = parse(&updates[0].path, Some(&updates[0].after)).unwrap(); assert_eq!( Path::new(value[IDE_ENV_KEY][IDE_SERVER_ENV].as_str().unwrap()), diff --git a/src-tauri/src/agents/launch.rs b/src-tauri/src/agents/launch.rs index 99623dc..00ba731 100644 --- a/src-tauri/src/agents/launch.rs +++ b/src-tauri/src/agents/launch.rs @@ -1284,6 +1284,63 @@ fn launch_cli_agent( }) } +#[cfg(target_os = "windows")] +fn windows_powershell_cli_script( + executable: &Path, + working_directory: &Path, + arguments: &[String], +) -> String { + let directory = windows_powershell_single_quoted_literal(&path_to_string(working_directory)); + let executable = windows_powershell_single_quoted_literal(&path_to_string(executable)); + if arguments + .first() + .is_some_and(|arg| arg == "--cpa-antigravity-cli") + { + let arguments = arguments + .iter() + .map(|argument| { + let mut quoted = String::from("\""); + let mut backslashes = 0; + for ch in argument.chars() { + if ch == '\\' { + backslashes += 1; + continue; + } + quoted.push_str(&"\\".repeat(if ch == '"' { + backslashes * 2 + 1 + } else { + backslashes + })); + backslashes = 0; + quoted.push(ch); + } + quoted.push_str(&"\\".repeat(backslashes * 2)); + quoted.push('"'); + quoted + }) + .collect::>() + .join(" "); + return format!( + "Set-Location -LiteralPath {directory}; \ + $cpaLaunchInfo = New-Object System.Diagnostics.ProcessStartInfo; \ + $cpaLaunchInfo.FileName = {executable}; \ + $cpaLaunchInfo.WorkingDirectory = {directory}; \ + $cpaLaunchInfo.UseShellExecute = $false; \ + $cpaLaunchInfo.Arguments = {}; \ + $cpaLaunchProcess = [System.Diagnostics.Process]::Start($cpaLaunchInfo); \ + try {{ $cpaLaunchProcess.WaitForExit(); $global:LASTEXITCODE = $cpaLaunchProcess.ExitCode }} \ + finally {{ $cpaLaunchProcess.Dispose() }}", + windows_powershell_single_quoted_literal(&arguments), + ); + } + let arguments = arguments + .iter() + .map(|argument| windows_powershell_single_quoted_literal(argument)) + .collect::>() + .join(" "); + format!("Set-Location -LiteralPath {directory}; & {executable} {arguments}") +} + #[cfg(target_os = "windows")] fn launch_cli_agent( executable: &Path, @@ -1331,17 +1388,7 @@ fn launch_cli_agent( } "powershell" => { let mut command = Command::new(windows_powershell_executable()); - let arguments = arguments - .iter() - .map(|argument| windows_powershell_single_quoted_literal(argument)) - .collect::>() - .join(" "); - let script = format!( - "Set-Location -LiteralPath {}; & {} {}", - windows_powershell_single_quoted_literal(&path_to_string(working_directory)), - windows_powershell_single_quoted_literal(&path_to_string(executable)), - arguments, - ); + let script = windows_powershell_cli_script(executable, working_directory, arguments); command.args(["-NoLogo", "-NoProfile", "-NoExit", "-Command", &script]); command.creation_flags(CREATE_NEW_CONSOLE); command @@ -1411,6 +1458,84 @@ mod tests { assert_eq!(normalize_agent_terminal(" auto "), "auto"); } + #[cfg(windows)] + #[test] + fn antigravity_powershell_waits_for_gui_helper_and_preserves_arguments() { + let directory = env::temp_dir() + .join(format!( + "cpa-powershell-{}-{}", + std::process::id(), + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(), + )) + .join("用户's [workspace] $value"); + fs::create_dir_all(&directory).unwrap(); + let executable = directory.join("GUI helper.exe"); + let compiled = directory.parent().unwrap().join("helper.exe"); + let source = r#" +using System; +using System.IO; +using System.Threading; +class GuiHelper { + static void Main(string[] args) { + Thread.Sleep(350); + File.WriteAllLines(Path.Combine(Environment.CurrentDirectory, "arguments.txt"), args); + File.WriteAllText(Path.Combine(Environment.CurrentDirectory, "finished.txt"), "done"); + } +} +"#; + let script = format!( + "Add-Type -TypeDefinition {} -OutputAssembly {} -OutputType WindowsApplication -ErrorAction Stop", + windows_powershell_single_quoted_literal(source), + windows_powershell_single_quoted_literal(&path_to_string(&compiled)), + ); + let run = |script: &str| { + let mut command = Command::new(windows_powershell_executable()); + command.args([ + "-NoLogo", + "-NoProfile", + "-NonInteractive", + "-Command", + script, + ]); + configure_background_command(&mut command); + let result = command_output_with_timeout(&mut command, Duration::from_secs(20)) + .unwrap() + .expect("PowerShell timed out"); + assert!( + result.status.success(), + "{}", + String::from_utf8_lossy(&result.stderr) + ); + }; + run(&script); + fs::rename(compiled, &executable).unwrap(); + let arguments = vec![ + "--cpa-antigravity-cli".into(), + path_to_string(&directory), + "trailing slash \\".into(), + "quote \" and \\\"".into(), + "literal $value & [x] 'quoted'".into(), + String::new(), + ]; + let script = format!( + "{}; if (![IO.File]::Exists({})) {{ throw 'GUI helper is still running' }}", + windows_powershell_cli_script(&executable, &directory, &arguments), + windows_powershell_single_quoted_literal(&path_to_string( + &directory.join("finished.txt") + )), + ); + run(&script); + let received = fs::read_to_string(directory.join("arguments.txt")).unwrap(); + assert_eq!( + received.lines().collect::>(), + arguments.iter().map(String::as_str).collect::>() + ); + fs::remove_dir_all(directory).unwrap(); + } + #[test] fn desktop_process_matching_is_scoped_to_the_exact_bundle() { let application = Path::new("/Applications/Claude Desktop.app/Contents/MacOS/Claude");