feat(agents): restore quick launch support for agent clients
This commit is contained in:
1 parent
0d7139eedd
commit
d0609e48f0
14 files changed
+1083
-13
No files matched your search
@@ -9,6 +9,7 @@
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@tauri-apps/api": "^2.0.0",
|
||||
"@tauri-apps/plugin-dialog": "^2.7.1",
|
||||
"lucide-react": "^1.24.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
@@ -210,6 +211,8 @@
|
||||
|
||||
"@tauri-apps/cli-win32-x64-msvc": ["@tauri-apps/cli-win32-x64-msvc@2.11.4", "", { "os": "win32", "cpu": "x64" }, "sha512-+vDiqBIU5dMISg/wNvX3sF+ZHfgJGJ5T0AcO+EHNXV9GGAG+P5fzodlDXD3QdKCRgZxMoCm5PPvj3BqLNjBthw=="],
|
||||
|
||||
"@tauri-apps/plugin-dialog": ["@tauri-apps/plugin-dialog@2.7.2", "", { "dependencies": { "@tauri-apps/api": "^2.11.0" } }, "sha512-pX0IGm1I3I6wc+zeKYcq1GSqogK6okCNX5fOdaNU5ab1AjGS6l1E5wFNjEb7meg7ZFSp0JUs+0jQGQNyOvLrsg=="],
|
||||
|
||||
"@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="],
|
||||
|
||||
"@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="],
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@tauri-apps/api": "^2.0.0",
|
||||
"@tauri-apps/plugin-dialog": "^2.7.1",
|
||||
"lucide-react": "^1.24.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0"
|
||||
|
||||
Generated
+143
-1
@@ -617,6 +617,7 @@ dependencies = [
|
||||
"tauri",
|
||||
"tauri-build",
|
||||
"tauri-plugin-autostart",
|
||||
"tauri-plugin-dialog",
|
||||
"tauri-plugin-opener",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
@@ -2519,6 +2520,7 @@ checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272"
|
||||
dependencies = [
|
||||
"bitflags 2.13.0",
|
||||
"block2",
|
||||
"libc",
|
||||
"objc2",
|
||||
"objc2-core-foundation",
|
||||
]
|
||||
@@ -3211,6 +3213,30 @@ dependencies = [
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rfd"
|
||||
version = "0.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a15ad77d9e70a92437d8f74c35d99b4e4691128df018833e99f90bcd36152672"
|
||||
dependencies = [
|
||||
"block2",
|
||||
"dispatch2",
|
||||
"glib-sys",
|
||||
"gobject-sys",
|
||||
"gtk-sys",
|
||||
"js-sys",
|
||||
"log",
|
||||
"objc2",
|
||||
"objc2-app-kit",
|
||||
"objc2-core-foundation",
|
||||
"objc2-foundation",
|
||||
"raw-window-handle",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ring"
|
||||
version = "0.17.14"
|
||||
@@ -4061,6 +4087,48 @@ dependencies = [
|
||||
"thiserror 2.0.18",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-dialog"
|
||||
version = "2.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b2d3c1dbe38037e7f590cdf2492594d5ceebe031e7bc7e827509b22a999d2940"
|
||||
dependencies = [
|
||||
"log",
|
||||
"raw-window-handle",
|
||||
"rfd",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tauri",
|
||||
"tauri-plugin",
|
||||
"tauri-plugin-fs",
|
||||
"thiserror 2.0.18",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-fs"
|
||||
version = "2.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b7ecc274121aca0c036a2b42d1cbe83d368d348f54e0bb8a735c2b1548e8f371"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"dunce",
|
||||
"glob",
|
||||
"log",
|
||||
"objc2-foundation",
|
||||
"percent-encoding",
|
||||
"schemars 0.8.22",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_repr",
|
||||
"tauri",
|
||||
"tauri-plugin",
|
||||
"tauri-utils",
|
||||
"thiserror 2.0.18",
|
||||
"toml 1.1.3+spec-1.1.0",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-opener"
|
||||
version = "2.5.4"
|
||||
@@ -5208,6 +5276,15 @@ dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.60.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
|
||||
dependencies = [
|
||||
"windows-targets 0.53.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.61.2"
|
||||
@@ -5241,13 +5318,30 @@ dependencies = [
|
||||
"windows_aarch64_gnullvm 0.52.6",
|
||||
"windows_aarch64_msvc 0.52.6",
|
||||
"windows_i686_gnu 0.52.6",
|
||||
"windows_i686_gnullvm",
|
||||
"windows_i686_gnullvm 0.52.6",
|
||||
"windows_i686_msvc 0.52.6",
|
||||
"windows_x86_64_gnu 0.52.6",
|
||||
"windows_x86_64_gnullvm 0.52.6",
|
||||
"windows_x86_64_msvc 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.53.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3"
|
||||
dependencies = [
|
||||
"windows-link 0.2.1",
|
||||
"windows_aarch64_gnullvm 0.53.1",
|
||||
"windows_aarch64_msvc 0.53.1",
|
||||
"windows_i686_gnu 0.53.1",
|
||||
"windows_i686_gnullvm 0.53.1",
|
||||
"windows_i686_msvc 0.53.1",
|
||||
"windows_x86_64_gnu 0.53.1",
|
||||
"windows_x86_64_gnullvm 0.53.1",
|
||||
"windows_x86_64_msvc 0.53.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-threading"
|
||||
version = "0.1.0"
|
||||
@@ -5278,6 +5372,12 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.42.2"
|
||||
@@ -5290,6 +5390,12 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.42.2"
|
||||
@@ -5302,12 +5408,24 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnullvm"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.42.2"
|
||||
@@ -5320,6 +5438,12 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.42.2"
|
||||
@@ -5332,6 +5456,12 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.42.2"
|
||||
@@ -5344,6 +5474,12 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.42.2"
|
||||
@@ -5356,6 +5492,12 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.5.40"
|
||||
|
||||
@@ -28,6 +28,7 @@ sha2 = "0.10"
|
||||
tar = "0.4"
|
||||
tauri = { version = "~2.11", features = ["tray-icon"] }
|
||||
tauri-plugin-autostart = "2"
|
||||
tauri-plugin-dialog = "2.7.1"
|
||||
tauri-plugin-opener = "2"
|
||||
tokio = { version = "1", features = ["io-util", "macros", "net", "sync", "time"] }
|
||||
tokio-util = "0.7"
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"core:event:allow-unlisten",
|
||||
"core:window:allow-set-theme",
|
||||
"core:window:allow-set-background-color",
|
||||
"core:webview:allow-set-webview-background-color"
|
||||
"core:webview:allow-set-webview-background-color",
|
||||
"dialog:allow-open"
|
||||
]
|
||||
}
|
||||
@@ -5,8 +5,10 @@ use super::*;
|
||||
mod commands;
|
||||
mod configuration;
|
||||
mod discovery;
|
||||
mod launch;
|
||||
mod state;
|
||||
pub(crate) use commands::*;
|
||||
pub(crate) use configuration::*;
|
||||
pub(crate) use discovery::*;
|
||||
pub(crate) use launch::*;
|
||||
pub(crate) use state::*;
|
||||
@@ -360,6 +360,16 @@ pub(crate) fn inspect_pi_provider_status(
|
||||
supported_platform: true,
|
||||
installed: executable.is_some(),
|
||||
plugin_installed,
|
||||
launch_targets: executable
|
||||
.as_ref()
|
||||
.map(|path| {
|
||||
vec![AgentLaunchTarget {
|
||||
id: "cli".to_string(),
|
||||
label: PI_AGENT_NAME.to_string(),
|
||||
detail: path_to_string(path),
|
||||
}]
|
||||
})
|
||||
.unwrap_or_default(),
|
||||
version: cli_version.clone(),
|
||||
cli_version,
|
||||
app_version: None,
|
||||
@@ -831,15 +841,28 @@ pub(crate) fn inspect_agent_config(
|
||||
} else {
|
||||
executable.as_deref().and_then(read_agent_version)
|
||||
};
|
||||
let codex_app_installation = (client == AgentClient::Codex)
|
||||
.then(|| find_codex_app_installation(home))
|
||||
.flatten();
|
||||
let app_version = match client {
|
||||
AgentClient::ClaudeDesktop => read_claude_desktop_version(home),
|
||||
AgentClient::Codex => read_codex_app_version(home),
|
||||
AgentClient::Codex => codex_app_installation
|
||||
.as_ref()
|
||||
.and_then(read_codex_app_installation_version),
|
||||
AgentClient::DeepSeekHarness => read_deepseek_harness_profile_version(home),
|
||||
AgentClient::ZCode => read_zcode_app_version(home),
|
||||
_ => None,
|
||||
};
|
||||
let version = cli_version.clone().or_else(|| app_version.clone());
|
||||
let installed = version.is_some() || (client == AgentClient::ZCode && executable.is_some());
|
||||
let installed = version.is_some()
|
||||
|| (client == AgentClient::ZCode && executable.is_some())
|
||||
|| codex_app_installation.is_some();
|
||||
let launch_targets = agent_launch_targets(
|
||||
client,
|
||||
executable.as_deref(),
|
||||
app_version.as_deref(),
|
||||
codex_app_installation.is_some(),
|
||||
);
|
||||
let mut warnings = Vec::new();
|
||||
if !client.supported_platform() {
|
||||
warnings.push("当前平台不支持 Claude Desktop 3P 配置".to_string());
|
||||
@@ -859,6 +882,7 @@ pub(crate) fn inspect_agent_config(
|
||||
supported_platform: client.supported_platform(),
|
||||
installed,
|
||||
plugin_installed: true,
|
||||
launch_targets,
|
||||
version,
|
||||
cli_version,
|
||||
app_version,
|
||||
@@ -883,6 +907,63 @@ pub(crate) fn inspect_agent_config(
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn agent_launch_targets(
|
||||
client: AgentClient,
|
||||
executable: Option<&Path>,
|
||||
app_version: Option<&str>,
|
||||
app_installed: bool,
|
||||
) -> Vec<AgentLaunchTarget> {
|
||||
let mut targets = Vec::new();
|
||||
match client {
|
||||
AgentClient::ClaudeDesktop => {
|
||||
if executable.is_some() || app_version.is_some() {
|
||||
targets.push(AgentLaunchTarget {
|
||||
id: "app".to_string(),
|
||||
label: "Claude Desktop".to_string(),
|
||||
detail: executable
|
||||
.map(path_to_string)
|
||||
.unwrap_or_else(|| "Claude Desktop application".to_string()),
|
||||
});
|
||||
}
|
||||
}
|
||||
AgentClient::Codex => {
|
||||
if app_installed {
|
||||
targets.push(AgentLaunchTarget {
|
||||
id: "app".to_string(),
|
||||
label: "Codex App".to_string(),
|
||||
detail: "Codex desktop application".to_string(),
|
||||
});
|
||||
}
|
||||
if let Some(executable) = executable {
|
||||
targets.push(AgentLaunchTarget {
|
||||
id: "cli".to_string(),
|
||||
label: "Codex CLI".to_string(),
|
||||
detail: path_to_string(executable),
|
||||
});
|
||||
}
|
||||
}
|
||||
AgentClient::ZCode => {
|
||||
if let Some(executable) = executable {
|
||||
targets.push(AgentLaunchTarget {
|
||||
id: "app".to_string(),
|
||||
label: "ZCode".to_string(),
|
||||
detail: path_to_string(executable),
|
||||
});
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
if let Some(executable) = executable {
|
||||
targets.push(AgentLaunchTarget {
|
||||
id: "cli".to_string(),
|
||||
label: client.name().to_string(),
|
||||
detail: path_to_string(executable),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
targets
|
||||
}
|
||||
|
||||
pub(crate) fn agent_configuration_is_synchronized(
|
||||
client: AgentClient,
|
||||
home: &Path,
|
||||
@@ -1260,27 +1341,27 @@ pub(crate) fn read_claude_desktop_version(home: &Path) -> Option<String> {
|
||||
find_claude_desktop_executable(home).and_then(|path| read_agent_version(&path))
|
||||
}
|
||||
|
||||
pub(crate) fn read_codex_app_version(home: &Path) -> Option<String> {
|
||||
pub(crate) fn read_codex_app_installation_version(installation: &CodexAppTarget) -> Option<String> {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
match find_codex_app_installation(home)? {
|
||||
match installation {
|
||||
CodexAppTarget::WindowsAppId(app_id) => {
|
||||
let _ = app_id;
|
||||
read_windows_codex_store_version()
|
||||
}
|
||||
CodexAppTarget::Application(path) => {
|
||||
read_windows_executable_version(&path).or_else(|| read_agent_version(&path))
|
||||
read_windows_executable_version(path).or_else(|| read_agent_version(path))
|
||||
}
|
||||
}
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let CodexAppTarget::Application(path) = find_codex_app_installation(home)?;
|
||||
read_macos_app_version(&path)
|
||||
let CodexAppTarget::Application(path) = installation;
|
||||
read_macos_app_version(path)
|
||||
}
|
||||
#[cfg(not(any(target_os = "windows", target_os = "macos")))]
|
||||
{
|
||||
let _ = home;
|
||||
let _ = installation;
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,397 @@
|
||||
use super::*;
|
||||
|
||||
#[tauri::command]
|
||||
pub(crate) fn launch_agent(
|
||||
app: tauri::AppHandle,
|
||||
gui_config_state: tauri::State<'_, GuiConfigState>,
|
||||
client: String,
|
||||
target: Option<String>,
|
||||
working_directory: Option<String>,
|
||||
) -> Result<(), String> {
|
||||
let home = app
|
||||
.path()
|
||||
.home_dir()
|
||||
.map_err(|error| format!("无法获取用户目录: {error}"))?;
|
||||
let config = gui_config_state.snapshot()?;
|
||||
let requested_target = target
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty());
|
||||
|
||||
if client.trim().eq_ignore_ascii_case(PI_AGENT_ID) {
|
||||
if requested_target.is_some_and(|value| value != "cli") {
|
||||
return Err("Pi 只支持 CLI 启动方式".to_string());
|
||||
}
|
||||
let status =
|
||||
inspect_pi_provider_status(&home, config.port, effective_agent_api_key(&config));
|
||||
if !status.installed {
|
||||
return Err("未检测到 Pi CLI,请先安装 Pi 并重新检测".to_string());
|
||||
}
|
||||
if !status.configured {
|
||||
return Err("请先安装并配置 Pi CLIProxyAPI provider 插件".to_string());
|
||||
}
|
||||
let executable =
|
||||
find_pi_executable(&home).ok_or_else(|| "未找到 Pi CLI 可执行文件".to_string())?;
|
||||
let launch_directory = resolve_launch_directory(working_directory.as_deref(), &home)?;
|
||||
return launch_cli_agent(&executable, PI_AGENT_NAME, &launch_directory, &[]);
|
||||
}
|
||||
|
||||
let client = AgentClient::parse(&client)?;
|
||||
if !client.supported_platform() {
|
||||
return Err(format!("当前平台不支持启动 {}", client.name()));
|
||||
}
|
||||
let status = inspect_agent_config(client, &home, config.port, effective_agent_api_key(&config));
|
||||
if !status.installed {
|
||||
return Err(format!("未检测到 {},请先安装并重新检测", client.name()));
|
||||
}
|
||||
validate_agent_launch_configuration(client, &status)?;
|
||||
if client == AgentClient::Codex && status.oauth_configuration {
|
||||
validate_codex_oauth_login(&home)?;
|
||||
}
|
||||
|
||||
let default_target = match client {
|
||||
AgentClient::ClaudeDesktop | AgentClient::ZCode => "app",
|
||||
AgentClient::Codex if find_codex_app_installation(&home).is_some() => "app",
|
||||
_ => "cli",
|
||||
};
|
||||
let requested_target = requested_target.unwrap_or(default_target);
|
||||
match (client, requested_target) {
|
||||
(AgentClient::ClaudeDesktop, "app") => launch_claude_desktop(&home),
|
||||
(AgentClient::ZCode, "app") => {
|
||||
let executable = find_zcode_desktop_executable(&home)
|
||||
.ok_or_else(|| "未找到 ZCode 应用程序".to_string())?;
|
||||
launch_desktop_agent(&executable, client.name())
|
||||
}
|
||||
(AgentClient::Codex, "app") => launch_codex_desktop(&home),
|
||||
(AgentClient::ClaudeDesktop | AgentClient::ZCode, "cli") => {
|
||||
Err(format!("{} 不支持 CLI 启动方式", client.name()))
|
||||
}
|
||||
(_, "cli") => {
|
||||
let executable = find_agent_executable(client, &home)
|
||||
.ok_or_else(|| format!("未找到 {} 的可执行文件", client.name()))?;
|
||||
let launch_directory = resolve_launch_directory(working_directory.as_deref(), &home)?;
|
||||
let environment_to_remove = if client == AgentClient::ClaudeCode {
|
||||
&["ANTHROPIC_API_KEY"][..]
|
||||
} else {
|
||||
&[]
|
||||
};
|
||||
launch_cli_agent(
|
||||
&executable,
|
||||
client.name(),
|
||||
&launch_directory,
|
||||
environment_to_remove,
|
||||
)
|
||||
}
|
||||
(_, "app") => Err(format!("{} 不支持桌面 App 启动方式", client.name())),
|
||||
_ => Err("不支持的智能体启动方式".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_agent_launch_configuration(
|
||||
client: AgentClient,
|
||||
status: &AgentConfigStatus,
|
||||
) -> Result<(), String> {
|
||||
if client == AgentClient::Codex
|
||||
|| (status.modification_enabled && status.modification_state == "applied")
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
Err(format!(
|
||||
"请先为 {} 应用配置修改,确保 CPA 配置生效后再启动",
|
||||
client.name()
|
||||
))
|
||||
}
|
||||
|
||||
fn resolve_launch_directory(value: Option<&str>, fallback: &Path) -> Result<PathBuf, String> {
|
||||
let Some(value) = value.map(str::trim).filter(|value| !value.is_empty()) else {
|
||||
return Ok(fallback.to_path_buf());
|
||||
};
|
||||
if value.chars().any(char::is_control) {
|
||||
return Err("工作目录包含无效字符".to_string());
|
||||
}
|
||||
let path = PathBuf::from(value);
|
||||
if !path.is_absolute() {
|
||||
return Err("工作目录必须是绝对路径".to_string());
|
||||
}
|
||||
if !path.is_dir() {
|
||||
return Err(format!("工作目录不存在: {}", path_to_string(&path)));
|
||||
}
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
fn launch_codex_desktop(home: &Path) -> Result<(), String> {
|
||||
let target = find_codex_app_installation(home)
|
||||
.ok_or_else(|| "未检测到 Codex 桌面应用,请重新检测或改用 Codex CLI".to_string())?;
|
||||
match target {
|
||||
#[cfg(target_os = "windows")]
|
||||
CodexAppTarget::WindowsAppId(app_id) => launch_windows_store_app(&app_id, "Codex App"),
|
||||
CodexAppTarget::Application(path) => launch_desktop_agent(&path, "Codex App"),
|
||||
}
|
||||
}
|
||||
|
||||
fn launch_claude_desktop(home: &Path) -> Result<(), String> {
|
||||
if let Some(executable) = find_claude_desktop_executable(home) {
|
||||
return launch_desktop_agent(&executable, "Claude Desktop");
|
||||
}
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
launch_windows_claude_store_app()
|
||||
}
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
Err("未检测到 Claude Desktop 应用,请先安装或重新检测".to_string())
|
||||
}
|
||||
|
||||
fn launch_desktop_agent(executable: &Path, label: &str) -> Result<(), String> {
|
||||
#[cfg(target_os = "macos")]
|
||||
let mut command = {
|
||||
let application = executable
|
||||
.ancestors()
|
||||
.find(|path| path.extension().and_then(|value| value.to_str()) == Some("app"))
|
||||
.unwrap_or(executable);
|
||||
let mut command = Command::new("open");
|
||||
command.arg(application);
|
||||
command
|
||||
};
|
||||
|
||||
#[cfg(any(target_os = "windows", target_os = "linux"))]
|
||||
let mut command = Command::new(executable);
|
||||
|
||||
#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
|
||||
{
|
||||
let _ = (executable, label);
|
||||
return Err("当前平台不支持桌面智能体".to_string());
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))]
|
||||
{
|
||||
command
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null());
|
||||
configure_background_command(&mut command);
|
||||
command
|
||||
.spawn()
|
||||
.map(|_| ())
|
||||
.map_err(|error| format!("启动 {label} 失败: {error}"))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn launch_windows_store_app(app_id: &str, label: &str) -> Result<(), String> {
|
||||
let mut command = Command::new(windows_explorer_executable());
|
||||
command
|
||||
.arg(format!("shell:AppsFolder\\{app_id}"))
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null());
|
||||
configure_background_command(&mut command);
|
||||
command
|
||||
.spawn()
|
||||
.map(|_| ())
|
||||
.map_err(|error| format!("启动 {label} 失败: {error}"))
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn launch_windows_claude_store_app() -> Result<(), String> {
|
||||
const SCRIPT: &str = r#"
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$appId = @(Get-StartApps) |
|
||||
Where-Object {
|
||||
$_.AppID -like 'Claude_*!*' -or
|
||||
$_.AppID -like 'Anthropic.Claude_*!*'
|
||||
} |
|
||||
Select-Object -First 1 -ExpandProperty AppID
|
||||
if (-not $appId) { throw 'Claude Desktop app entry was not found' }
|
||||
Start-Process "shell:AppsFolder\$appId"
|
||||
"#;
|
||||
let encoded = windows_powershell_encoded_command(SCRIPT);
|
||||
let mut command = Command::new(windows_powershell_executable());
|
||||
command.args([
|
||||
"-NoLogo",
|
||||
"-NoProfile",
|
||||
"-NonInteractive",
|
||||
"-ExecutionPolicy",
|
||||
"Bypass",
|
||||
"-EncodedCommand",
|
||||
&encoded,
|
||||
]);
|
||||
configure_background_command(&mut command);
|
||||
let output = command
|
||||
.output()
|
||||
.map_err(|error| format!("启动 Claude Desktop 失败: {error}"))?;
|
||||
if output.status.success() {
|
||||
Ok(())
|
||||
} else {
|
||||
let detail = String::from_utf8_lossy(&output.stderr).trim().to_string();
|
||||
Err(if detail.is_empty() {
|
||||
"未找到可启动的 Claude Desktop 应用".to_string()
|
||||
} else {
|
||||
format!("启动 Claude Desktop 失败: {detail}")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn shell_single_quote(value: &str) -> String {
|
||||
format!("'{}'", value.replace('\'', "'\"'\"'"))
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn launch_cli_agent(
|
||||
executable: &Path,
|
||||
label: &str,
|
||||
working_directory: &Path,
|
||||
environment_to_remove: &[&str],
|
||||
) -> Result<(), String> {
|
||||
let removals = environment_to_remove
|
||||
.iter()
|
||||
.map(|key| format!("-u {}", shell_single_quote(key)))
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ");
|
||||
let command_line = format!(
|
||||
"cd {} && exec env {} {}",
|
||||
shell_single_quote(&path_to_string(working_directory)),
|
||||
removals,
|
||||
shell_single_quote(&path_to_string(executable)),
|
||||
);
|
||||
let script = format!(
|
||||
"tell application \"Terminal\"\nactivate\ndo script \"{}\"\nend tell",
|
||||
command_line.replace('\\', "\\\\").replace('"', "\\\"")
|
||||
);
|
||||
let output = Command::new("osascript")
|
||||
.arg("-e")
|
||||
.arg(script)
|
||||
.output()
|
||||
.map_err(|error| format!("启动 {label} 终端失败: {error}"))?;
|
||||
if output.status.success() {
|
||||
Ok(())
|
||||
} else {
|
||||
let detail = String::from_utf8_lossy(&output.stderr).trim().to_string();
|
||||
Err(format!("启动 {label} 终端失败: {detail}"))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn launch_cli_agent(
|
||||
executable: &Path,
|
||||
label: &str,
|
||||
working_directory: &Path,
|
||||
environment_to_remove: &[&str],
|
||||
) -> Result<(), String> {
|
||||
let terminals: &[(&str, &[&str])] = &[
|
||||
("x-terminal-emulator", &["-e"]),
|
||||
("gnome-terminal", &["--"]),
|
||||
("konsole", &["-e"]),
|
||||
("xfce4-terminal", &["-e"]),
|
||||
("mate-terminal", &["--"]),
|
||||
("kitty", &["-e"]),
|
||||
("alacritty", &["-e"]),
|
||||
("ghostty", &["-e"]),
|
||||
("xterm", &["-e"]),
|
||||
];
|
||||
let path = env::var_os("PATH").unwrap_or_default();
|
||||
let mut last_error = None;
|
||||
for (terminal, arguments) in terminals {
|
||||
if !env::split_paths(&path).any(|directory| directory.join(terminal).is_file()) {
|
||||
continue;
|
||||
}
|
||||
let mut command = Command::new(terminal);
|
||||
command
|
||||
.args(*arguments)
|
||||
.arg(executable)
|
||||
.current_dir(working_directory)
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null());
|
||||
for key in environment_to_remove {
|
||||
command.env_remove(key);
|
||||
}
|
||||
match command.spawn() {
|
||||
Ok(_) => return Ok(()),
|
||||
Err(error) => last_error = Some(error.to_string()),
|
||||
}
|
||||
}
|
||||
Err(match last_error {
|
||||
Some(error) => format!("启动 {label} 失败: {error}"),
|
||||
None => format!("启动 {label} 失败:未找到可用的终端程序"),
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn launch_cli_agent(
|
||||
executable: &Path,
|
||||
label: &str,
|
||||
working_directory: &Path,
|
||||
environment_to_remove: &[&str],
|
||||
) -> Result<(), String> {
|
||||
use std::os::windows::process::CommandExt;
|
||||
|
||||
const CREATE_NEW_CONSOLE: u32 = 0x0000_0010;
|
||||
let mut command = windows_command_for_executable(executable, true);
|
||||
command
|
||||
.current_dir(working_directory)
|
||||
.creation_flags(CREATE_NEW_CONSOLE);
|
||||
for key in environment_to_remove {
|
||||
command.env_remove(key);
|
||||
}
|
||||
command
|
||||
.spawn()
|
||||
.map(|_| ())
|
||||
.map_err(|error| format!("启动 {label} 失败: {error}"))
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
|
||||
fn launch_cli_agent(
|
||||
_executable: &Path,
|
||||
label: &str,
|
||||
_working_directory: &Path,
|
||||
_environment_to_remove: &[&str],
|
||||
) -> Result<(), String> {
|
||||
Err(format!("当前平台不支持启动 {label}"))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn relative_launch_directory_is_rejected() {
|
||||
let error = resolve_launch_directory(Some("relative/project"), Path::new("/fallback"))
|
||||
.expect_err("relative path should be rejected");
|
||||
assert!(error.contains("绝对路径"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn omitted_launch_directory_uses_fallback() {
|
||||
let fallback = if cfg!(target_os = "windows") {
|
||||
Path::new(r"C:\Users\tester")
|
||||
} else {
|
||||
Path::new("/home/tester")
|
||||
};
|
||||
assert_eq!(resolve_launch_directory(None, fallback).unwrap(), fallback);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn codex_exposes_independent_app_and_cli_targets() {
|
||||
let executable = Path::new("codex");
|
||||
let targets =
|
||||
agent_launch_targets(AgentClient::Codex, Some(executable), Some("1.0.0"), true);
|
||||
assert_eq!(
|
||||
targets
|
||||
.iter()
|
||||
.map(|target| target.id.as_str())
|
||||
.collect::<Vec<_>>(),
|
||||
["app", "cli"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn desktop_and_cli_clients_get_the_correct_target_kind() {
|
||||
let executable = Path::new("agent");
|
||||
let zcode =
|
||||
agent_launch_targets(AgentClient::ZCode, Some(executable), Some("1.0.0"), false);
|
||||
let kimi = agent_launch_targets(AgentClient::KimiCode, Some(executable), None, false);
|
||||
assert_eq!(zcode[0].id, "app");
|
||||
assert_eq!(kimi[0].id, "cli");
|
||||
}
|
||||
}
|
||||
@@ -763,6 +763,7 @@ struct AgentConfigStatus {
|
||||
supported_platform: bool,
|
||||
installed: bool,
|
||||
plugin_installed: bool,
|
||||
launch_targets: Vec<AgentLaunchTarget>,
|
||||
version: Option<String>,
|
||||
cli_version: Option<String>,
|
||||
app_version: Option<String>,
|
||||
@@ -784,6 +785,14 @@ struct AgentConfigStatus {
|
||||
error: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct AgentLaunchTarget {
|
||||
id: String,
|
||||
label: String,
|
||||
detail: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct AgentConfigActionResult {
|
||||
@@ -1770,6 +1779,7 @@ fn main() {
|
||||
let start_hidden = should_start_hidden(&gui_config);
|
||||
|
||||
let app = tauri::Builder::default()
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
.plugin(tauri_plugin_autostart::init(
|
||||
tauri_plugin_autostart::MacosLauncher::LaunchAgent,
|
||||
None,
|
||||
@@ -1974,6 +1984,7 @@ fn main() {
|
||||
clear_codex_config,
|
||||
set_agent_config_enabled,
|
||||
update_agent_config,
|
||||
launch_agent,
|
||||
get_lan_ipv4,
|
||||
save_gui_settings,
|
||||
save_network_routing_settings,
|
||||
|
||||
@@ -500,6 +500,23 @@ export const jaOverrides = {
|
||||
'agents.error.mappingSelectionGone': 'Claude Desktop のモデルマッピングに利用できないモデルがあります。モデルを更新して選び直してください',
|
||||
'agents.error.claudeCodeRuntimeSettingsInvalid': '最大コンテキストは 100000~1000000、自動圧縮率は 1~100 にしてください',
|
||||
'agents.error.applyFirst': '選択したモデルはまだ適用されていません。先に「設定変更を適用」をクリックしてください',
|
||||
'agents.launch.enableFirst': '先に設定変更を適用してください',
|
||||
'agents.launch.applyFirst': '先に選択中のモデルを適用してください',
|
||||
'agents.launch.starting': '起動中',
|
||||
'agents.launch.startCli': 'CLI を起動',
|
||||
'agents.launch.startApp': 'App を起動',
|
||||
'agents.launch.start': '{target} を起動',
|
||||
'agents.launch.unavailable': '起動できません',
|
||||
'agents.launchDirectory.noDirectory': '未選択',
|
||||
'agents.launchDirectory.chooseDirectory': 'ディレクトリを選択',
|
||||
'agents.launchDirectory.selectedDirectory': '選択した作業ディレクトリ',
|
||||
'agents.launchDirectory.choosing': '選択中',
|
||||
'agents.launchDirectory.dialogTitle': '{client} の起動ディレクトリを選択',
|
||||
'agents.launchDirectory.dialogDescription': 'プロジェクトの作業ディレクトリを選択してから、{client} を起動します。',
|
||||
'agents.launchDirectory.workingDirectory': '作業ディレクトリ',
|
||||
'agents.launchDirectory.browse': '参照',
|
||||
'agents.launchDirectory.directoryRequired': '先に作業ディレクトリを選択してください',
|
||||
'agents.launchDirectory.launch': '{client} を起動',
|
||||
'agents.status.detecting': '検出中',
|
||||
'agents.status.unsupported': 'プラットフォーム非対応',
|
||||
'agents.status.notInstalled': '未インストール',
|
||||
@@ -613,6 +630,7 @@ export const jaOverrides = {
|
||||
'agents.oauthLoginRequired.enableClearConfiguration': '設定を消去',
|
||||
'agents.oauthLoginRequired.enableDescriptionSuffix': 'を使用してください。',
|
||||
'agents.oauthLoginRequired.applyDescription': '有効な Codex のログイン認証情報が見つからないため、OAuth 設定を適用できません。Codex で ChatGPT アカウントにログインしてから、設定を適用してください。',
|
||||
'agents.oauthLoginRequired.launchDescription': '有効なログイン認証情報が見つからないため Codex を起動できません。先に Codex アプリで ChatGPT アカウントにログインしてください。',
|
||||
'agents.oauthLoginRequired.confirm': '了解しました',
|
||||
'agents.modify.apply': '設定変更を適用',
|
||||
'agents.modify.update': '設定を更新',
|
||||
|
||||
@@ -500,6 +500,23 @@ export const en: Record<MessageKey, string> = {
|
||||
'agents.error.mappingSelectionGone': 'A Claude Desktop mapping is no longer available. Refresh the models and select it again.',
|
||||
'agents.error.claudeCodeRuntimeSettingsInvalid': 'Maximum context must be 100000–1000000 and the auto-compact percentage must be 1–100.',
|
||||
'agents.error.applyFirst': 'The selected model has not been applied. Click “Apply Configuration Changes” first.',
|
||||
'agents.launch.enableFirst': 'Apply the configuration changes first',
|
||||
'agents.launch.applyFirst': 'Apply the selected model first',
|
||||
'agents.launch.starting': 'Starting',
|
||||
'agents.launch.startCli': 'Start CLI',
|
||||
'agents.launch.startApp': 'Start App',
|
||||
'agents.launch.start': 'Start {target}',
|
||||
'agents.launch.unavailable': 'Cannot Start',
|
||||
'agents.launchDirectory.noDirectory': 'Not selected',
|
||||
'agents.launchDirectory.chooseDirectory': 'Choose Directory',
|
||||
'agents.launchDirectory.selectedDirectory': 'Selected working directory',
|
||||
'agents.launchDirectory.choosing': 'Choosing',
|
||||
'agents.launchDirectory.dialogTitle': 'Choose a Launch Directory for {client}',
|
||||
'agents.launchDirectory.dialogDescription': 'Choose a project working directory, then start {client}.',
|
||||
'agents.launchDirectory.workingDirectory': 'Working directory',
|
||||
'agents.launchDirectory.browse': 'Browse',
|
||||
'agents.launchDirectory.directoryRequired': 'Select a working directory first',
|
||||
'agents.launchDirectory.launch': 'Start {client}',
|
||||
'agents.status.detecting': 'Detecting',
|
||||
'agents.status.unsupported': 'Unsupported Platform',
|
||||
'agents.status.notInstalled': 'Not Installed',
|
||||
@@ -613,6 +630,7 @@ export const en: Record<MessageKey, string> = {
|
||||
'agents.oauthLoginRequired.enableClearConfiguration': 'Clear Configuration',
|
||||
'agents.oauthLoginRequired.enableDescriptionSuffix': 'first.',
|
||||
'agents.oauthLoginRequired.applyDescription': 'OAuth configuration cannot be applied because no valid Codex sign-in credentials were found. Sign in to your ChatGPT account in Codex, then apply the configuration.',
|
||||
'agents.oauthLoginRequired.launchDescription': 'Codex cannot be started because no valid sign-in credentials were found. Sign in to your ChatGPT account in the Codex app first.',
|
||||
'agents.oauthLoginRequired.confirm': 'Got it',
|
||||
'agents.modify.apply': 'Apply Configuration Changes',
|
||||
'agents.modify.update': 'Update Configuration',
|
||||
|
||||
@@ -498,6 +498,23 @@ export const zhCN = {
|
||||
'agents.error.mappingSelectionGone': 'Claude Desktop 模型映射包含不可用模型,请刷新模型后重新选择',
|
||||
'agents.error.claudeCodeRuntimeSettingsInvalid': '最大窗口必须为 100000–1000000,触发压缩百分比必须为 1–100',
|
||||
'agents.error.applyFirst': '模型选择尚未应用,请先点击“应用配置修改”',
|
||||
'agents.launch.enableFirst': '请先应用配置修改',
|
||||
'agents.launch.applyFirst': '请先应用当前模型选择',
|
||||
'agents.launch.starting': '启动中',
|
||||
'agents.launch.startCli': '启动 CLI',
|
||||
'agents.launch.startApp': '启动 App',
|
||||
'agents.launch.start': '启动 {target}',
|
||||
'agents.launch.unavailable': '无法启动',
|
||||
'agents.launchDirectory.noDirectory': '尚未选择',
|
||||
'agents.launchDirectory.chooseDirectory': '选择目录',
|
||||
'agents.launchDirectory.selectedDirectory': '已选择的工作目录',
|
||||
'agents.launchDirectory.choosing': '选择中',
|
||||
'agents.launchDirectory.dialogTitle': '选择 {client} 启动目录',
|
||||
'agents.launchDirectory.dialogDescription': '选择项目工作目录,然后启动 {client}。',
|
||||
'agents.launchDirectory.workingDirectory': '工作目录',
|
||||
'agents.launchDirectory.browse': '浏览',
|
||||
'agents.launchDirectory.directoryRequired': '请先选择工作目录',
|
||||
'agents.launchDirectory.launch': '启动 {client}',
|
||||
'agents.status.detecting': '检测中',
|
||||
'agents.status.unsupported': '平台不支持',
|
||||
'agents.status.notInstalled': '未安装',
|
||||
@@ -611,6 +628,7 @@ export const zhCN = {
|
||||
'agents.oauthLoginRequired.enableClearConfiguration': '清空配置',
|
||||
'agents.oauthLoginRequired.enableDescriptionSuffix': '功能。',
|
||||
'agents.oauthLoginRequired.applyDescription': '无法应用 OAuth 配置:未检测到有效的 Codex 登录凭据。请先在 Codex 中登录 ChatGPT 账号,然后再应用配置。',
|
||||
'agents.oauthLoginRequired.launchDescription': '无法启动 Codex:未检测到有效的登录凭据。请先在 Codex 应用中登录 ChatGPT 账号。',
|
||||
'agents.oauthLoginRequired.confirm': '我知道了',
|
||||
'agents.modify.apply': '应用配置修改',
|
||||
'agents.modify.update': '更新配置',
|
||||
|
||||
+258
-2
@@ -10,16 +10,20 @@ import {
|
||||
} from 'react';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { listen } from '@tauri-apps/api/event';
|
||||
import { open } from '@tauri-apps/plugin-dialog';
|
||||
import {
|
||||
AlertTriangle,
|
||||
AppWindow,
|
||||
BadgeCheck,
|
||||
Bot,
|
||||
Check,
|
||||
ChevronDown,
|
||||
LoaderCircle,
|
||||
Play,
|
||||
RefreshCw,
|
||||
Search,
|
||||
Trash2,
|
||||
Terminal,
|
||||
Wrench,
|
||||
X,
|
||||
} from 'lucide-react';
|
||||
@@ -74,6 +78,7 @@ type AgentConfigStatus = {
|
||||
supportedPlatform: boolean;
|
||||
installed: boolean;
|
||||
pluginInstalled: boolean;
|
||||
launchTargets: AgentLaunchTarget[];
|
||||
version: string | null;
|
||||
cliVersion: string | null;
|
||||
appVersion: string | null;
|
||||
@@ -93,6 +98,12 @@ type AgentConfigStatus = {
|
||||
error: string | null;
|
||||
};
|
||||
|
||||
type AgentLaunchTarget = {
|
||||
id: 'app' | 'cli';
|
||||
label: string;
|
||||
detail: string;
|
||||
};
|
||||
|
||||
type AgentConfigActionResult = {
|
||||
outcome: 'applied' | 'default';
|
||||
enabled: boolean;
|
||||
@@ -107,7 +118,7 @@ type PiProviderUpdateStatus = {
|
||||
updateAvailable: boolean;
|
||||
};
|
||||
|
||||
type OAuthLoginRequiredAction = 'enable' | 'apply';
|
||||
type OAuthLoginRequiredAction = 'enable' | 'apply' | 'launch';
|
||||
|
||||
type ClaudeModelMappings = {
|
||||
opus: string;
|
||||
@@ -612,7 +623,7 @@ export function AgentsPage() {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [modelLoading, setModelLoading] = useState(false);
|
||||
const [busyAction, setBusyAction] = useState<
|
||||
'apply' | 'close-config' | 'default' | 'clear' | 'install-pi' | 'update-pi' | 'repair-pi' | 'uninstall-pi' | 'oauth-check' | null
|
||||
'apply' | 'close-config' | 'default' | 'clear' | 'install-pi' | 'update-pi' | 'repair-pi' | 'uninstall-pi' | 'oauth-check' | 'directory' | 'launch' | 'launch-cli' | 'launch-app' | null
|
||||
>(null);
|
||||
const busy = busyAction !== null;
|
||||
const [detectionError, setDetectionError] = useState('');
|
||||
@@ -624,6 +635,11 @@ export function AgentsPage() {
|
||||
const [clearError, setClearError] = useState('');
|
||||
const [clearNotice, setClearNotice] = useState('');
|
||||
const [clearConfirmOpen, setClearConfirmOpen] = useState(false);
|
||||
const [launchError, setLaunchError] = useState('');
|
||||
const [launchDirectoryDialogOpen, setLaunchDirectoryDialogOpen] = useState(false);
|
||||
const [launchDirectory, setLaunchDirectory] = useState('');
|
||||
const [launchDirectoryTarget, setLaunchDirectoryTarget] = useState<AgentLaunchTarget | null>(null);
|
||||
const [launchDirectoryError, setLaunchDirectoryError] = useState('');
|
||||
const [oauthLoginRequiredAction, setOauthLoginRequiredAction] = useState<OAuthLoginRequiredAction | null>(null);
|
||||
const [oauthConfigurationDraft, setOauthConfigurationDraft] = useState<boolean | null>(null);
|
||||
const [piProviderUpdateStatus, setPiProviderUpdateStatus] = useState<PiProviderUpdateStatus | null>(null);
|
||||
@@ -747,6 +763,10 @@ export function AgentsPage() {
|
||||
setClearError('');
|
||||
setClearNotice('');
|
||||
setClearConfirmOpen(false);
|
||||
setLaunchError('');
|
||||
setLaunchDirectoryDialogOpen(false);
|
||||
setLaunchDirectoryTarget(null);
|
||||
setLaunchDirectoryError('');
|
||||
setOauthLoginRequiredAction(null);
|
||||
setOauthConfigurationDraft(null);
|
||||
// Preserve each Claude client's unsaved draft while navigating between clients.
|
||||
@@ -903,6 +923,19 @@ export function AgentsPage() {
|
||||
? claudeMappingsReady && claudeCodeRuntimeSettingsReady
|
||||
: selectedModelOption),
|
||||
);
|
||||
const activeLaunchTargets = activeStatus?.launchTargets ?? [];
|
||||
const defaultLaunchTarget = activeLaunchTargets[0] ?? null;
|
||||
const cliLaunchTarget = activeLaunchTargets.find((target) => target.id === 'cli') ?? null;
|
||||
const appLaunchTarget = activeLaunchTargets.find((target) => target.id === 'app') ?? null;
|
||||
const launchEnabled = Boolean(
|
||||
activeStatus?.supportedPlatform
|
||||
&& activeStatus.installed
|
||||
&& (selected === 'codex'
|
||||
|| (selected === 'pi'
|
||||
? activeStatus.configured
|
||||
: activeStatus.modificationEnabled && activeStatus.modificationState === 'applied')),
|
||||
);
|
||||
const canLaunchTarget = (target: AgentLaunchTarget | null) => launchEnabled && Boolean(target);
|
||||
const modelHint = modelSelectionError
|
||||
|| modelError
|
||||
|| (modelLoading
|
||||
@@ -1249,6 +1282,82 @@ export function AgentsPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const openLaunchDirectoryDialog = (target: AgentLaunchTarget) => {
|
||||
setLaunchDirectory('');
|
||||
setLaunchDirectoryTarget(target);
|
||||
setLaunchDirectoryError('');
|
||||
setLaunchDirectoryDialogOpen(true);
|
||||
};
|
||||
|
||||
const chooseLaunchDirectory = async () => {
|
||||
setBusyAction('directory');
|
||||
setLaunchDirectoryError('');
|
||||
try {
|
||||
const selectedDirectory = await open({
|
||||
directory: true,
|
||||
multiple: false,
|
||||
defaultPath: launchDirectory || undefined,
|
||||
title: t('agents.launchDirectory.dialogTitle', { client: activeDefinition.name }),
|
||||
});
|
||||
if (typeof selectedDirectory === 'string') setLaunchDirectory(selectedDirectory);
|
||||
} catch (requestError) {
|
||||
setLaunchDirectoryError(String(requestError));
|
||||
} finally {
|
||||
setBusyAction(null);
|
||||
}
|
||||
};
|
||||
|
||||
const invokeAgentLaunch = async (
|
||||
target: AgentLaunchTarget,
|
||||
workingDirectory: string | null = null,
|
||||
) => {
|
||||
const action = selected === 'codex'
|
||||
? target.id === 'cli' ? 'launch-cli' : 'launch-app'
|
||||
: 'launch';
|
||||
setBusyAction(action);
|
||||
setLaunchError('');
|
||||
try {
|
||||
if (draftChanged) throw new Error(t('agents.error.applyFirst'));
|
||||
await invoke('launch_agent', {
|
||||
client: selected,
|
||||
target: target.id,
|
||||
workingDirectory,
|
||||
});
|
||||
if (workingDirectory) {
|
||||
setLaunchDirectoryDialogOpen(false);
|
||||
setLaunchDirectoryTarget(null);
|
||||
}
|
||||
} catch (requestError) {
|
||||
if (workingDirectory) {
|
||||
setLaunchDirectoryError(String(requestError));
|
||||
} else if (!handleOAuthLoginError(requestError, 'launch')) {
|
||||
setLaunchError(String(requestError));
|
||||
}
|
||||
} finally {
|
||||
setBusyAction(null);
|
||||
}
|
||||
};
|
||||
|
||||
const launchAgent = async (target: AgentLaunchTarget | null) => {
|
||||
if (!target) return;
|
||||
if (target.id === 'cli') {
|
||||
openLaunchDirectoryDialog(target);
|
||||
return;
|
||||
}
|
||||
await invokeAgentLaunch(target);
|
||||
};
|
||||
|
||||
const launchFromDirectoryDialog = async () => {
|
||||
if (!launchDirectoryTarget) return;
|
||||
const workingDirectory = launchDirectory.trim();
|
||||
if (!workingDirectory) {
|
||||
setLaunchDirectoryError(t('agents.launchDirectory.directoryRequired'));
|
||||
return;
|
||||
}
|
||||
setLaunchDirectoryError('');
|
||||
await invokeAgentLaunch(launchDirectoryTarget, workingDirectory);
|
||||
};
|
||||
|
||||
const openDefaultConfirmation = () => {
|
||||
setDefaultError('');
|
||||
setDefaultConfirmOpen(true);
|
||||
@@ -1714,6 +1823,77 @@ export function AgentsPage() {
|
||||
</section>
|
||||
)}
|
||||
|
||||
<div className="agent-config-footer">
|
||||
<div className="agent-launch-control">
|
||||
<div className="agent-launch-actions">
|
||||
{selected === 'codex' ? (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
className="secondary-button agent-launch-button"
|
||||
onClick={() => void launchAgent(cliLaunchTarget)}
|
||||
disabled={busy || !canLaunchTarget(cliLaunchTarget) || draftChanged}
|
||||
title={draftChanged
|
||||
? t('agents.launch.applyFirst')
|
||||
: cliLaunchTarget?.detail ?? t('agents.launch.unavailable')}
|
||||
>
|
||||
{busyAction === 'launch-cli'
|
||||
? <LoaderCircle size={16} className="spin" />
|
||||
: <Terminal size={16} />}
|
||||
{busyAction === 'launch-cli'
|
||||
? t('agents.launch.starting')
|
||||
: t('agents.launch.startCli')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="primary-button agent-launch-button"
|
||||
onClick={() => void launchAgent(appLaunchTarget)}
|
||||
disabled={busy || !canLaunchTarget(appLaunchTarget) || draftChanged}
|
||||
title={draftChanged
|
||||
? t('agents.launch.applyFirst')
|
||||
: appLaunchTarget?.detail ?? t('agents.launch.unavailable')}
|
||||
>
|
||||
{busyAction === 'launch-app'
|
||||
? <LoaderCircle size={16} className="spin" />
|
||||
: <AppWindow size={16} />}
|
||||
{busyAction === 'launch-app'
|
||||
? t('agents.launch.starting')
|
||||
: t('agents.launch.startApp')}
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
className="primary-button agent-launch-button"
|
||||
onClick={() => void launchAgent(defaultLaunchTarget)}
|
||||
disabled={busy || !canLaunchTarget(defaultLaunchTarget) || draftChanged}
|
||||
title={draftChanged
|
||||
? t('agents.launch.applyFirst')
|
||||
: defaultLaunchTarget?.detail ?? (launchEnabled
|
||||
? t('agents.launch.unavailable')
|
||||
: t('agents.launch.enableFirst'))}
|
||||
>
|
||||
{busyAction === 'launch'
|
||||
? <LoaderCircle size={16} className="spin" />
|
||||
: defaultLaunchTarget?.id === 'cli'
|
||||
? <Terminal size={16} />
|
||||
: <Play size={16} />}
|
||||
{busyAction === 'launch'
|
||||
? t('agents.launch.starting')
|
||||
: defaultLaunchTarget
|
||||
? t('agents.launch.start', { target: defaultLaunchTarget.label })
|
||||
: t('agents.launch.unavailable')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{launchError ? (
|
||||
<span className="agent-inline-message error" role="alert" aria-live="polite">
|
||||
{launchError}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
@@ -1730,6 +1910,82 @@ export function AgentsPage() {
|
||||
</section>
|
||||
</div>
|
||||
|
||||
{launchDirectoryDialogOpen ? (
|
||||
<div className="config-dialog-backdrop" onMouseDown={(event) => {
|
||||
if (event.currentTarget === event.target && !busy) {
|
||||
setLaunchDirectoryDialogOpen(false);
|
||||
setLaunchDirectoryTarget(null);
|
||||
}
|
||||
}}>
|
||||
<section
|
||||
className="config-dialog agent-launch-directory-dialog"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="agent-launch-directory-title"
|
||||
>
|
||||
<div className="config-dialog-heading">
|
||||
<div>
|
||||
<h2 id="agent-launch-directory-title">
|
||||
{t('agents.launchDirectory.dialogTitle', { client: activeDefinition.name })}
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
<p>{t('agents.launchDirectory.dialogDescription', { client: activeDefinition.name })}</p>
|
||||
<div className="config-dialog-field">
|
||||
<span>{t('agents.launchDirectory.workingDirectory')}</span>
|
||||
<button
|
||||
type="button"
|
||||
className="agent-launch-directory-picker"
|
||||
onClick={() => void chooseLaunchDirectory()}
|
||||
disabled={busy}
|
||||
autoFocus
|
||||
>
|
||||
<span>
|
||||
<small>{launchDirectory
|
||||
? t('agents.launchDirectory.selectedDirectory')
|
||||
: t('agents.launchDirectory.noDirectory')}</small>
|
||||
<strong title={launchDirectory || undefined}>
|
||||
{launchDirectory || t('agents.launchDirectory.chooseDirectory')}
|
||||
</strong>
|
||||
</span>
|
||||
<b>{busyAction === 'directory'
|
||||
? t('agents.launchDirectory.choosing')
|
||||
: t('agents.launchDirectory.browse')}</b>
|
||||
</button>
|
||||
</div>
|
||||
{launchDirectoryError ? (
|
||||
<span className="agent-inline-message error" role="alert" aria-live="polite">
|
||||
{launchDirectoryError}
|
||||
</span>
|
||||
) : null}
|
||||
<div className="config-dialog-actions two-actions">
|
||||
<button
|
||||
type="button"
|
||||
className="secondary-button"
|
||||
onClick={() => {
|
||||
setLaunchDirectoryDialogOpen(false);
|
||||
setLaunchDirectoryTarget(null);
|
||||
}}
|
||||
disabled={busy}
|
||||
>
|
||||
{t('common.cancel')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="primary-button"
|
||||
onClick={() => void launchFromDirectoryDialog()}
|
||||
disabled={busy || !launchDirectory.trim() || !launchDirectoryTarget}
|
||||
>
|
||||
{busyAction === 'launch' || busyAction === 'launch-cli'
|
||||
? <LoaderCircle size={16} className="spin" />
|
||||
: null}
|
||||
{t('agents.launchDirectory.launch', { client: activeDefinition.name })}
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{defaultConfirmOpen ? (
|
||||
<div className="config-dialog-backdrop">
|
||||
<section className="config-dialog agent-restore-dialog" role="alertdialog" aria-modal="true" aria-labelledby="agent-default-title">
|
||||
|
||||
+122
-1
@@ -5523,8 +5523,104 @@ textarea:focus-visible {
|
||||
}
|
||||
|
||||
.agent-config-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 14px;
|
||||
min-height: 48px;
|
||||
padding-top: 14px;
|
||||
border-top: 1px solid var(--theme-e3e1db);
|
||||
}
|
||||
|
||||
.agent-launch-control {
|
||||
display: grid;
|
||||
min-width: 0;
|
||||
justify-items: end;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.agent-launch-actions {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.agent-launch-actions > button {
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.agent-launch-control > .agent-inline-message {
|
||||
max-width: 420px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.agent-launch-directory-dialog {
|
||||
grid-template-rows: auto;
|
||||
width: min(620px, calc(100vw - 32px));
|
||||
height: auto;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.agent-launch-directory-dialog > p {
|
||||
margin: 0;
|
||||
color: var(--theme-5f5a53);
|
||||
font-size: var(--font-size-label);
|
||||
line-height: var(--line-height-relaxed);
|
||||
}
|
||||
|
||||
.agent-launch-directory-picker {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
min-height: 64px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
padding: 11px 13px;
|
||||
border: 1px solid var(--theme-d8d2c8);
|
||||
border-radius: 9px;
|
||||
background: var(--theme-fff);
|
||||
color: var(--theme-2d2a26);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.agent-launch-directory-picker:hover:not(:disabled) {
|
||||
border-color: var(--theme-bdb5a9);
|
||||
background: var(--theme-faf8f4);
|
||||
}
|
||||
|
||||
.agent-launch-directory-picker:focus-visible {
|
||||
border-color: var(--theme-8d8377);
|
||||
outline: 0;
|
||||
box-shadow: 0 0 0 3px rgba(45, 42, 38, 0.1);
|
||||
}
|
||||
|
||||
.agent-launch-directory-picker > span {
|
||||
display: grid;
|
||||
min-width: 0;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.agent-launch-directory-picker small {
|
||||
color: var(--theme-8a847c);
|
||||
font-size: var(--font-size-meta);
|
||||
}
|
||||
|
||||
.agent-launch-directory-picker strong {
|
||||
overflow: hidden;
|
||||
font-size: var(--font-size-label);
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.agent-launch-directory-picker > b {
|
||||
flex: none;
|
||||
color: var(--theme-8069ad);
|
||||
font-size: var(--font-size-label);
|
||||
}
|
||||
|
||||
.agent-status-grid {
|
||||
@@ -7423,7 +7519,8 @@ textarea:focus-visible {
|
||||
}
|
||||
|
||||
.agent-status-grid,
|
||||
.agent-status-messages {
|
||||
.agent-status-messages,
|
||||
.agent-config-footer {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
@@ -8052,6 +8149,30 @@ textarea:focus-visible {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.agent-config-footer,
|
||||
.agent-launch-actions {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.agent-launch-control {
|
||||
width: 100%;
|
||||
justify-items: stretch;
|
||||
}
|
||||
|
||||
.agent-launch-actions {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.agent-launch-actions > button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.agent-launch-control > .agent-inline-message {
|
||||
max-width: none;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.oauth-card {
|
||||
grid-template-rows: auto auto auto;
|
||||
min-height: 0;
|
||||
|
||||
Reference in new issue
Block a user