feat(agents): add Cursor CLI support and remove Antigravity and Cursor IDE integrations

This commit is contained in:
lzt404 committed 2026-09-21 21:58:01 +08:00
1 parent 84c29782b0
commit 8901eff9bb
24 files changed
+530 -634

No files matched your search

+2 -2
View File
@@ -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` を直接実行する場合は環境変数を自分で設定してください。
## その他の機能
+4 -2
View File
@@ -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
+2 -2
View File
@@ -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` 时需自行设置环境变量。
## 其他功能
+2
View File
@@ -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::*;
+92 -321
View File
@@ -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<PathBuf> {
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::<Value>(&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<PathBuf> {
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<PathBuf> {
@@ -83,90 +29,6 @@ pub(crate) fn find_antigravity_cli(home: &Path) -> Option<PathBuf> {
}
}
pub(crate) fn find_antigravity_ide(home: &Path) -> Option<PathBuf> {
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<PathBuf> {
#[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<String> {
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<PathBuf> {
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<String, String> {
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<Vec<AgentFileUpdate>, 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<Vec<&'static str>> {
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<Vec<&'static str>> {
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<bool, String> {
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>), 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<String> {
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<Command, String> {
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<i32,
let mut command = antigravity_helper_command(args)?;
#[cfg(target_os = "windows")]
{
use std::os::windows::process::CommandExt;
if args.get(1).is_some_and(|a| a == "--cpa-antigravity-cli") {
unsafe {
windows_sys::Win32::System::Console::AttachConsole(u32::MAX);
}
} else {
command.creation_flags(0x0800_0000);
unsafe {
windows_sys::Win32::System::Console::AttachConsole(u32::MAX);
}
}
#[cfg(unix)]
+78 -226
View File
@@ -22,8 +22,6 @@ impl Home {
fn apply(&self, client: AgentClient, model: &str) {
let paths = config_paths(client.id(), &self.0).unwrap();
let before = config_images(&paths).unwrap();
let server = self.0.join("language_server.exe");
fs::write(&server, b"fixture").unwrap();
let updates = paths
.iter()
.map(|path| AgentFileUpdate {
@@ -35,7 +33,6 @@ impl Home {
"http://127.0.0.1:8317",
"test-key",
model,
Some(&server),
)
.unwrap(),
})
@@ -63,169 +60,96 @@ impl Drop for Home {
}
#[test]
fn antigravity_clients_have_independent_paths_and_launch_targets() {
fn antigravity_cli_paths_and_launch_targets() {
let home = Home::new();
let ide = antigravity_config_paths(AgentClient::AntigravityIde, &home.0);
let cli = antigravity_config_paths(AgentClient::AntigravityCli, &home.0);
assert!(ide
.iter()
.all(|p| !cli.contains(p) && p.starts_with(&home.0)));
assert_eq!(cli.len(), 2);
assert_eq!(cli[0], home.0.join(".gemini/antigravity-cli/settings.json"));
for (client, target) in [
(AgentClient::AntigravityIde, "app"),
(AgentClient::AntigravityCli, "cli"),
] {
let targets = agent_launch_targets(client, Some(Path::new("fixture")), None, false);
assert_eq!(targets.len(), 1);
assert_eq!(targets[0].id, target);
}
assert!(!should_probe_primary_agent_executable_version(
AgentClient::AntigravityIde
));
assert_eq!(
cli[1],
home.0.join(".gemini/antigravity-cli/cpa-connection.json")
);
let targets =
agent_launch_targets(AgentClient::AntigravityCli, Some(Path::new("fixture")), None, false);
assert_eq!(targets.len(), 1);
assert_eq!(targets[0].id, "cli");
assert!(should_probe_primary_agent_executable_version(
AgentClient::AntigravityCli
));
}
#[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] {
let home = Home::new();
let paths = config_paths(client.id(), &home.0).unwrap();
let original = if client == AgentClient::AntigravityIde {
json!({"editor.fontSize": 15, IDE_BINARY_KEY: "original-server", IDE_ENV_KEY: {"KEEP":"original", "GEMINI_API_KEY":"original-key"}})
} else {
json!({"modelProvider":"original", "colorScheme":"dark", "customModelsConfig":{"customModels":{"User Model":{"modelName":"user"},CLI_MODEL_LABEL:{"modelName":"original", "maxTokens":123}}}})
};
home.save(&paths[0], &original);
home.apply(client, "model-one");
let applied = config_images(&paths).unwrap();
home.apply(client, "model-one");
assert_eq!(config_images(&paths).unwrap(), applied);
let mut current =
parse(&paths[0], read_optional_text(&paths[0]).unwrap().as_deref()).unwrap();
current["user-added"] = json!({"keep":[1,2]});
home.save(&paths[0], &current);
home.apply(client, "model-two");
assert!(
!inspect_antigravity_config(client, &paths, 8318, "test-key")
.unwrap()
.0
);
assert!(
!inspect_antigravity_config(client, &paths, 8317, "other-key")
.unwrap()
.0
);
clear_agent_managed_configuration(client, &home.0, 8317).unwrap();
let restored = parse(&paths[0], read_optional_text(&paths[0]).unwrap().as_deref()).unwrap();
let mut expected = original;
expected["user-added"] = json!({"keep":[1,2]});
assert_eq!(restored, expected);
if client == AgentClient::AntigravityCli {
assert!(!paths[1].exists());
let client = AgentClient::AntigravityCli;
let home = Home::new();
let paths = config_paths(client.id(), &home.0).unwrap();
let original = json!({
"modelProvider": "original",
"colorScheme": "dark",
"customModelsConfig": {
"customModels": {
"User Model": {"modelName": "user"},
CLI_MODEL_LABEL: {"modelName": "original", "maxTokens": 123}
}
}
assert!(!antigravity_has_marker(client, &paths).unwrap());
}
});
home.save(&paths[0], &original);
home.apply(client, "model-one");
let applied = config_images(&paths).unwrap();
home.apply(client, "model-one");
assert_eq!(config_images(&paths).unwrap(), applied);
let mut current =
parse(&paths[0], read_optional_text(&paths[0]).unwrap().as_deref()).unwrap();
current["user-added"] = json!({"keep": [1, 2]});
home.save(&paths[0], &current);
home.apply(client, "model-two");
assert!(
!inspect_antigravity_config(client, &paths, 8318, "test-key")
.unwrap()
.0
);
assert!(
!inspect_antigravity_config(client, &paths, 8317, "other-key")
.unwrap()
.0
);
clear_agent_managed_configuration(client, &home.0, 8317).unwrap();
let restored = parse(&paths[0], read_optional_text(&paths[0]).unwrap().as_deref()).unwrap();
let mut expected = original;
expected["user-added"] = json!({"keep": [1, 2]});
assert_eq!(restored, expected);
assert!(!paths[1].exists());
assert!(!antigravity_has_marker(client, &paths).unwrap());
}
#[test]
fn antigravity_manual_backup_restores_only_its_client() {
let home = Home::new();
for client in [AgentClient::AntigravityIde, AgentClient::AntigravityCli] {
home.apply(client, "model-one");
}
let ide_paths = config_paths("antigravity-ide", &home.0).unwrap();
let client = AgentClient::AntigravityCli;
home.apply(client, "model-one");
let cli_paths = config_paths("antigravity-cli", &home.0).unwrap();
let ide = config_images(&ide_paths).unwrap();
let cli = config_images(&cli_paths).unwrap();
let backup = create_backup("antigravity-cli", &home.0).unwrap();
home.apply(AgentClient::AntigravityCli, "model-two");
test_restore_backup(AgentClient::AntigravityCli, &home.0, &backup.id);
home.apply(client, "model-two");
test_restore_backup(client, &home.0, &backup.id);
assert_eq!(config_images(&cli_paths).unwrap(), cli);
clear_agent_managed_configuration(AgentClient::AntigravityCli, &home.0, 8317).unwrap();
assert_eq!(config_images(&ide_paths).unwrap(), ide);
clear_agent_managed_configuration(client, &home.0, 8317).unwrap();
}
#[test]
fn antigravity_rejects_malformed_owned_containers() {
let path = Path::new("settings.json");
for (client, input) in [
(
AgentClient::AntigravityCli,
json!({"customModelsConfig":[]}),
),
(
AgentClient::AntigravityCli,
json!({"customModelsConfig":{"customModels":false}}),
),
(AgentClient::AntigravityIde, json!({IDE_ENV_KEY:"invalid"})),
for input in [
json!({"customModelsConfig": []}),
json!({"customModelsConfig": {"customModels": false}}),
] {
assert!(build_antigravity_config(
client,
AgentClient::AntigravityCli,
path,
Some(&input.to_string()),
Some(&serde_json::to_string(&input).unwrap()),
"http://127.0.0.1:8317",
"key",
"model",
Some(Path::new("server"))
"test-key",
"model-one",
)
.is_err());
}
@@ -310,98 +234,26 @@ fn antigravity_cli_helper_uses_managed_credentials_and_model() {
assert!(antigravity_helper_command(&args).is_err(), "{key}");
}
fs::write(&paths[1], valid).unwrap();
home.save(&paths[0], &json!({"modelProvider":"other"}));
home.save(&paths[0], &json!({"modelProvider": "other"}));
assert!(antigravity_helper_command(&args).is_err());
}
#[cfg(windows)]
#[test]
fn antigravity_ide_discovery_and_template_use_native_server() {
let home = Home::new();
let install = home.0.join("AppData/Local/Programs/Antigravity IDE");
let executable = install.join("Antigravity IDE.exe");
let resources = install.join("resources/app");
let server = resources.join("extensions/antigravity/bin/language_server_windows_x64.exe");
fs::create_dir_all(server.parent().unwrap()).unwrap();
fs::write(&server, []).unwrap();
fs::write(&executable, []).unwrap();
home.save(
&resources.join("product.json"),
&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!(
read_antigravity_ide_version(&executable).as_deref(),
Some("2.5.5")
);
assert_eq!(
antigravity_language_server(&executable),
Some(server.clone())
);
let updates = build_agent_template_updates(AgentDefaultConfiguration {
client: AgentClient::AntigravityIde,
home: &home.0,
port: 8317,
api_key: "test-key",
model: "model-one",
models: &[],
codex_catalog: None,
oauth_configuration: false,
claude_code_model_mappings: None,
claude_desktop_model_mappings: None,
})
.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()),
server.as_path()
);
assert_eq!(value[IDE_ENV_KEY][IDE_MODEL_ENV], "model-one");
assert_eq!(value[IDE_BINARY_KEY], json!(env::current_exe().unwrap()));
fs::remove_file(&server).unwrap();
assert!(build_antigravity_updates(
AgentClient::AntigravityIde,
&home.0,
"http://127.0.0.1:8317",
"key",
"model",
false
)
.is_err());
}
#[test]
fn antigravity_removal_without_history_keeps_unmanaged_settings() {
for client in [AgentClient::AntigravityIde, AgentClient::AntigravityCli] {
let home = Home::new();
home.apply(client, "model-one");
let paths = antigravity_config_paths(client, &home.0);
let mut settings =
parse(&paths[0], read_optional_text(&paths[0]).unwrap().as_deref()).unwrap();
settings["externalSetting"] = json!(42);
home.save(&paths[0], &settings);
let removals = prepare_antigravity_removal(client, &paths).unwrap();
let restored = parse(
&paths[0],
Some(std::str::from_utf8(removals[0].1.as_ref().unwrap()).unwrap()),
)
.unwrap();
assert_eq!(restored, json!({"externalSetting":42}));
if client == AgentClient::AntigravityCli {
assert!(removals[1].1.is_none());
}
}
let client = AgentClient::AntigravityCli;
let home = Home::new();
home.apply(client, "model-one");
let paths = antigravity_config_paths(client, &home.0);
let mut settings =
parse(&paths[0], read_optional_text(&paths[0]).unwrap().as_deref()).unwrap();
settings["externalSetting"] = json!(42);
home.save(&paths[0], &settings);
let removals = prepare_antigravity_removal(client, &paths).unwrap();
let restored = parse(
&paths[0],
Some(std::str::from_utf8(removals[0].1.as_ref().unwrap()).unwrap()),
)
.unwrap();
assert_eq!(restored, json!({"externalSetting": 42}));
assert!(removals[1].1.is_none());
}
+5 -1
View File
@@ -9,6 +9,7 @@ static CODEX_CATALOG_REFRESH: tokio::sync::Notify = tokio::sync::Notify::const_n
enum AgentStatusDetectionTarget {
Client(AgentClient),
PiProvider,
Cursor(CursorClient),
}
pub(crate) fn inspect_agent_config_statuses(
@@ -30,8 +31,8 @@ pub(crate) fn inspect_agent_config_statuses(
AgentStatusDetectionTarget::Client(AgentClient::DeepSeekHarness),
AgentStatusDetectionTarget::Client(AgentClient::ZCode),
AgentStatusDetectionTarget::Client(AgentClient::WorkBuddy),
AgentStatusDetectionTarget::Client(AgentClient::AntigravityIde),
AgentStatusDetectionTarget::Client(AgentClient::AntigravityCli),
AgentStatusDetectionTarget::Cursor(CursorClient::Cli),
AgentStatusDetectionTarget::Client(AgentClient::KimiCode),
AgentStatusDetectionTarget::Client(AgentClient::GrokBuild),
AgentStatusDetectionTarget::PiProvider,
@@ -60,6 +61,9 @@ pub(crate) fn inspect_agent_config_statuses(
AgentStatusDetectionTarget::PiProvider => {
inspect_pi_provider_status(home, config.port, api_key)
}
AgentStatusDetectionTarget::Cursor(client) => {
inspect_cursor_client(client, home)
}
};
results
.lock()
+4 -4
View File
@@ -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)?,
};
+123
View File
@@ -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<Self> {
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<PathBuf> {
find_cursor_cli(home)
}
}
pub(crate) fn find_cursor_cli(home: &Path) -> Option<PathBuf> {
#[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<String>) -> 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);
}
}
+6 -8
View File
@@ -48,7 +48,7 @@ pub(crate) fn agent_config_paths(client: AgentClient, home: &Path) -> Vec<PathBu
]
}
AgentClient::WorkBuddy => 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<PathBuf>
}
pub(crate) fn find_agent_executable(client: AgentClient, home: &Path) -> Option<PathBuf> {
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);
+29 -7
View File
@@ -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);
+2 -2
View File
@@ -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);
-13
View File
@@ -353,19 +353,6 @@ pub(crate) fn find_windows_registered_zcode_executable() -> Option<PathBuf> {
})
}
#[cfg(not(test))]
pub(crate) fn find_windows_registered_antigravity_ide() -> Option<PathBuf> {
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<PathBuf> {
for executable in ["WorkBuddyAI.exe", "WorkBuddy.exe"] {
-1
View File
@@ -75,7 +75,6 @@ fn tracked_configuration_paths(app: &tauri::AppHandle) -> Result<Vec<PathBuf>, S
AgentClient::DeepSeekHarness,
AgentClient::ZCode,
AgentClient::WorkBuddy,
AgentClient::AntigravityIde,
AgentClient::AntigravityCli,
AgentClient::KimiCode,
AgentClient::GrokBuild,
+3 -5
View File
@@ -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"],
-1
View File
@@ -14,7 +14,6 @@ fn configuration_paths_ignore_inherited_environment() {
"deepseek-harness",
"zcode",
"workbuddy",
"antigravity-ide",
"antigravity-cli",
"kimi-code",
"grok-build",
+5
View File
@@ -0,0 +1,5 @@
<svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M22.8416 1.387C23.4132 1.0062 24.1735 1.44515 24.1295 2.13055L22.6326 25.4656C22.5957 26.0405 21.8646 26.2609 21.5162 25.8022L15.0114 17.2395C14.8429 17.0177 14.5923 16.873 14.316 16.838L3.6481 15.486C3.07656 15.4136 2.9019 14.6703 3.38136 14.3509L22.8416 1.387Z" fill="#1E1E1E"/>
<path d="M1.79102 15.2782L24.2386 0.457031L14.6808 17.0116L1.79102 15.2782Z" fill="#000000"/>
<path d="M22.6278 27.3078L24.2394 0.457031L14.6816 17.0116L22.6278 27.3078Z" fill="#777777"/>
</svg>

After

Width:  |  Height:  |  Size: 587 B

+2 -2
View File
@@ -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 を再起動してください。既存の会話のモデルは自動で切り替わりません。',
+2 -2
View File
@@ -937,9 +937,9 @@ export const en: Record<MessageKey, string> = {
'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.',
+2 -2
View File
@@ -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;已有会话的模型不会自动切换。',
+76 -29
View File
@@ -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<AgentViewState>) => {
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
</aside>
<section className="panel agent-config-panel">
<div className="agent-subpage-tabs" role="tablist" aria-label={t('agents.tabs.label')}>
{availableSubpages.length > 1 ? (
<div className="agent-subpage-tabs" role="tablist" aria-label={t('agents.tabs.label')}>
{availableSubpages.map((subpage) => (
<button
type="button"
@@ -2058,6 +2083,7 @@ export function AgentsPage({ embedded = false, onConfigurationApplied }: AgentsP
</button>
))}
</div>
) : null}
{embedded && activeSubpage === 'core' ? (
<div className="agent-minimal-config" id="agent-subpage-panel-core" role="tabpanel" aria-labelledby="agent-subpage-tab-core">
<div className="agent-minimal-client-summary">
@@ -2073,7 +2099,13 @@ export function AgentsPage({ embedded = false, onConfigurationApplied }: AgentsP
<MessageNotice message={activeStatus?.error || activeStatus?.warnings.join(';')} tone={activeStatus?.error ? 'error' : 'info'} />
{selected === 'claude-desktop' ? desktopModelEditor : <div className="agent-minimal-field">
{isCursorClient ? (
<div className="agent-unmanaged-notice">
<p className="agent-model-hint">
{t('agents.modify.cursorCliHint')}
</p>
</div>
) : selected === 'claude-desktop' ? desktopModelEditor : <div className="agent-minimal-field">
<label htmlFor="embedded-agent-model">{t(isDeepSeekHarnessClient ? 'agents.harness.defaultModel' : 'agents.useModel')}</label>
<AgentModelPicker
models={isClaudeModelMappingClient ? claudeMappingModels : models}
@@ -2089,23 +2121,25 @@ export function AgentsPage({ embedded = false, onConfigurationApplied }: AgentsP
{isDeepSeekHarnessClient ? <p className="agent-model-hint">{t('agents.harness.defaultHint')}</p> : null}
<div className="agent-save-bar">
{configurationFeedback}
<div className={`agent-save-actions${!isPiClient ? ' agent-codex-save-actions' : ''}`}>
{closeConfigurationButton}
<button
type="button"
className="primary-button"
onClick={applySelectedConfiguration}
disabled={busy || !canEnable || configurationWriteBlocked}
>
{['apply', 'install-pi', 'repair-pi'].includes(busyAction ?? '') ? <LoaderCircle size={16} className="spin" /> : null}
{isPiClient
? activeStatus?.pluginInstalled ? configurationActionLabel : t('agents.pi.install')
: configurationActionLabel}
</button>
{!isCursorClient ? (
<div className="agent-save-bar">
{configurationFeedback}
<div className={`agent-save-actions${!isPiClient ? " agent-codex-save-actions" : ""}`}>
{closeConfigurationButton}
<button
type="button"
className="primary-button"
onClick={applySelectedConfiguration}
disabled={busy || !canEnable || configurationWriteBlocked}
>
{['apply', 'install-pi', 'repair-pi'].includes(busyAction ?? '') ? <LoaderCircle size={16} className="spin" /> : null}
{isPiClient
? activeStatus?.pluginInstalled ? configurationActionLabel : t('agents.pi.install')
: configurationActionLabel}
</button>
</div>
</div>
</div>
) : null}
</div>
) : null}
@@ -2158,7 +2192,7 @@ export function AgentsPage({ embedded = false, onConfigurationApplied }: AgentsP
</>
) : (
<div>
<span>{t('agents.clientVersion')}</span>
<span>{selected === 'cursor-cli' ? t('agents.cliVersion') : t('agents.clientVersion')}</span>
<strong title={activeStatus?.version ?? undefined}>{activeStatus?.version ?? t('agents.notFetched')}</strong>
</div>
)}
@@ -2166,7 +2200,18 @@ export function AgentsPage({ embedded = false, onConfigurationApplied }: AgentsP
<MessageNotice message={activeStatus?.error || activeStatus?.warnings.join(';')} tone={activeStatus?.error ? 'error' : 'info'} />
{!isClaudeModelMappingClient ? (
{isCursorClient ? (
<section className="agent-core-setting-section">
<div className="agent-section-heading">
<div><strong>{t('agents.description.cursorCli')}</strong></div>
</div>
<p className="agent-model-hint">
{t('agents.modify.cursorCliHint')}
</p>
</section>
) : null}
{!isCursorClient && !isClaudeModelMappingClient ? (
<section className="agent-core-setting-section agent-model-section">
<div className="agent-section-heading">
<div><strong>{t(isDeepSeekHarnessClient ? 'agents.harness.defaultModel' : 'agents.useModel')}</strong></div>
@@ -2396,9 +2441,10 @@ export function AgentsPage({ embedded = false, onConfigurationApplied }: AgentsP
</section>
) : null}
{!isCursorClient ? (
<div className="agent-save-bar">
{configurationFeedback}
<div className={`agent-save-actions${!isPiClient ? ' agent-codex-save-actions' : ''}`}>
<div className={`agent-save-actions${!isPiClient ? " agent-codex-save-actions" : ""}`}>
{closeConfigurationButton}
<button type="button" className="primary-button" onClick={applySelectedConfiguration}
disabled={busy || !canEnable || configurationWriteBlocked}
@@ -2408,6 +2454,7 @@ export function AgentsPage({ embedded = false, onConfigurationApplied }: AgentsP
</button>
</div>
</div>
) : null}
</div>
) : null}
@@ -2443,7 +2490,7 @@ export function AgentsPage({ embedded = false, onConfigurationApplied }: AgentsP
<MessageNotice tone="success" message={!configurationErrorMessage ? configurationNotice || clearNotice : null}
onDismiss={() => { setConfigurationNotice(''); setClearNotice(''); }} />
{activeSubpage === 'core' ? <AgentRunControls name={activeDefinition.name} dualTargets={hasIndependentCliAndApp}
desktop={hasIndependentCliAndApp || selected === 'claude-desktop' || selected === 'zcode' || selected === 'workbuddy' || selected === 'antigravity-ide'}
desktop={hasIndependentCliAndApp || selected === 'claude-desktop' || selected === 'zcode' || selected === 'workbuddy'}
targets={activeLaunchTargets} enabled={launchEnabled} busyAction={busyAction}
harness={isDeepSeekHarnessClient ? deepSeekHarnessProcessStatus : null}
onLaunch={(target) => void launchAgent(target)} onRestart={() => void restartDesktopApp()}
+2 -2
View File
@@ -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();
+87
View File
@@ -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);
});
+2 -2
View File
@@ -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<string,any>={};
@@ -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'&&currentOauth,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'&&currentOauth,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('模拟关闭失败,已回滚');