From d0609e48f0047bee4decde2d4a73730a90c38b03 Mon Sep 17 00:00:00 2001 From: lzt404 <2596933790@qq.com> Date: Sun, 16 Aug 2026 01:37:38 +0800 Subject: [PATCH] feat(agents): restore quick launch support for agent clients --- bun.lock | 3 + package.json | 1 + src-tauri/Cargo.lock | 144 +++++++++- src-tauri/Cargo.toml | 1 + src-tauri/capabilities/default.json | 3 +- src-tauri/src/agents.rs | 2 + src-tauri/src/agents/discovery.rs | 97 ++++++- src-tauri/src/agents/launch.rs | 397 ++++++++++++++++++++++++++++ src-tauri/src/main.rs | 11 + src/i18n/ja.ts | 18 ++ src/i18n/locales/en.ts | 18 ++ src/i18n/locales/zh-CN.ts | 18 ++ src/pages/AgentsPage.tsx | 260 +++++++++++++++++- src/styles.css | 123 ++++++++- 14 files changed, 1083 insertions(+), 13 deletions(-) create mode 100644 src-tauri/src/agents/launch.rs diff --git a/bun.lock b/bun.lock index a9153b2..4c4d04e 100644 --- a/bun.lock +++ b/bun.lock @@ -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=="], diff --git a/package.json b/package.json index 4a1c659..b51334e 100644 --- a/package.json +++ b/package.json @@ -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" diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index cf25c25..4b4be79 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -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" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 67ffab7..979b968 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -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" diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index 0a29d28..e06f87d 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -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" ] } diff --git a/src-tauri/src/agents.rs b/src-tauri/src/agents.rs index 0af46f0..4887ed0 100644 --- a/src-tauri/src/agents.rs +++ b/src-tauri/src/agents.rs @@ -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::*; diff --git a/src-tauri/src/agents/discovery.rs b/src-tauri/src/agents/discovery.rs index a73aa08..2cea8a0 100644 --- a/src-tauri/src/agents/discovery.rs +++ b/src-tauri/src/agents/discovery.rs @@ -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 { + 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 { find_claude_desktop_executable(home).and_then(|path| read_agent_version(&path)) } -pub(crate) fn read_codex_app_version(home: &Path) -> Option { +pub(crate) fn read_codex_app_installation_version(installation: &CodexAppTarget) -> Option { #[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 } } diff --git a/src-tauri/src/agents/launch.rs b/src-tauri/src/agents/launch.rs new file mode 100644 index 0000000..14c317b --- /dev/null +++ b/src-tauri/src/agents/launch.rs @@ -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, + working_directory: Option, +) -> 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 { + 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::>() + .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::>(), + ["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"); + } +} diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 5336ec3..a24a01d 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -763,6 +763,7 @@ struct AgentConfigStatus { supported_platform: bool, installed: bool, plugin_installed: bool, + launch_targets: Vec, version: Option, cli_version: Option, app_version: Option, @@ -784,6 +785,14 @@ struct AgentConfigStatus { error: Option, } +#[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, diff --git a/src/i18n/ja.ts b/src/i18n/ja.ts index d328521..3c29477 100644 --- a/src/i18n/ja.ts +++ b/src/i18n/ja.ts @@ -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': '設定を更新', diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts index af1536c..0d9aff4 100644 --- a/src/i18n/locales/en.ts +++ b/src/i18n/locales/en.ts @@ -500,6 +500,23 @@ export const en: Record = { '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 = { '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', diff --git a/src/i18n/locales/zh-CN.ts b/src/i18n/locales/zh-CN.ts index 2e38336..c151822 100644 --- a/src/i18n/locales/zh-CN.ts +++ b/src/i18n/locales/zh-CN.ts @@ -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': '更新配置', diff --git a/src/pages/AgentsPage.tsx b/src/pages/AgentsPage.tsx index 9a10840..0f3a3ef 100644 --- a/src/pages/AgentsPage.tsx +++ b/src/pages/AgentsPage.tsx @@ -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(null); + const [launchDirectoryError, setLaunchDirectoryError] = useState(''); const [oauthLoginRequiredAction, setOauthLoginRequiredAction] = useState(null); const [oauthConfigurationDraft, setOauthConfigurationDraft] = useState(null); const [piProviderUpdateStatus, setPiProviderUpdateStatus] = useState(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() { )} +
+
+
+ {selected === 'codex' ? ( + <> + + + + ) : ( + + )} +
+ {launchError ? ( + + {launchError} + + ) : null} +
+
+ ) : null} @@ -1730,6 +1910,82 @@ export function AgentsPage() { + {launchDirectoryDialogOpen ? ( +
{ + if (event.currentTarget === event.target && !busy) { + setLaunchDirectoryDialogOpen(false); + setLaunchDirectoryTarget(null); + } + }}> +
+
+
+

+ {t('agents.launchDirectory.dialogTitle', { client: activeDefinition.name })} +

+
+
+

{t('agents.launchDirectory.dialogDescription', { client: activeDefinition.name })}

+
+ {t('agents.launchDirectory.workingDirectory')} + +
+ {launchDirectoryError ? ( + + {launchDirectoryError} + + ) : null} +
+ + +
+
+
+ ) : null} + {defaultConfirmOpen ? (
diff --git a/src/styles.css b/src/styles.css index d3c0e24..73481d9 100644 --- a/src/styles.css +++ b/src/styles.css @@ -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;