diff --git a/README.ja.md b/README.ja.md index 3ae74e9..ce053c8 100644 --- a/README.ja.md +++ b/README.ja.md @@ -100,15 +100,15 @@ OpenAI、Claude、Gemini、およびその他の互換形式の間で変換で - Pi(CLIProxyAPI provider 拡張機能) - ZCode - WorkBuddy / WorkBuddy AI -- Antigravity IDE - Antigravity CLI +- Cursor CLI - Kimi Code - Grok Build 対応クライアントでは、利用可能なモデルカタログの同期、デフォルトモデルの選択、管理設定を適用する前の 元設定のバックアップ、以前の設定への復元、利用可能なデスクトップまたは CLI エントリーポイントの起動ができます。 -Antigravity IDE と Antigravity CLI は設定ファイルが異なるため、個別のエントリーとして表示されます。どちらも CPA の Gemini 互換 API に接続します。IDE は設定の適用・解除後に再起動してください。IDE 内のモデル名表示は変わりませんが、リクエストには CPA で選択したモデルが使われます。CLI は CPA から起動すると、そのプロセスに接続先と API キーが渡されます。`agy` を直接実行する場合は環境変数を自分で設定してください。 +Antigravity CLI は CPA の Gemini 互換 API に接続します。CLI は CPA から起動すると、そのプロセスに接続先と API キーが渡されます。`agy` を直接実行する場合は環境変数を自分で設定してください。 ## その他の機能 diff --git a/README.md b/README.md index 03c40af..b578031 100644 --- a/README.md +++ b/README.md @@ -118,8 +118,8 @@ proxy. Supported clients include: - Pi (with the CLIProxyAPI provider extension) - ZCode - WorkBuddy / WorkBuddy AI -- Antigravity IDE - Antigravity CLI +- Cursor CLI - Kimi Code - Grok Build @@ -127,7 +127,9 @@ For supported clients, the application can synchronize the available model catal default model, back up the original configuration before applying managed settings, and restore the previous configuration. -Antigravity IDE and Antigravity CLI have separate entries because they use different configuration files. Both connect through CPA's Gemini-compatible API. Restart the IDE after applying or disabling settings; its model labels stay unchanged, while requests use the model selected in CPA. Launch the CLI from CPA to supply its endpoint and API key for that process. Running `agy` directly requires setting those environment variables yourself. +Antigravity CLI connects through CPA's Gemini-compatible API. Launch the CLI from CPA to supply its endpoint and API key for that process. Running `agy` directly requires setting those environment variables yourself. + +Cursor CLI has a separate entry. It supports local installation detection and launch with terminal and working directory selection, and uses Cursor’s native configuration and account system rather than local CPA model routing. ## Additional Capabilities diff --git a/README.zh-CN.md b/README.zh-CN.md index a2f53a9..48a4647 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -109,8 +109,8 @@ API 接入页面按照协议或 Provider 管理上游 API 凭证和服务地址 - Pi(通过 CLIProxyAPI provider 插件) - ZCode - WorkBuddy / WorkBuddy AI -- Antigravity IDE - Antigravity CLI +- Cursor CLI - Kimi Code - Grok Build @@ -121,7 +121,7 @@ WorkBuddy 接入沿用模型选择、更新配置、关闭配置修改、手动 支持 WorkBuddy 与 WorkBuddy AI 的 `models.json` 对象及数组格式,并保留已有自定义模型。 应用后需在 WorkBuddy 内选择 `CPA:模型名`;已有会话不会自动切换模型,未显示时可重启 App。 -Antigravity IDE 与 Antigravity CLI 使用不同的配置文件,因此提供独立入口,均通过 CPA 的 Gemini 兼容接口连接。IDE 应用或关闭配置后需要重启;IDE 内的模型标签保持不变,实际请求使用 CPA 中选定的模型。CLI 请从 CPA 启动,以便为该进程注入接口地址和密钥;直接运行 `agy` 时需自行设置环境变量。 +Antigravity CLI 通过 CPA 的 Gemini 兼容接口连接。CLI 请从 CPA 启动,以便为该进程注入接口地址和密钥;直接运行 `agy` 时需自行设置环境变量。 ## 其他功能 diff --git a/src-tauri/src/agents.rs b/src-tauri/src/agents.rs index 35d95c8..4ead7cb 100644 --- a/src-tauri/src/agents.rs +++ b/src-tauri/src/agents.rs @@ -12,6 +12,7 @@ mod templates; mod transactions; mod workbuddy; mod antigravity; +mod cursor; #[cfg(target_os = "windows")] mod windows_probe; pub(crate) use backups::*; @@ -26,5 +27,6 @@ pub(crate) use templates::*; pub(crate) use transactions::*; pub(crate) use workbuddy::*; pub(crate) use antigravity::*; +pub(crate) use cursor::*; #[cfg(target_os = "windows")] pub(crate) use windows_probe::*; diff --git a/src-tauri/src/agents/antigravity.rs b/src-tauri/src/agents/antigravity.rs index 7f39f28..3576866 100644 --- a/src-tauri/src/agents/antigravity.rs +++ b/src-tauri/src/agents/antigravity.rs @@ -6,67 +6,13 @@ mod tests; pub(crate) const ANTIGRAVITY_CONNECTION_FILE: &str = "cpa-connection.json"; const CLI_MODEL_LABEL: &str = "CPA (EasyCLIProxyAPI)"; -const IDE_BINARY_KEY: &str = "codeiumDev.languageServerBinaryPath"; -const IDE_ENV_KEY: &str = "codeiumDev.languageServerEnv"; -const IDE_SERVER_ENV: &str = "CPA_ANTIGRAVITY_LANGUAGE_SERVER"; -const IDE_MODEL_ENV: &str = "CPA_ANTIGRAVITY_MODEL"; -pub(crate) fn antigravity_config_paths(client: AgentClient, home: &Path) -> Vec { - if client == AgentClient::AntigravityCli { - let directory = home.join(".gemini/antigravity-cli"); - return vec![ - directory.join("settings.json"), - directory.join(ANTIGRAVITY_CONNECTION_FILE), - ]; - } - #[cfg(target_os = "windows")] - let directory = - agent_configuration_environment("APPDATA").unwrap_or_else(|| home.join("AppData/Roaming")); - #[cfg(target_os = "macos")] - let directory = home.join("Library/Application Support"); - #[cfg(not(any(target_os = "windows", target_os = "macos")))] - 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"); - if !current.exists() && legacy.exists() { - legacy - } else { - current - } +pub(crate) fn antigravity_config_paths(_client: AgentClient, home: &Path) -> Vec { + let directory = home.join(".gemini/antigravity-cli"); + vec![ + directory.join("settings.json"), + directory.join(ANTIGRAVITY_CONNECTION_FILE), + ] } pub(crate) fn find_antigravity_cli(home: &Path) -> Option { @@ -83,90 +29,6 @@ pub(crate) fn find_antigravity_cli(home: &Path) -> Option { } } -pub(crate) fn find_antigravity_ide(home: &Path) -> Option { - let mut candidates = Vec::new(); - #[cfg(target_os = "windows")] - { - let local = agent_configuration_environment("LOCALAPPDATA") - .unwrap_or_else(|| home.join("AppData/Local")); - let mut roots = vec![local.join("Programs")]; - for name in ["ProgramFiles", "ProgramFiles(x86)"] { - if let Some(root) = agent_configuration_environment(name) { - roots.push(root); - } - } - for root in roots { - candidates.push(root.join("Antigravity IDE/Antigravity IDE.exe")); - candidates.push(root.join("Antigravity/Antigravity.exe")); - } - } - #[cfg(target_os = "macos")] - for root in [PathBuf::from("/Applications"), home.join("Applications")] { - for name in ["Antigravity IDE", "Antigravity"] { - candidates.push(root.join(format!("{name}.app/Contents/MacOS/Electron"))); - } - } - #[cfg(not(any(target_os = "windows", target_os = "macos")))] - { - candidates.extend([ - PathBuf::from("/usr/share/antigravity-ide/antigravity-ide"), - PathBuf::from("/opt/antigravity-ide/antigravity-ide"), - PathBuf::from("/usr/share/antigravity/antigravity"), - home.join(".local/share/antigravity-ide/antigravity-ide"), - ]); - } - let found = candidates.into_iter().find(|p| p.is_file()); - #[cfg(all(target_os = "windows", not(test)))] - let found = found.or_else(find_windows_registered_antigravity_ide); - found -} - -fn antigravity_resources(executable: &Path) -> Option { - #[cfg(target_os = "macos")] - { - Some(executable.parent()?.parent()?.join("Resources/app")) - } - #[cfg(not(target_os = "macos"))] - { - Some(executable.parent()?.join("resources/app")) - } -} - -pub(crate) fn read_antigravity_ide_version(executable: &Path) -> Option { - let value: Value = serde_json::from_str( - &fs::read_to_string(antigravity_resources(executable)?.join("product.json")).ok()?, - ) - .ok()?; - value - .get("ideVersion") - .and_then(Value::as_str) - .map(str::to_string) -} - -fn antigravity_language_server(executable: &Path) -> Option { - let platform = if cfg!(target_os = "windows") { - "windows" - } else if cfg!(target_os = "macos") { - "macos" - } else { - "linux" - }; - let arch = if cfg!(target_arch = "aarch64") { - "arm" - } else { - "x64" - }; - let suffix = if cfg!(target_os = "windows") { - ".exe" - } else { - "" - }; - let server = antigravity_resources(executable)?.join(format!( - "extensions/antigravity/bin/language_server_{platform}_{arch}{suffix}" - )); - server.is_file().then_some(server) -} - fn object_at<'a>(root: &'a mut Value, keys: &[&str]) -> Result<&'a mut Value, String> { let mut value = root; for key in keys { @@ -186,28 +48,18 @@ fn object_at<'a>(root: &'a mut Value, keys: &[&str]) -> Result<&'a mut Value, St } pub(crate) fn build_antigravity_config( - client: AgentClient, + _client: AgentClient, path: &Path, existing: Option<&str>, origin: &str, api_key: &str, model: &str, - server: Option<&Path>, ) -> Result { let mut root = parse(path, existing)?; if !root.is_object() { return Err("Antigravity 配置根节点必须是对象".into()); } - if client == AgentClient::AntigravityIde { - let server = server.ok_or("未找到 Antigravity IDE 语言服务,请安装 IDE 后重新检测")?; - let helper = env::current_exe().map_err(|_| "无法定位 CPA 启动适配程序")?; - let environment = object_at(&mut root, &[IDE_ENV_KEY])?; - environment[IDE_SERVER_ENV] = json!(server); - environment[IDE_MODEL_ENV] = json!(model); - environment["GEMINI_API_KEY"] = json!(api_key); - environment["GOOGLE_GEMINI_BASE_URL"] = json!(origin); - root[IDE_BINARY_KEY] = json!(helper); - } else if path + if path .file_name() .is_some_and(|n| n == ANTIGRAVITY_CONNECTION_FILE) { @@ -234,13 +86,6 @@ pub(crate) fn build_antigravity_updates( model: &str, template: bool, ) -> Result, String> { - let server = if client == AgentClient::AntigravityIde { - find_antigravity_ide(home) - .as_deref() - .and_then(antigravity_language_server) - } else { - None - }; antigravity_config_paths(client, home) .into_iter() .map(|path| { @@ -256,23 +101,14 @@ pub(crate) fn build_antigravity_updates( origin, api_key, model, - server.as_deref(), )?; Ok(AgentFileUpdate { path, after }) }) .collect() } -fn managed_fields(client: AgentClient, path: &Path) -> Vec> { - if client == AgentClient::AntigravityIde { - vec![ - vec![IDE_BINARY_KEY], - vec![IDE_ENV_KEY, IDE_SERVER_ENV], - vec![IDE_ENV_KEY, IDE_MODEL_ENV], - vec![IDE_ENV_KEY, "GEMINI_API_KEY"], - vec![IDE_ENV_KEY, "GOOGLE_GEMINI_BASE_URL"], - ] - } else if path +fn managed_fields(_client: AgentClient, path: &Path) -> Vec> { + if path .file_name() .is_some_and(|n| n == ANTIGRAVITY_CONNECTION_FILE) { @@ -339,81 +175,47 @@ pub(crate) fn restore_antigravity_config( } pub(crate) fn antigravity_has_marker( - client: AgentClient, + _client: AgentClient, paths: &[PathBuf], ) -> Result { - let path = if client == AgentClient::AntigravityIde { - &paths[0] - } else { - paths.get(1).ok_or("Antigravity CLI 配置路径缺失")? - }; + let path = paths.get(1).ok_or("Antigravity CLI 配置路径缺失")?; let root = parse(path, read_optional_text(path)?.as_deref())?; - Ok(if client == AgentClient::AntigravityIde { - field(&root, &[IDE_ENV_KEY, IDE_SERVER_ENV]).is_some() - } else { - root.get("provider").and_then(Value::as_str) == Some(MANAGED_AGENT_PROVIDER_ID) - }) + Ok(root.get("provider").and_then(Value::as_str) == Some(MANAGED_AGENT_PROVIDER_ID)) } pub(crate) fn inspect_antigravity_config( - client: AgentClient, + _client: AgentClient, paths: &[PathBuf], port: u16, api_key: &str, ) -> Result<(bool, Option), String> { let settings = parse(&paths[0], read_optional_text(&paths[0])?.as_deref())?; let origin = managed_core_loopback_origin(port); - if client == AgentClient::AntigravityIde { - let environment = settings.get(IDE_ENV_KEY).unwrap_or(&Value::Null); - let model = environment - .get(IDE_MODEL_ENV) - .and_then(Value::as_str) - .filter(|s| !s.is_empty()) - .map(str::to_string); - let helper = env::current_exe().ok(); - let configured = model.is_some() - && settings - .get(IDE_BINARY_KEY) - .and_then(Value::as_str) - .map(Path::new) - == helper.as_deref() - && environment - .get(IDE_SERVER_ENV) - .and_then(Value::as_str) - .is_some_and(|p| Path::new(p).is_file()) - && environment.get("GEMINI_API_KEY").and_then(Value::as_str) == Some(api_key) - && environment - .get("GOOGLE_GEMINI_BASE_URL") - .and_then(Value::as_str) - == Some(&origin); - Ok((configured, model)) - } else { - let path = paths.get(1).ok_or("Antigravity CLI 配置路径缺失")?; - let connection = parse(path, read_optional_text(path)?.as_deref())?; - let model = connection - .get("model") - .and_then(Value::as_str) - .filter(|s| !s.is_empty()) - .map(str::to_string); - let configured = model.is_some() - && settings.get("modelProvider").and_then(Value::as_str) == Some("gemini") - && field( - &settings, - &[ - "customModelsConfig", - "customModels", - CLI_MODEL_LABEL, - "modelName", - ], - ) - .and_then(Value::as_str) - == model.as_deref() - && connection.get("provider").and_then(Value::as_str) - == Some(MANAGED_AGENT_PROVIDER_ID) - && connection.get("baseUrl").and_then(Value::as_str) == Some(&origin) - && connection.get("apiKey").and_then(Value::as_str) == Some(api_key); - Ok((configured, model)) - } + let path = paths.get(1).ok_or("Antigravity CLI 配置路径缺失")?; + let connection = parse(path, read_optional_text(path)?.as_deref())?; + let model = connection + .get("model") + .and_then(Value::as_str) + .filter(|s| !s.is_empty()) + .map(str::to_string); + let configured = model.is_some() + && settings.get("modelProvider").and_then(Value::as_str) == Some("gemini") + && field( + &settings, + &[ + "customModelsConfig", + "customModels", + CLI_MODEL_LABEL, + "modelName", + ], + ) + .and_then(Value::as_str) + == model.as_deref() + && connection.get("provider").and_then(Value::as_str) + == Some(MANAGED_AGENT_PROVIDER_ID) + && connection.get("baseUrl").and_then(Value::as_str) == Some(&origin) + && connection.get("apiKey").and_then(Value::as_str) == Some(api_key); + Ok((configured, model)) } pub(crate) fn prepare_antigravity_removal( @@ -441,87 +243,61 @@ pub(crate) fn antigravity_cli_helper_arguments(home: &Path) -> Vec { pub(crate) fn antigravity_helper_requested(args: &[std::ffi::OsString]) -> bool { args.get(1).is_some_and(|a| a == "--cpa-antigravity-cli") - || (env::var_os(IDE_SERVER_ENV).is_some() - && args.iter().skip(1).any(|a| { - a == "--extension_server_port" - || a.to_string_lossy().starts_with("--extension_server_port=") - })) } fn antigravity_helper_command(args: &[std::ffi::OsString]) -> Result { - let cli = args.get(1).is_some_and(|a| a == "--cpa-antigravity-cli"); - let mut command = if cli { - let home = args - .get(2) - .map(PathBuf::from) - .ok_or("Antigravity CLI 启动参数缺少用户目录")?; - let paths = antigravity_config_paths(AgentClient::AntigravityCli, &home); - let connection = parse(&paths[1], read_optional_text(&paths[1])?.as_deref())?; - if connection.get("provider").and_then(Value::as_str) != Some(MANAGED_AGENT_PROVIDER_ID) { - return Err("Antigravity CLI 的 CPA 配置已关闭,请重新应用配置".into()); - } - let key = connection - .get("apiKey") - .and_then(Value::as_str) - .filter(|s| !s.is_empty()) - .ok_or("Antigravity CLI 密钥缺失")?; - let url = connection - .get("baseUrl") - .and_then(Value::as_str) - .filter(|s| !s.trim().is_empty()) - .ok_or("Antigravity CLI 地址缺失")?; - let model = connection - .get("model") - .and_then(Value::as_str) - .filter(|s| !s.trim().is_empty()) - .ok_or("Antigravity CLI 模型缺失")?; - let settings = parse(&paths[0], read_optional_text(&paths[0])?.as_deref())?; - if settings.get("modelProvider").and_then(Value::as_str) != Some("gemini") - || field( - &settings, - &[ - "customModelsConfig", - "customModels", - CLI_MODEL_LABEL, - "modelName", - ], - ) - .and_then(Value::as_str) - != Some(model) - { - return Err("Antigravity CLI 配置已变化,请重新应用配置".into()); - } - let executable = find_antigravity_cli(&home).ok_or("未找到 Antigravity CLI")?; - let mut command = Command::new(executable); - command - .arg(format!("--gemini_dir={}", home.join(".gemini").display())) - .arg(format!("--model={CLI_MODEL_LABEL}")) - .args(args.iter().skip(3)) - .env("GEMINI_API_KEY", key) - .env("GOOGLE_GEMINI_BASE_URL", url); - command - } else { - let executable = env::var_os(IDE_SERVER_ENV) - .map(PathBuf::from) - .ok_or("Antigravity IDE 语言服务路径缺失")?; - if Some(&executable) == env::current_exe().ok().as_ref() { - return Err("Antigravity IDE 语言服务路径不能指向 CPA".into()); - } - let model = env::var(IDE_MODEL_ENV).map_err(|_| "Antigravity IDE 模型缺失")?; - if model.trim().is_empty() { - return Err("Antigravity IDE 模型缺失".into()); - } - let mut command = Command::new(executable); - command - .args(args.iter().skip(1)) - .arg("--model_api_client_type=gemini") - .arg(format!("--override_model_name={model}")); - command - }; + if !args.get(1).is_some_and(|a| a == "--cpa-antigravity-cli") { + return Err("未知的 Antigravity 启动参数".into()); + } + let home = args + .get(2) + .map(PathBuf::from) + .ok_or("Antigravity CLI 启动参数缺少用户目录")?; + let paths = antigravity_config_paths(AgentClient::AntigravityCli, &home); + let connection = parse(&paths[1], read_optional_text(&paths[1])?.as_deref())?; + if connection.get("provider").and_then(Value::as_str) != Some(MANAGED_AGENT_PROVIDER_ID) { + return Err("Antigravity CLI 的 CPA 配置已关闭,请重新应用配置".into()); + } + let key = connection + .get("apiKey") + .and_then(Value::as_str) + .filter(|s| !s.is_empty()) + .ok_or("Antigravity CLI 密钥缺失")?; + let url = connection + .get("baseUrl") + .and_then(Value::as_str) + .filter(|s| !s.trim().is_empty()) + .ok_or("Antigravity CLI 地址缺失")?; + let model = connection + .get("model") + .and_then(Value::as_str) + .filter(|s| !s.trim().is_empty()) + .ok_or("Antigravity CLI 模型缺失")?; + let settings = parse(&paths[0], read_optional_text(&paths[0])?.as_deref())?; + if settings.get("modelProvider").and_then(Value::as_str) != Some("gemini") + || field( + &settings, + &[ + "customModelsConfig", + "customModels", + CLI_MODEL_LABEL, + "modelName", + ], + ) + .and_then(Value::as_str) + != Some(model) + { + return Err("Antigravity CLI 配置已变化,请重新应用配置".into()); + } + let executable = find_antigravity_cli(&home).ok_or("未找到 Antigravity CLI")?; + let mut command = Command::new(executable); command - .env_remove("GOOGLE_API_KEY") - .env_remove(IDE_SERVER_ENV) - .env_remove(IDE_MODEL_ENV); + .arg(format!("--gemini_dir={}", home.join(".gemini").display())) + .arg(format!("--model={CLI_MODEL_LABEL}")) + .args(args.iter().skip(3)) + .env("GEMINI_API_KEY", key) + .env("GOOGLE_GEMINI_BASE_URL", url); + command.env_remove("GOOGLE_API_KEY"); Ok(command) } @@ -529,13 +305,8 @@ pub(crate) fn run_antigravity_helper(args: &[std::ffi::OsString]) -> Result { inspect_pi_provider_status(home, config.port, api_key) } + AgentStatusDetectionTarget::Cursor(client) => { + inspect_cursor_client(client, home) + } }; results .lock() diff --git a/src-tauri/src/agents/configuration.rs b/src-tauri/src/agents/configuration.rs index a52db9e..ee3fd27 100644 --- a/src-tauri/src/agents/configuration.rs +++ b/src-tauri/src/agents/configuration.rs @@ -187,7 +187,7 @@ pub(crate) fn build_agent_updates_with_oauth( }, ]) } - AgentClient::AntigravityIde | AgentClient::AntigravityCli => build_antigravity_updates(client, home, &root_base, api_key, model, false), + AgentClient::AntigravityCli => build_antigravity_updates(client, home, &root_base, api_key, model, false), AgentClient::WorkBuddy => { let before = read_optional_text(&paths[0])?; Ok(vec![AgentFileUpdate { @@ -551,7 +551,7 @@ pub(crate) fn write_agent_configuration_file( { return write_codex_private_file(path, content); } - if matches!(client, AgentClient::AntigravityIde | AgentClient::AntigravityCli) { + if client == AgentClient::AntigravityCli { return write_codex_private_file(path, content); } if client == AgentClient::DeepSeekHarness { @@ -1633,7 +1633,7 @@ pub(crate) fn prepare_agent_managed_removal( AgentClient::DeepSeekHarness => prepare_deepseek_harness_managed_removal(paths), AgentClient::ZCode => prepare_zcode_managed_removal(paths), AgentClient::WorkBuddy => prepare_workbuddy_managed_removal(paths), - AgentClient::AntigravityIde | AgentClient::AntigravityCli => prepare_antigravity_removal(client, paths), + AgentClient::AntigravityCli => prepare_antigravity_removal(client, paths), AgentClient::KimiCode => prepare_kimi_code_managed_removal(paths), AgentClient::GrokBuild => prepare_grok_build_managed_removal(paths), } @@ -2672,7 +2672,7 @@ pub(crate) fn build_agent_session_restored_bytes_with_preference( } AgentClient::ZCode => build_restored_zcode_config(current, original)?, AgentClient::WorkBuddy => build_restored_workbuddy_config(current, original)?, - AgentClient::AntigravityIde | AgentClient::AntigravityCli => restore_antigravity_config(client, path, current, original)?, + AgentClient::AntigravityCli => restore_antigravity_config(client, path, current, original)?, AgentClient::KimiCode => build_restored_kimi_code_config(current, original)?, AgentClient::GrokBuild => build_restored_grok_build_config(current, original)?, }; diff --git a/src-tauri/src/agents/cursor.rs b/src-tauri/src/agents/cursor.rs new file mode 100644 index 0000000..27a5119 --- /dev/null +++ b/src-tauri/src/agents/cursor.rs @@ -0,0 +1,123 @@ +use super::*; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum CursorClient { + Cli, +} + +impl CursorClient { + pub(crate) fn parse(value: &str) -> Option { + match value.trim().to_ascii_lowercase().as_str() { + "cursor-cli" => Some(Self::Cli), + _ => None, + } + } + + pub(crate) fn id(self) -> &'static str { + "cursor-cli" + } + + pub(crate) fn name(self) -> &'static str { + "Cursor CLI" + } + + pub(crate) fn target(self) -> &'static str { + "cli" + } + + pub(crate) fn executable(self, home: &Path) -> Option { + find_cursor_cli(home) + } +} + +pub(crate) fn find_cursor_cli(home: &Path) -> Option { + #[cfg(target_os = "windows")] + { + let directory = agent_configuration_environment("LOCALAPPDATA") + .unwrap_or_else(|| home.join("AppData/Local")) + .join("cursor-agent"); + for name in ["cursor-agent.exe", "cursor-agent.cmd", "agent.exe", "agent.cmd"] { + let candidate = directory.join(name); + if candidate.is_file() { + return Some(candidate); + } + } + } + find_named_agent_executable(home, &["cursor-agent"]) +} + +pub(crate) fn inspect_cursor_client(client: CursorClient, home: &Path) -> AgentConfigStatus { + let executable = client.executable(home); + let version = executable.as_deref().and_then(|path| read_agent_version(path, home)); + cursor_status(client, executable.as_deref(), version) +} + +fn cursor_status(client: CursorClient, executable: Option<&Path>, version: Option) -> AgentConfigStatus { + AgentConfigStatus { + id: client.id().into(), + name: client.name().into(), + supported_platform: true, + installed: executable.is_some(), + plugin_installed: true, + launch_targets: executable.map(|path| AgentLaunchTarget { + id: client.target().into(), + label: client.name().into(), + detail: path_to_string(path), + }).into_iter().collect(), + cli_version: version.clone(), + app_version: None, + version, + plugin_version: None, + config_paths: Vec::new(), + config_exists: false, + config_valid: true, + configured: false, + configuration_synchronized: false, + connection_state: "unsupported".into(), + current_model: None, + oauth_configuration: false, + codex_native_oauth: false, + modification_enabled: false, + modification_state: "unconfigured".into(), + backup_available: false, + applied_model: None, + claude_code_model_mappings: None, + claude_desktop_model_mappings: None, + warnings: Vec::new(), + error: None, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn cursor_cli_has_a_native_launch_target_and_no_cpa_state() { + assert_eq!(CursorClient::parse("cursor-cli"), Some(CursorClient::Cli)); + assert_eq!(CursorClient::parse("cursor-ide"), None); + let status = cursor_status(CursorClient::Cli, Some(Path::new("/installed/client")), Some("1.2.3".into())); + assert_eq!(status.id, "cursor-cli"); + assert!(status.installed); + assert_eq!(status.launch_targets.len(), 1); + assert_eq!(status.launch_targets[0].id, "cli"); + assert_eq!(status.connection_state, "unsupported"); + assert!(!status.configured); + assert!(!status.modification_enabled); + assert!(!status.configuration_synchronized); + assert!(status.config_paths.is_empty()); + assert_eq!(status.cli_version.as_deref(), Some("1.2.3")); + assert!(status.app_version.is_none()); + let missing = cursor_status(CursorClient::Cli, None, None); + assert!(!missing.installed); + assert!(missing.launch_targets.is_empty()); + } + + #[test] + fn cursor_clients_cannot_enter_managed_configuration_operations() { + for id in ["cursor-ide", "cursor-cli", " CURSOR-CLI "] { + assert!(AgentClient::parse(id).unwrap_err().contains("CPA")); + } + assert_eq!(CursorClient::parse("codex"), None); + } +} diff --git a/src-tauri/src/agents/discovery.rs b/src-tauri/src/agents/discovery.rs index fe2b432..6b466a4 100644 --- a/src-tauri/src/agents/discovery.rs +++ b/src-tauri/src/agents/discovery.rs @@ -48,7 +48,7 @@ pub(crate) fn agent_config_paths(client: AgentClient, home: &Path) -> Vec vec![workbuddy_home(home).join("models.json")], - AgentClient::AntigravityIde | AgentClient::AntigravityCli => antigravity_config_paths(client, home), + AgentClient::AntigravityCli => antigravity_config_paths(client, home), AgentClient::ZCode => vec![ home.join(".zcode/v2").join(ZCODE_CONFIG_FILE), home.join(".zcode/cli").join(ZCODE_CONFIG_FILE), @@ -983,7 +983,6 @@ pub(crate) fn inspect_agent_config( AgentClient::DeepSeekHarness => read_deepseek_harness_profile_version(home), AgentClient::ZCode => executable.as_deref().and_then(read_zcode_app_version), AgentClient::WorkBuddy => executable.as_deref().and_then(read_workbuddy_app_version), - AgentClient::AntigravityIde => executable.as_deref().and_then(read_antigravity_ide_version), _ => None, }; let version = cli_version.clone().or_else(|| app_version.clone()); @@ -1069,13 +1068,13 @@ pub(crate) fn agent_installation_detected( version.is_some() || (matches!( client, - AgentClient::ClaudeDesktop | AgentClient::OpenCode | AgentClient::ZCode | AgentClient::WorkBuddy | AgentClient::AntigravityIde + AgentClient::ClaudeDesktop | AgentClient::OpenCode | AgentClient::ZCode | AgentClient::WorkBuddy ) && executable_found) || app_installed } pub(crate) fn should_probe_primary_agent_executable_version(client: AgentClient) -> bool { - !matches!(client, AgentClient::ClaudeDesktop | AgentClient::ZCode | AgentClient::WorkBuddy | AgentClient::AntigravityIde) + !matches!(client, AgentClient::ClaudeDesktop | AgentClient::ZCode | AgentClient::WorkBuddy) } pub(crate) fn agent_launch_targets( @@ -1129,7 +1128,7 @@ pub(crate) fn agent_launch_targets( }); } } - AgentClient::ZCode | AgentClient::WorkBuddy | AgentClient::AntigravityIde => { + AgentClient::ZCode | AgentClient::WorkBuddy => { if let Some(executable) = executable { targets.push(AgentLaunchTarget { id: "app".to_string(), @@ -1266,7 +1265,7 @@ pub(crate) fn inspect_agent_managed_config( false, )) } - AgentClient::AntigravityIde | AgentClient::AntigravityCli => inspect_antigravity_config(client, paths, port, api_key).map(|(configured, model)| (configured, model, false)), + AgentClient::AntigravityCli => inspect_antigravity_config(client, paths, port, api_key).map(|(configured, model)| (configured, model, false)), AgentClient::WorkBuddy => inspect_workbuddy_agent_config(&paths[0], port, api_key) .map(|(configured, model)| (configured, model, false)), AgentClient::KimiCode => inspect_kimi_code_agent_config(&paths[0], port, api_key) @@ -1476,7 +1475,7 @@ pub(crate) fn agent_has_managed_marker( } AgentClient::DeepSeekHarness => deepseek_harness_has_managed_marker(paths), AgentClient::WorkBuddy => workbuddy_has_managed_marker(&paths[0]), - AgentClient::AntigravityIde | AgentClient::AntigravityCli => antigravity_has_marker(client, paths), + AgentClient::AntigravityCli => antigravity_has_marker(client, paths), AgentClient::ZCode => { let prefix = format!("{MANAGED_AGENT_PROVIDER_ID}/"); for path in paths { @@ -2393,7 +2392,6 @@ pub(crate) fn find_windows_codex_app_executable(home: &Path) -> Option } pub(crate) fn find_agent_executable(client: AgentClient, home: &Path) -> Option { - if client == AgentClient::AntigravityIde { return find_antigravity_ide(home); } if client == AgentClient::AntigravityCli { return find_antigravity_cli(home); } if client == AgentClient::ClaudeDesktop { return find_claude_desktop_executable(home); diff --git a/src-tauri/src/agents/launch.rs b/src-tauri/src/agents/launch.rs index 00ba731..c62ade0 100644 --- a/src-tauri/src/agents/launch.rs +++ b/src-tauri/src/agents/launch.rs @@ -165,6 +165,31 @@ pub(crate) fn launch_agent( ); } + if let Some(cursor_client) = CursorClient::parse(&client) { + if requested_target.is_some_and(|target| target != cursor_client.target()) { + return Err(format!( + "{} 不支持 {} 启动方式", + cursor_client.name(), + requested_target.unwrap_or_default() + )); + } + let status = inspect_cursor_client(cursor_client, &home); + if !status.installed { + return Err(format!("未检测到 {},请先安装并重新检测", cursor_client.name())); + } + let executable = find_cursor_cli(&home) + .ok_or_else(|| "未找到 Cursor CLI 可执行文件".to_string())?; + let launch_directory = resolve_launch_directory(working_directory.as_deref(), &home)?; + return launch_cli_agent( + &executable, + cursor_client.name(), + &launch_directory, + &[], + &[], + &terminal, + ); + } + let client = AgentClient::parse(&client)?; if !client.supported_platform() { return Err(format!("当前平台不支持启动 {}", client.name())); @@ -181,10 +206,6 @@ pub(crate) fn launch_agent( let requested_target = requested_target.unwrap_or(default_target); match (client, requested_target) { (AgentClient::ClaudeDesktop, "app") => launch_claude_desktop(&home), - (AgentClient::AntigravityIde, "app") => { - let executable = find_antigravity_ide(&home).ok_or("未找到 Antigravity IDE")?; - launch_desktop_agent(&executable, client.name()) - } (AgentClient::AntigravityCli, "cli") if antigravity_has_marker(client, &agent_config_paths(client, &home))? => { if !status.configured { @@ -206,7 +227,7 @@ pub(crate) fn launch_agent( } (AgentClient::Codex, "app") => launch_codex_desktop(&home), (AgentClient::OpenCode, "app") => launch_opencode_desktop(&home), - (AgentClient::ClaudeDesktop | AgentClient::ZCode | AgentClient::WorkBuddy | AgentClient::AntigravityIde, "cli") => { + (AgentClient::ClaudeDesktop | AgentClient::ZCode | AgentClient::WorkBuddy, "cli") => { Err(format!("{} 不支持 CLI 启动方式", client.name())) } (_, "cli") => { @@ -706,6 +727,9 @@ pub(crate) async fn restart_opencode_app(app: tauri::AppHandle) -> Result<(), St #[tauri::command] pub(crate) async fn restart_agent_app(app: tauri::AppHandle, client: String) -> Result<(), String> { + if client.trim().eq_ignore_ascii_case("cursor-cli") { + return Err("Cursor CLI 不支持桌面应用重启".to_string()); + } let client = AgentClient::parse(&client)?; if !matches!( client, @@ -714,7 +738,6 @@ pub(crate) async fn restart_agent_app(app: tauri::AppHandle, client: String) -> | AgentClient::ClaudeDesktop | AgentClient::ZCode | AgentClient::WorkBuddy - | AgentClient::AntigravityIde ) { return Err(format!("{} 不支持桌面应用重启", client.name())); } @@ -761,7 +784,6 @@ fn find_desktop_restart_target( AgentClient::WorkBuddy => { find_workbuddy_desktop_executable(home).map(DesktopAppTarget::Application) } - AgentClient::AntigravityIde => find_antigravity_ide(home).map(DesktopAppTarget::Application), AgentClient::ClaudeDesktop => { let executable = find_claude_desktop_executable(home).map(DesktopAppTarget::Application); diff --git a/src-tauri/src/agents/state.rs b/src-tauri/src/agents/state.rs index 28681af..e4f3562 100644 --- a/src-tauri/src/agents/state.rs +++ b/src-tauri/src/agents/state.rs @@ -727,7 +727,7 @@ pub(crate) fn fresh_agent_contents_with_oauth( build_deepseek_harness_settings(None, &openai_base, model, models)?, build_deepseek_harness_credentials(None, api_key)?, ]), - AgentClient::AntigravityIde | AgentClient::AntigravityCli => Err("Antigravity 模板需要安装和用户目录信息".into()), + AgentClient::AntigravityCli => Err("Antigravity 模板需要安装和用户目录信息".into()), AgentClient::WorkBuddy => Ok(vec![build_workbuddy_agent_config( None, &openai_base, api_key, model, models, )?]), @@ -1541,7 +1541,7 @@ pub(crate) fn build_agent_template_updates( claude_code_model_mappings, claude_desktop_model_mappings, } = request; - if matches!(client, AgentClient::AntigravityIde | AgentClient::AntigravityCli) { + if client == AgentClient::AntigravityCli { return build_antigravity_updates(client, home, &managed_core_loopback_origin(port), api_key, model, true); } let paths = agent_config_paths(client, home); diff --git a/src-tauri/src/agents/windows_probe.rs b/src-tauri/src/agents/windows_probe.rs index 758a44e..bfa35c2 100644 --- a/src-tauri/src/agents/windows_probe.rs +++ b/src-tauri/src/agents/windows_probe.rs @@ -353,19 +353,6 @@ pub(crate) fn find_windows_registered_zcode_executable() -> Option { }) } -#[cfg(not(test))] -pub(crate) fn find_windows_registered_antigravity_ide() -> Option { - for executable in ["Antigravity IDE.exe", "Antigravity.exe"] { - if let Some(path) = collect_windows_desktop_registrations(executable, |name| { - name == "Antigravity IDE" || name.starts_with("Antigravity IDE (") - || name == "Antigravity" || name.starts_with("Antigravity (") - }).into_iter().find_map(|r| parse_windows_desktop_registration(r.kind, &r.value, executable)) { - return Some(path); - } - } - None -} - #[cfg(not(test))] pub(crate) fn find_windows_registered_workbuddy_executable() -> Option { for executable in ["WorkBuddyAI.exe", "WorkBuddy.exe"] { diff --git a/src-tauri/src/configuration_watcher.rs b/src-tauri/src/configuration_watcher.rs index 0efaa9f..43074cf 100644 --- a/src-tauri/src/configuration_watcher.rs +++ b/src-tauri/src/configuration_watcher.rs @@ -75,7 +75,6 @@ fn tracked_configuration_paths(app: &tauri::AppHandle) -> Result, S AgentClient::DeepSeekHarness, AgentClient::ZCode, AgentClient::WorkBuddy, - AgentClient::AntigravityIde, AgentClient::AntigravityCli, AgentClient::KimiCode, AgentClient::GrokBuild, diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index f712094..5ec6ee8 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -1363,7 +1363,6 @@ enum AgentClient { DeepSeekHarness, ZCode, WorkBuddy, - AntigravityIde, AntigravityCli, KimiCode, GrokBuild, @@ -1388,10 +1387,12 @@ impl AgentClient { "deepseek-harness" => Ok(Self::DeepSeekHarness), "zcode" => Ok(Self::ZCode), "workbuddy" => Ok(Self::WorkBuddy), - "antigravity-ide" => Ok(Self::AntigravityIde), "antigravity-cli" => Ok(Self::AntigravityCli), "kimi-code" => Ok(Self::KimiCode), "grok-build" => Ok(Self::GrokBuild), + "cursor-ide" | "cursor-cli" => { + Err(format!("Cursor 客户端暂不支持 CPA 托管配置: {value}")) + } _ => Err(format!("不支持的智能体客户端: {value}")), } } @@ -1407,7 +1408,6 @@ impl AgentClient { Self::DeepSeekHarness => "deepseek-harness", Self::ZCode => "zcode", Self::WorkBuddy => "workbuddy", - Self::AntigravityIde => "antigravity-ide", Self::AntigravityCli => "antigravity-cli", Self::KimiCode => "kimi-code", Self::GrokBuild => "grok-build", @@ -1425,7 +1425,6 @@ impl AgentClient { Self::DeepSeekHarness => "DeepSeek Harness", Self::ZCode => "ZCode", Self::WorkBuddy => "WorkBuddy", - Self::AntigravityIde => "Antigravity IDE", Self::AntigravityCli => "Antigravity CLI", Self::KimiCode => "Kimi Code", Self::GrokBuild => "Grok Build", @@ -1452,7 +1451,6 @@ impl AgentClient { Self::DeepSeekHarness => &["dsh"], Self::ZCode => &["zcode"], Self::WorkBuddy => &[], - Self::AntigravityIde => &[], Self::AntigravityCli => &["agy"], Self::KimiCode => &["kimi"], Self::GrokBuild => &["grok"], diff --git a/src-tauri/src/tests/agent_paths.rs b/src-tauri/src/tests/agent_paths.rs index ea58320..e2490c6 100644 --- a/src-tauri/src/tests/agent_paths.rs +++ b/src-tauri/src/tests/agent_paths.rs @@ -14,7 +14,6 @@ fn configuration_paths_ignore_inherited_environment() { "deepseek-harness", "zcode", "workbuddy", - "antigravity-ide", "antigravity-cli", "kimi-code", "grok-build", diff --git a/src/assets/icons/cursor.svg b/src/assets/icons/cursor.svg new file mode 100644 index 0000000..3810b3e --- /dev/null +++ b/src/assets/icons/cursor.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/i18n/ja.ts b/src/i18n/ja.ts index cafd4f8..48942c3 100644 --- a/src/i18n/ja.ts +++ b/src/i18n/ja.ts @@ -937,9 +937,9 @@ export const jaOverrides = { 'agents.description.openclaw': 'CPA モデルプロバイダーと既定モデルを使用', 'agents.description.hermes': 'CPA カスタムプロバイダーを使用', 'agents.description.deepseekHarness': 'Harness のネイティブ DeepSeek 設定と分離された OpenAI 互換ルートで CPA を使用', - 'agents.description.antigravityIde': 'CPA の Gemini 互換 API で Antigravity IDE を設定', + 'agents.description.cursorCli': 'Cursor CLI (cursor-agent) の検出とネイティブ起動に対応', + 'agents.modify.cursorCliHint': 'Cursor CLI は cursor-agent 経由で起動され、~/.cursor/cli-config.json で設定されます。CPA ではローカルインストールの検出と、任意のディレクトリやターミナル選択からの起動をサポートしています。', 'agents.description.antigravityCli': 'CPA の Gemini 互換 API で Antigravity CLI を設定', - 'agents.modify.antigravityIdeHint': '設定の適用・解除後は IDE を再起動してください。リクエストにはここで選択した CPA モデルが使用され、IDE 内のモデル表示名は変わりません。初回は IDE へのログインが必要です。', 'agents.modify.antigravityCliHint': '設定適用後はこのページの起動ボタンを使用してください。CPA が CLI プロセスに接続先とキーを渡します。agy を直接実行する場合は環境変数の設定が必要です。', 'agents.description.workbuddy': 'CPA の OpenAI Chat Completions API で WorkBuddy のカスタムモデルを設定', 'agents.modify.workbuddyHint': '適用後、WorkBuddy で「CPA:モデル名」を選択してください。表示されない場合は App を再起動してください。既存の会話のモデルは自動で切り替わりません。', diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts index 399fdb0..7fa917a 100644 --- a/src/i18n/locales/en.ts +++ b/src/i18n/locales/en.ts @@ -937,9 +937,9 @@ export const en: Record = { 'agents.description.openclaw': 'Uses the CPA model provider and default model', 'agents.description.hermes': 'Uses the CPA custom provider', 'agents.description.deepseekHarness': 'Uses CPA through an isolated, OpenAI-compatible Harness route', - 'agents.description.antigravityIde': 'Configure Antigravity IDE with CPA’s Gemini-compatible API', + 'agents.description.cursorCli': 'Supports detection and native launch for Cursor CLI (cursor-agent)', + 'agents.modify.cursorCliHint': 'Cursor CLI runs via cursor-agent and is configured in ~/.cursor/cli-config.json. CPA supports local installation detection and launch with custom terminal and working directory.', 'agents.description.antigravityCli': 'Configure Antigravity CLI with CPA’s Gemini-compatible API', - 'agents.modify.antigravityIdeHint': 'Restart the IDE after applying or disabling configuration. Requests use the CPA model selected here; model labels inside the IDE stay unchanged. Initial IDE sign-in is still required.', 'agents.modify.antigravityCliHint': 'Use Launch on this page after applying configuration. CPA supplies the endpoint and key to that CLI process. Running agy directly requires setting the environment variables yourself.', 'agents.description.workbuddy': 'Configures a WorkBuddy custom model using the CPA OpenAI Chat Completions API', 'agents.modify.workbuddyHint': 'After applying, select “CPA:model name” in WorkBuddy. Restart the app if it is missing; existing conversations keep their selected model.', diff --git a/src/i18n/locales/zh-CN.ts b/src/i18n/locales/zh-CN.ts index 9f3ed41..52f12cb 100644 --- a/src/i18n/locales/zh-CN.ts +++ b/src/i18n/locales/zh-CN.ts @@ -936,9 +936,9 @@ export const zhCN = { 'agents.description.openclaw': '使用 CPA 模型供应商和默认模型', 'agents.description.hermes': '使用 CPA 自定义提供方', 'agents.description.deepseekHarness': '通过通用 OpenAI 兼容路由使用 CPA,并与 Harness 原生 DeepSeek 配置隔离', - 'agents.description.antigravityIde': '使用 CPA 的 Gemini 兼容接口配置 Antigravity IDE', + 'agents.description.cursorCli': '支持 Cursor CLI (cursor-agent) 的检测与原生启动', + 'agents.modify.cursorCliHint': 'Cursor CLI 通过 cursor-agent 运行,使用 ~/.cursor/cli-config.json 配置。CPA 支持本地安装检测,并支持自定义终端与工作目录启动。', 'agents.description.antigravityCli': '使用 CPA 的 Gemini 兼容接口配置 Antigravity CLI', - 'agents.modify.antigravityIdeHint': '应用或关闭配置后请重启 IDE。模型请求使用此处选择的 CPA 模型;IDE 内的模型标签保持原样。首次使用仍需完成 IDE 登录。', 'agents.modify.antigravityCliHint': '应用后请使用此页的启动按钮,CPA 会为本次 CLI 进程注入地址和密钥。直接运行 agy 需要自行设置环境变量。', 'agents.description.workbuddy': '使用 CPA 的 OpenAI Chat Completions 接口配置 WorkBuddy 自定义模型', 'agents.modify.workbuddyHint': '应用后,在 WorkBuddy 的模型选择器中选择「CPA:模型名」。若未显示,请重启 App;已有会话的模型不会自动切换。', diff --git a/src/pages/AgentsPage.tsx b/src/pages/AgentsPage.tsx index fd96dad..e2c4e34 100644 --- a/src/pages/AgentsPage.tsx +++ b/src/pages/AgentsPage.tsx @@ -38,6 +38,7 @@ import piIcon from '../assets/icons/pi-logo-on-light.svg'; import zcodeIcon from '../assets/icons/zcode.png'; import workbuddyIcon from '../assets/icons/workbuddy.png'; import antigravityIcon from '../assets/icons/antigravity.svg'; +import cursorIcon from '../assets/icons/cursor.svg'; import { agentModelAlias, filterAgentModels, @@ -82,8 +83,8 @@ type AgentClientId = | 'deepseek-harness' | 'zcode' | 'workbuddy' - | 'antigravity-ide' | 'antigravity-cli' + | 'cursor-cli' | 'kimi-code' | 'grok-build' | 'pi'; @@ -233,7 +234,7 @@ type AgentDefinition = { name: string; icon?: string; Icon?: ComponentType<{ size?: number; 'aria-hidden'?: boolean }>; - descriptionKey: 'agents.description.claudeCode' | 'agents.description.claudeDesktop' | 'agents.description.codex' | 'agents.description.opencode' | 'agents.description.openclaw' | 'agents.description.hermes' | 'agents.description.deepseekHarness' | 'agents.description.zcode' | 'agents.description.workbuddy' | 'agents.description.antigravityIde' | 'agents.description.antigravityCli' | 'agents.description.kimiCode' | 'agents.description.grokBuild' | 'agents.description.pi'; + descriptionKey: 'agents.description.claudeCode' | 'agents.description.claudeDesktop' | 'agents.description.codex' | 'agents.description.opencode' | 'agents.description.openclaw' | 'agents.description.hermes' | 'agents.description.deepseekHarness' | 'agents.description.zcode' | 'agents.description.workbuddy' | 'agents.description.antigravityCli' | 'agents.description.cursorCli' | 'agents.description.kimiCode' | 'agents.description.grokBuild' | 'agents.description.pi'; }; type AgentSubpageId = 'core' | 'management' | 'sessions'; @@ -287,8 +288,8 @@ const agentDefinitions: AgentDefinition[] = [ icon: grokIcon, descriptionKey: 'agents.description.grokBuild', }, - { id: 'antigravity-ide', name: 'Antigravity IDE', icon: antigravityIcon, descriptionKey: 'agents.description.antigravityIde' }, { id: 'antigravity-cli', name: 'Antigravity CLI', icon: antigravityIcon, descriptionKey: 'agents.description.antigravityCli' }, + { id: 'cursor-cli', name: 'Cursor CLI', icon: cursorIcon, descriptionKey: 'agents.description.cursorCli' }, { id: 'workbuddy', name: 'WorkBuddy', @@ -329,6 +330,21 @@ const agentSubpages: AgentSubpageDefinition[] = [ { id: 'management', labelKey: 'agents.tabs.management', + clients: [ + 'claude-code', + 'claude-desktop', + 'codex', + 'opencode', + 'openclaw', + 'hermes', + 'deepseek-harness', + 'zcode', + 'workbuddy', + 'antigravity-cli', + 'kimi-code', + 'grok-build', + 'pi', + ], }, { id: 'sessions', @@ -796,8 +812,11 @@ export function AgentsPage({ embedded = false, onConfigurationApplied }: AgentsP const [viewStateByClient, setViewStateByClient] = useState(() => agentViewStateCache); const viewMode = embedded ? 'embedded' : 'full'; const viewState = viewStateByClient[viewMode][selected] ?? DEFAULT_AGENT_VIEW_STATE; - const activeSubpage = viewState.subpage === 'sessions' && (embedded || selected !== 'codex') - ? DEFAULT_AGENT_SUBPAGE : viewState.subpage; + const isCursorClient = selected === 'cursor-cli'; + const activeSubpage = isCursorClient + ? DEFAULT_AGENT_SUBPAGE + : viewState.subpage === 'sessions' && (embedded || selected !== 'codex') + ? DEFAULT_AGENT_SUBPAGE : viewState.subpage; const updateViewState = (patch: Partial) => { setViewStateByClient((current) => { const next = { @@ -919,6 +938,12 @@ export function AgentsPage({ embedded = false, onConfigurationApplied }: AgentsP }, []); const loadModels = useCallback(async (client: AgentClientId, preferredModel = '') => { + if (client === 'cursor-cli') { + setModelLoading(false); + setModelError(''); + setModels([]); + return; + } const requestId = modelRequestRef.current + 1; modelRequestRef.current = requestId; setModelLoading(true); @@ -1223,7 +1248,6 @@ export function AgentsPage({ embedded = false, onConfigurationApplied }: AgentsP const modificationDescription = activeStatus?.modificationState === 'invalid' ? t('agents.modify.invalid') : nativeOauth ? t('agents.nativeOAuth.activeHint') - : selected === 'antigravity-ide' ? t('agents.modify.antigravityIdeHint') : selected === 'antigravity-cli' ? t('agents.modify.antigravityCliHint') : selected === 'workbuddy' ? t('agents.modify.workbuddyHint') @@ -1748,7 +1772,7 @@ export function AgentsPage({ embedded = false, onConfigurationApplied }: AgentsP }; const restartDesktopApp = async () => { - if (!['codex', 'opencode', 'claude-desktop', 'zcode', 'workbuddy', 'antigravity-ide'].includes(selected)) return; + if (!['codex', 'opencode', 'claude-desktop', 'zcode', 'workbuddy'].includes(selected)) return; setBusyAction('restart-app'); setLaunchError(''); try { @@ -2030,7 +2054,8 @@ export function AgentsPage({ embedded = false, onConfigurationApplied }: AgentsP
-
+ {availableSubpages.length > 1 ? ( +
{availableSubpages.map((subpage) => (
+ ) : null} {embedded && activeSubpage === 'core' ? (
@@ -2073,7 +2099,13 @@ export function AgentsPage({ embedded = false, onConfigurationApplied }: AgentsP - {selected === 'claude-desktop' ? desktopModelEditor :
+ {isCursorClient ? ( +
+

+ {t('agents.modify.cursorCliHint')} +

+
+ ) : selected === 'claude-desktop' ? desktopModelEditor :
{t('agents.harness.defaultHint')}

: null} -
- {configurationFeedback} -
- {closeConfigurationButton} - + {!isCursorClient ? ( +
+ {configurationFeedback} +
+ {closeConfigurationButton} + +
-
+ ) : null}
) : null} @@ -2158,7 +2192,7 @@ export function AgentsPage({ embedded = false, onConfigurationApplied }: AgentsP ) : (
- {t('agents.clientVersion')} + {selected === 'cursor-cli' ? t('agents.cliVersion') : t('agents.clientVersion')} {activeStatus?.version ?? t('agents.notFetched')}
)} @@ -2166,7 +2200,18 @@ export function AgentsPage({ embedded = false, onConfigurationApplied }: AgentsP - {!isClaudeModelMappingClient ? ( + {isCursorClient ? ( +
+
+
{t('agents.description.cursorCli')}
+
+

+ {t('agents.modify.cursorCliHint')} +

+
+ ) : null} + + {!isCursorClient && !isClaudeModelMappingClient ? (
{t(isDeepSeekHarnessClient ? 'agents.harness.defaultModel' : 'agents.useModel')}
@@ -2396,9 +2441,10 @@ export function AgentsPage({ embedded = false, onConfigurationApplied }: AgentsP
) : null} + {!isCursorClient ? (
{configurationFeedback} -
+
{closeConfigurationButton}
+ ) : null}
) : null} @@ -2443,7 +2490,7 @@ export function AgentsPage({ embedded = false, onConfigurationApplied }: AgentsP { setConfigurationNotice(''); setClearNotice(''); }} /> {activeSubpage === 'core' ? void launchAgent(target)} onRestart={() => void restartDesktopApp()} diff --git a/tests/agent-management-ui.cjs b/tests/agent-management-ui.cjs index 28fec6a..169c5ac 100644 --- a/tests/agent-management-ui.cjs +++ b/tests/agent-management-ui.cjs @@ -66,7 +66,7 @@ const path = require('node:path'); assert.equal(await tab('基础配置').getAttribute('aria-selected'), 'true'); assert.equal(await tab('会话管理').count(), 0); - for (const client of ['claude-desktop', 'zcode', 'workbuddy', 'antigravity-ide', 'opencode']) { + for (const client of ['claude-desktop', 'zcode', 'workbuddy', 'opencode']) { await open(mode + 'client=' + client + '&app-only'); assert.ok(await button('重启 App').isEnabled()); await button('重启 App').click(); @@ -80,7 +80,7 @@ const path = require('node:path'); await open(mode + 'client=kimi-code'); assert.equal(await button('重启 App').count(), 0); - for (const client of ['claude-code', 'claude-desktop', 'opencode', 'openclaw', 'hermes', 'deepseek-harness', 'zcode', 'workbuddy', 'antigravity-ide', 'antigravity-cli', 'kimi-code', 'grok-build']) { + for (const client of ['claude-code', 'claude-desktop', 'opencode', 'openclaw', 'hermes', 'deepseek-harness', 'zcode', 'workbuddy', 'antigravity-cli', 'kimi-code', 'grok-build']) { await open(mode + 'client=' + client); assert.ok(await button('关闭配置修改').isEnabled()); await tab('配置管理').click(); diff --git a/tests/cursor-agents-ui.cjs b/tests/cursor-agents-ui.cjs new file mode 100644 index 0000000..6d95594 --- /dev/null +++ b/tests/cursor-agents-ui.cjs @@ -0,0 +1,87 @@ +const { chromium } = require(process.env.PLAYWRIGHT_MODULE || 'playwright'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const os = require('node:os'); +const path = require('node:path'); + +(async () => { + const browser = await chromium.launch({ channel: 'msedge', headless: true }); + const screenshots = path.join(os.tmpdir(), 'cpa-cursor-ui-review'); + fs.mkdirSync(screenshots, { recursive: true }); + try { + const page = await browser.newPage({ viewport: { width: 1280, height: 900 } }); + const errors = []; + page.on('pageerror', error => errors.push(String(error))); + + const open = async (query = '') => { + await page.addInitScript(() => { + localStorage.setItem('cpa-gui.agent-launch-directory-history.v1', JSON.stringify({ + 'cursor-cli': ['C:\\test\\workspace'] + })); + }); + await page.goto('http://localhost:1421/tests/fixtures/agent-backups.html?' + query); + await page.waitForFunction(() => { + const refresh = document.querySelector('.agent-header-actions button'); + return refresh && !refresh.disabled + && window.fixtureCalls.some(call => call.cmd === 'get_agent_config_statuses'); + }); + }; + + const button = name => page.getByRole('button', { name, exact: true }); + const calls = cmd => page.evaluate(cmd => window.fixtureCalls.filter(call => call.cmd === cmd), cmd); + + const BTN_MANUAL_BACKUP = '\u624b\u52a8\u5907\u4efd'; + const BTN_RESTORE_BACKUP = '\u6062\u590d\u5907\u4efd'; + const BTN_APPLY_TEMPLATE = '\u5e94\u7528 ezcpa \u6a21\u677f'; + const BTN_UPDATE_CONFIG = '\u66f4\u65b0\u914d\u7f6e'; + const BTN_CLEAR_CONFIG = '\u6e05\u7a7a\u914d\u7f6e'; + const BTN_LAUNCH_CLI = '\u542f\u52a8 CLI'; + + for (const mode of ['', 'embedded&']) { + await open(mode + 'client=cursor-cli'); + + assert.equal(await page.locator('.agent-list-items').getByText('Cursor IDE').count(), 0); + assert.ok(await page.locator('.agent-list-items').getByText('Cursor CLI').isVisible()); + + assert.equal(await page.locator('.agent-model-trigger').count(), 0); + assert.equal(await page.locator('.agent-model-search').count(), 0); + assert.equal(await button(BTN_MANUAL_BACKUP).count(), 0); + assert.equal(await button(BTN_RESTORE_BACKUP).count(), 0); + assert.equal(await button(BTN_APPLY_TEMPLATE).count(), 0); + assert.equal(await button(BTN_UPDATE_CONFIG).count(), 0); + assert.equal(await button(BTN_CLEAR_CONFIG).count(), 0); + + const cliModelCalls = (await calls('get_agent_models')).filter(c => c.args && c.args.client === 'cursor-cli'); + assert.equal(cliModelCalls.length, 0, 'Cursor CLI must not fetch CPA models'); + + assert.ok(await button(BTN_LAUNCH_CLI).isVisible()); + assert.equal(await page.getByRole('button', { name: '\u91cd\u542f App', exact: true }).count(), 0); + + await button(BTN_LAUNCH_CLI).click(); + const dialog = page.locator('.agent-launch-directory-dialog'); + await dialog.waitFor(); + assert.ok(await dialog.isVisible()); + + const dialogLaunchBtn = dialog.locator('button.primary-button'); + await dialogLaunchBtn.click(); + await dialog.waitFor({ state: 'hidden' }); + + await page.waitForFunction(() => { + return window.fixtureCalls.some(call => call.cmd === 'launch_agent' && call.args && call.args.client === 'cursor-cli'); + }); + const cliLaunchCalls = (await calls('launch_agent')).filter(c => c.args && c.args.client === 'cursor-cli'); + assert.ok(cliLaunchCalls.length > 0); + assert.equal(cliLaunchCalls[cliLaunchCalls.length - 1].args.target, 'cli'); + + await page.screenshot({ path: path.join(screenshots, 'cursor-' + (mode ? 'embedded' : 'full') + '.png'), fullPage: true }); + } + + assert.equal(errors.length, 0, 'Page errors: ' + errors.join(', ')); + console.log('Cursor agents UI test passed successfully in both full and embedded modes!'); + } finally { + await browser.close(); + } +})().catch(error => { + console.error(error); + process.exit(1); +}); diff --git a/tests/fixtures/agent-backups.tsx b/tests/fixtures/agent-backups.tsx index 40040d3..368c837 100644 --- a/tests/fixtures/agent-backups.tsx +++ b/tests/fixtures/agent-backups.tsx @@ -9,7 +9,7 @@ if(params.has('reset-selections'))localStorage.removeItem('cpa-gui.agent-model-s localStorage.setItem('easy-cli-proxy-api.locale',params.get('locale') || 'zh-CN'); document.documentElement.dataset.theme = params.get('theme') || 'light'; localStorage.setItem('cpa-gui.agent-selected-client.v1', params.get('client') || 'codex'); -const ids = ['claude-code','claude-desktop','codex','opencode','openclaw','hermes','deepseek-harness','antigravity-ide','antigravity-cli','workbuddy','zcode','kimi-code','grok-build','pi']; +const ids = ['claude-code','claude-desktop','codex','opencode','openclaw','hermes','deepseek-harness','antigravity-cli','cursor-cli','workbuddy','zcode','kimi-code','grok-build','pi']; let count=0; let backupCount=0; const backups:any[]=[]; let currentModel=params.has('fresh')?null:'gpt-one'; let nativeOauth=params.has('native-oauth'); let nativeSwitchCount=0; let codexClosed=false; let closeCount=0; let currentOauth=false; const currentMappings:Record={}; @@ -35,7 +35,7 @@ mockIPC(async (cmd,args:any) => { calls.push({cmd,args}); if(cmd==='plugin:event|listen') return 1; if(cmd==='plugin:event|unlisten'||cmd==='set_app_locale') return null; - if(cmd==='get_agent_config_statuses'||cmd==='refresh_agent_config_statuses') return ids.map(id=>({id,name:id,supportedPlatform:true,installed:!params.has('not-installed'),pluginInstalled:!params.has('no-plugin'),launchTargets:params.has('not-installed')?[]:['claude-desktop','zcode','workbuddy','antigravity-ide'].includes(id)?[{id:'app',label:id,detail:'test desktop'}]:['codex','opencode'].includes(id)&&!params.has('cli-only')?[...(params.has('app-only')?[]:[{id:'cli',label:'CLI',detail:'test CLI'}]),{id:'app',label:'APP',detail:'test desktop'}]:[{id:'cli',label:'CLI',detail:'test CLI'}],version:'1.0',cliVersion:'1.0',appVersion:null,pluginVersion:'1.0',configValid:params.get('state')!=='invalid',connectionState:params.get('state') || (currentModel?'configured':'not-configured'),configured:!!currentModel,configurationSynchronized:!!currentModel,currentModel,oauthConfiguration:id==='codex'&¤tOauth,codexNativeOauth:id==='codex'&&nativeOauth,modificationEnabled:!!currentModel,modificationState:currentModel?'applied':'unconfigured',backupAvailable:false,appliedModel:currentModel,claudeCodeModelMappings:id==='claude-code'?currentMappings[id]??null:null,claudeDesktopModelMappings:id==='claude-desktop'?currentMappings[id]??null:null,warnings:[],error:null})).map(status=>status.id==='codex'&&(nativeOauth||codexClosed)?{...status,configured:false,connectionState:'not-configured',configurationSynchronized:false,currentModel:null,appliedModel:null,modificationEnabled:false,modificationState:'unconfigured',oauthConfiguration:false}:status); + if(cmd==='get_agent_config_statuses'||cmd==='refresh_agent_config_statuses') return ids.map(id=>({id,name:id,supportedPlatform:true,installed:!params.has('not-installed'),pluginInstalled:!params.has('no-plugin'),launchTargets:params.has('not-installed')?[]:['claude-desktop','zcode','workbuddy'].includes(id)?[{id:'app',label:id,detail:'test desktop'}]:['codex','opencode'].includes(id)&&!params.has('cli-only')?[...(params.has('app-only')?[]:[{id:'cli',label:'CLI',detail:'test CLI'}]),{id:'app',label:'APP',detail:'test desktop'}]:[{id:'cli',label:'CLI',detail:'test CLI'}],version:'1.0',cliVersion:'1.0',appVersion:null,pluginVersion:'1.0',configValid:params.get('state')!=='invalid',connectionState:params.get('state') || (currentModel?'configured':'not-configured'),configured:!!currentModel,configurationSynchronized:!!currentModel,currentModel,oauthConfiguration:id==='codex'&¤tOauth,codexNativeOauth:id==='codex'&&nativeOauth,modificationEnabled:!!currentModel,modificationState:currentModel?'applied':'unconfigured',backupAvailable:false,appliedModel:currentModel,claudeCodeModelMappings:id==='claude-code'?currentMappings[id]??null:null,claudeDesktopModelMappings:id==='claude-desktop'?currentMappings[id]??null:null,warnings:[],error:null})).map(status=>status.id==='codex'&&(nativeOauth||codexClosed)?{...status,configured:false,connectionState:'not-configured',configurationSynchronized:false,currentModel:null,appliedModel:null,modificationEnabled:false,modificationState:'unconfigured',oauthConfiguration:false}:status); if(cmd==='close_codex_config_modification') { closeCount++; if(params.has('fail-close')&&closeCount===1)throw new Error('模拟关闭失败,已回滚');