feat(agents): add Cursor CLI support and remove Antigravity and Cursor IDE integrations
This commit is contained in:
1 parent
84c29782b0
commit
8901eff9bb
24 files changed
+530
-634
No files matched your search
+2
-2
@@ -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` を直接実行する場合は環境変数を自分で設定してください。
|
||||
|
||||
## その他の機能
|
||||
|
||||
|
||||
@@ -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
@@ -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` 时需自行设置环境变量。
|
||||
|
||||
## 其他功能
|
||||
|
||||
|
||||
@@ -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::*;
|
||||
@@ -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)]
|
||||
|
||||
@@ -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], ¤t);
|
||||
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], ¤t);
|
||||
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());
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -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)?,
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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"] {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -14,7 +14,6 @@ fn configuration_paths_ignore_inherited_environment() {
|
||||
"deepseek-harness",
|
||||
"zcode",
|
||||
"workbuddy",
|
||||
"antigravity-ide",
|
||||
"antigravity-cli",
|
||||
"kimi-code",
|
||||
"grok-build",
|
||||
|
||||
@@ -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
@@ -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 を再起動してください。既存の会話のモデルは自動で切り替わりません。',
|
||||
|
||||
@@ -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.',
|
||||
|
||||
@@ -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
@@ -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()}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
Vendored
+2
-2
@@ -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'&¤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('模拟关闭失败,已回滚');
|
||||
|
||||
Reference in new issue
Block a user