feat(agents): overhaul Claude Code configuration and launch flow

- add role-based Opus, Sonnet, and Haiku model mappings
- redesign the Claude Code configuration interface
- launch the CLI with a selected working directory
- use the native system directory picker
- inject managed environment variables at runtime
- use token-only authentication and migrate legacy dual-auth configs
- persist the working directory prompt preference
This commit is contained in:
lzt404 committed 2026-08-02 16:59:27 +08:00
1 parent efc8811d0c
commit faa7e97326
11 files changed
+1208 -136

No files matched your search

+3
View File
@@ -6,6 +6,7 @@
"name": "cpa-gui",
"dependencies": {
"@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",
@@ -199,6 +200,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=="],
+1
View File
@@ -13,6 +13,7 @@
},
"dependencies": {
"@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"
+143 -1
View File
@@ -605,6 +605,7 @@ dependencies = [
"tar",
"tauri",
"tauri-build",
"tauri-plugin-dialog",
"tauri-plugin-opener",
"tokio",
"tokio-util",
@@ -2487,6 +2488,7 @@ checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272"
dependencies = [
"bitflags 2.13.0",
"block2",
"libc",
"objc2",
"objc2-core-foundation",
]
@@ -3168,6 +3170,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"
@@ -4004,6 +4030,48 @@ dependencies = [
"walkdir",
]
[[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"
@@ -5151,6 +5219,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"
@@ -5184,13 +5261,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"
@@ -5221,6 +5315,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"
@@ -5233,6 +5333,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"
@@ -5245,12 +5351,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"
@@ -5263,6 +5381,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"
@@ -5275,6 +5399,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"
@@ -5287,6 +5417,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"
@@ -5299,6 +5435,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"
+1
View File
@@ -26,6 +26,7 @@ serde_yaml = "0.9"
sha2 = "0.10"
tar = "0.4"
tauri = { version = "~2.11", features = ["tray-icon"] }
tauri-plugin-dialog = "2.7.1"
tauri-plugin-opener = "2"
tokio = { version = "1", features = ["macros", "sync", "time"] }
tokio-util = "0.7"
+2 -1
View File
@@ -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"
]
}
+443 -26
View File
@@ -479,6 +479,8 @@ struct GuiConfigFile {
routing_session_affinity: bool,
routing_session_affinity_ttl: String,
codex_session_repair_on_launch: bool,
claude_code_working_directory: String,
claude_code_working_directory_prompt_disabled: bool,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize)]
@@ -566,6 +568,8 @@ impl Default for GuiConfigFile {
routing_session_affinity: false,
routing_session_affinity_ttl: String::new(),
codex_session_repair_on_launch: false,
claude_code_working_directory: String::new(),
claude_code_working_directory_prompt_disabled: false,
}
}
}
@@ -588,6 +592,8 @@ struct GuiConfigPresence {
proxy_url: Option<String>,
routing_session_affinity: Option<bool>,
routing_session_affinity_ttl: Option<String>,
claude_code_working_directory: Option<String>,
claude_code_working_directory_prompt_disabled: Option<bool>,
}
#[derive(Clone, Serialize)]
@@ -618,7 +624,10 @@ struct AgentConfigStatus {
modification_state: String,
backup_available: bool,
applied_model: Option<String>,
claude_code_model_mappings: Option<ClaudeDesktopModelMappings>,
claude_desktop_model_mappings: Option<ClaudeDesktopModelMappings>,
claude_code_working_directory: Option<String>,
claude_code_working_directory_prompt_disabled: bool,
warnings: Vec<String>,
error: Option<String>,
}
@@ -874,6 +883,7 @@ struct AgentConfigurationOptions<'a> {
models: &'a [AgentModelOption],
codex_catalog: Option<&'a str>,
oauth_configuration: bool,
claude_code_model_mappings: Option<&'a ClaudeDesktopModelMappings>,
claude_desktop_model_mappings: Option<&'a ClaudeDesktopModelMappings>,
}
@@ -1330,6 +1340,20 @@ impl GuiConfigState {
})
}
fn set_claude_code_launch_preferences(
&self,
working_directory: Option<&Path>,
prompt_disabled: bool,
) -> Result<GuiConfigFile, String> {
self.update(|config| {
if let Some(working_directory) = working_directory {
config.claude_code_working_directory = path_to_string(working_directory);
}
config.claude_code_working_directory_prompt_disabled = prompt_disabled;
Ok(())
})
}
fn set_locale(&self, locale: String) -> Result<GuiConfigFile, String> {
self.update(|config| {
config.locale = normalize_app_locale(&locale).to_string();
@@ -1612,14 +1636,14 @@ fn resolve_windows_close_request(
fn inspect_agent_config_statuses(
app: &tauri::AppHandle,
port: u16,
api_key: &str,
config: &GuiConfigFile,
) -> Result<Vec<AgentConfigStatus>, String> {
let home = app
.path()
.home_dir()
.map_err(|error| format!("无法获取用户目录: {error}"))?;
Ok([
let api_key = effective_agent_api_key(config);
let mut statuses = [
AgentClient::ClaudeCode,
AgentClient::ClaudeDesktop,
AgentClient::Codex,
@@ -1628,8 +1652,19 @@ fn inspect_agent_config_statuses(
AgentClient::Hermes,
]
.into_iter()
.map(|client| inspect_agent_config(client, &home, port, api_key))
.collect())
.map(|client| inspect_agent_config(client, &home, config.port, api_key))
.collect::<Vec<_>>();
if let Some(status) = statuses
.iter_mut()
.find(|status| status.id == AgentClient::ClaudeCode.id())
{
status.claude_code_working_directory =
(!config.claude_code_working_directory.trim().is_empty())
.then(|| config.claude_code_working_directory.clone());
status.claude_code_working_directory_prompt_disabled =
config.claude_code_working_directory_prompt_disabled;
}
Ok(statuses)
}
fn refresh_agent_config_status_cache(
@@ -1644,7 +1679,7 @@ fn refresh_agent_config_status_cache(
let config = gui_config_state.snapshot()?;
let port = config.port;
let api_key = effective_agent_api_key(&config);
let statuses = inspect_agent_config_statuses(app, port, api_key)?;
let statuses = inspect_agent_config_statuses(app, &config)?;
cache.replace(port, api_key, statuses.clone())?;
Ok(statuses)
}
@@ -1688,6 +1723,29 @@ async fn get_agent_models(
Ok(fetch_prepared_agent_models(client, &config).await?.models)
}
fn validate_claude_code_working_directory(value: &str) -> Result<PathBuf, String> {
let value = value.trim();
if value.is_empty() {
return Err(
"CLAUDE_CODE_WORKING_DIRECTORY_REQUIRED: Please select a working directory".to_string(),
);
}
if value.chars().any(char::is_control) {
return Err("The Claude Code working directory contains invalid characters".to_string());
}
let path = PathBuf::from(value);
if !path.is_absolute() {
return Err("The Claude Code working directory must be an absolute path".to_string());
}
if !path.is_dir() {
return Err(format!(
"CLAUDE_CODE_WORKING_DIRECTORY_INVALID: The working directory does not exist: {}",
path_to_string(&path)
));
}
Ok(path)
}
#[tauri::command]
fn check_codex_oauth_login(app: tauri::AppHandle) -> Result<(), String> {
let home = app
@@ -2111,6 +2169,25 @@ fn resolve_claude_desktop_model_mappings(
}))
}
fn resolve_claude_code_model_mappings(
client: AgentClient,
models: &[AgentModelOption],
selected_model: &str,
requested: Option<ClaudeDesktopModelMappings>,
) -> Result<Option<ClaudeDesktopModelMappings>, String> {
if client != AgentClient::ClaudeCode {
return Ok(None);
}
let requested = requested.unwrap_or_else(|| ClaudeDesktopModelMappings::all(selected_model));
let resolve =
|model: &str| resolve_available_agent_model(models, &validate_agent_model(model)?);
Ok(Some(ClaudeDesktopModelMappings {
opus: resolve(&requested.opus)?,
sonnet: resolve(&requested.sonnet)?,
haiku: resolve(&requested.haiku)?,
}))
}
fn prepare_codex_agent_models(
runtime_models: &[codex_catalog::CodexRuntimeModel],
) -> Result<PreparedAgentModels, String> {
@@ -2136,6 +2213,7 @@ async fn apply_agent_config(
client: String,
model: String,
oauth_configuration: bool,
claude_code_model_mappings: Option<ClaudeDesktopModelMappings>,
claude_desktop_model_mappings: Option<ClaudeDesktopModelMappings>,
) -> Result<AgentConfigActionResult, String> {
let client = AgentClient::parse(&client)?;
@@ -2151,6 +2229,12 @@ async fn apply_agent_config(
validate_agent_can_enable(client, &home, config.port, api_key)?;
let prepared = fetch_prepared_agent_models(client, &config).await?;
let model = resolve_available_agent_model(&prepared.models, &validate_agent_model(&model)?)?;
let claude_code_model_mappings = resolve_claude_code_model_mappings(
client,
&prepared.models,
&model,
claude_code_model_mappings,
)?;
let claude_desktop_model_mappings = resolve_claude_desktop_model_mappings(
client,
&prepared.models,
@@ -2173,6 +2257,7 @@ async fn apply_agent_config(
models: &prepared.models,
codex_catalog: prepared.codex_catalog.as_deref(),
oauth_configuration,
claude_code_model_mappings: claude_code_model_mappings.as_ref(),
claude_desktop_model_mappings: claude_desktop_model_mappings.as_ref(),
},
)
@@ -2202,6 +2287,7 @@ async fn reset_agent_config_to_default(
client: String,
model: String,
oauth_configuration: bool,
claude_code_model_mappings: Option<ClaudeDesktopModelMappings>,
claude_desktop_model_mappings: Option<ClaudeDesktopModelMappings>,
) -> Result<AgentConfigActionResult, String> {
let client = AgentClient::parse(&client)?;
@@ -2217,6 +2303,12 @@ async fn reset_agent_config_to_default(
validate_agent_can_enable(client, &home, config.port, api_key)?;
let prepared = fetch_prepared_agent_models(client, &config).await?;
let model = resolve_available_agent_model(&prepared.models, &validate_agent_model(&model)?)?;
let claude_code_model_mappings = resolve_claude_code_model_mappings(
client,
&prepared.models,
&model,
claude_code_model_mappings,
)?;
let claude_desktop_model_mappings = resolve_claude_desktop_model_mappings(
client,
&prepared.models,
@@ -2238,6 +2330,7 @@ async fn reset_agent_config_to_default(
models: &prepared.models,
codex_catalog: prepared.codex_catalog.as_deref(),
oauth_configuration,
claude_code_model_mappings: claude_code_model_mappings.as_ref(),
claude_desktop_model_mappings: claude_desktop_model_mappings.as_ref(),
})
}
@@ -2262,6 +2355,7 @@ async fn set_agent_config_enabled(
model: String,
enabled: bool,
force_restore: bool,
claude_code_model_mappings: Option<ClaudeDesktopModelMappings>,
claude_desktop_model_mappings: Option<ClaudeDesktopModelMappings>,
) -> Result<AgentConfigActionResult, String> {
let client = AgentClient::parse(&client)?;
@@ -2278,6 +2372,12 @@ async fn set_agent_config_enabled(
let prepared = fetch_prepared_agent_models(client, &config).await?;
let model =
resolve_available_agent_model(&prepared.models, &validate_agent_model(&model)?)?;
let claude_code_model_mappings = resolve_claude_code_model_mappings(
client,
&prepared.models,
&model,
claude_code_model_mappings,
)?;
let claude_desktop_model_mappings = resolve_claude_desktop_model_mappings(
client,
&prepared.models,
@@ -2300,6 +2400,7 @@ async fn set_agent_config_enabled(
models: &prepared.models,
codex_catalog: prepared.codex_catalog.as_deref(),
oauth_configuration: false,
claude_code_model_mappings: claude_code_model_mappings.as_ref(),
claude_desktop_model_mappings: claude_desktop_model_mappings.as_ref(),
},
)
@@ -2318,6 +2419,7 @@ async fn update_agent_config(
gui_config_state: tauri::State<'_, GuiConfigState>,
client: String,
model: String,
claude_code_model_mappings: Option<ClaudeDesktopModelMappings>,
claude_desktop_model_mappings: Option<ClaudeDesktopModelMappings>,
) -> Result<AgentConfigActionResult, String> {
let client = AgentClient::parse(&client)?;
@@ -2330,6 +2432,12 @@ async fn update_agent_config(
let api_key = effective_agent_api_key(&config);
let prepared = fetch_prepared_agent_models(client, &config).await?;
let model = resolve_available_agent_model(&prepared.models, &validate_agent_model(&model)?)?;
let claude_code_model_mappings = resolve_claude_code_model_mappings(
client,
&prepared.models,
&model,
claude_code_model_mappings,
)?;
let claude_desktop_model_mappings = resolve_claude_desktop_model_mappings(
client,
&prepared.models,
@@ -2352,6 +2460,7 @@ async fn update_agent_config(
models: &prepared.models,
codex_catalog: prepared.codex_catalog.as_deref(),
oauth_configuration: false,
claude_code_model_mappings: claude_code_model_mappings.as_ref(),
claude_desktop_model_mappings: claude_desktop_model_mappings.as_ref(),
},
)
@@ -2361,8 +2470,11 @@ async fn update_agent_config(
async fn launch_agent(
app: tauri::AppHandle,
gui_config_state: tauri::State<'_, GuiConfigState>,
cache: tauri::State<'_, AgentConfigStatusCache>,
client: String,
target: Option<String>,
working_directory: Option<String>,
suppress_working_directory_prompt: Option<bool>,
) -> Result<(), String> {
let client = AgentClient::parse(&client)?;
if !client.supported_platform() {
@@ -2430,7 +2542,59 @@ async fn launch_agent(
}
let executable = find_agent_executable(client, &home)
.ok_or_else(|| format!("未找到 {} 的可执行文件", client.name()))?;
launch_cli_agent(&executable, client.name(), &home)
if client == AgentClient::ClaudeCode {
let explicit_working_directory = working_directory
.as_deref()
.map(validate_claude_code_working_directory)
.transpose()?;
let selected_working_directory = match explicit_working_directory {
Some(path) => path,
None if config.claude_code_working_directory_prompt_disabled => {
validate_claude_code_working_directory(&config.claude_code_working_directory)?
}
None => {
return Err(
"CLAUDE_CODE_WORKING_DIRECTORY_REQUIRED: Please select a working directory"
.to_string(),
)
}
};
if working_directory.is_some() {
gui_config_state.set_claude_code_launch_preferences(
Some(&selected_working_directory),
suppress_working_directory_prompt.unwrap_or(false),
)?;
cache.clear()?;
}
let claude_config_path = agent_config_paths(client, &home)[0].clone();
{
let _guard = AGENT_CONFIG_FILE_LOCK
.lock()
.map_err(|_| "Claude Code configuration lock is poisoned".to_string())?;
remove_claude_code_conflicting_api_key(&claude_config_path)?;
}
let mappings = inspect_claude_code_model_mappings(&claude_config_path)?
.or_else(|| {
status
.current_model
.as_deref()
.map(ClaudeDesktopModelMappings::all)
})
.ok_or_else(|| "Claude Code model environment is not configured".to_string())?;
let environment = claude_code_launch_environment(
&format!("http://127.0.0.1:{port}"),
effective_agent_api_key(&config),
&mappings,
);
return launch_cli_agent(
&executable,
client.name(),
&selected_working_directory,
&environment,
&["ANTHROPIC_API_KEY"],
);
}
launch_cli_agent(&executable, client.name(), &home, &[], &[])
}
fn validate_agent_launch_modification(
@@ -2994,7 +3158,12 @@ fn inspect_agent_config(
modification_state: modification.state,
backup_available: modification.backup_available,
applied_model: modification.applied_model,
claude_code_model_mappings: (client == AgentClient::ClaudeCode)
.then(|| inspect_claude_code_model_mappings(&paths[0]).ok().flatten())
.flatten(),
claude_desktop_model_mappings: modification.claude_desktop_model_mappings,
claude_code_working_directory: None,
claude_code_working_directory_prompt_disabled: false,
warnings,
error,
}
@@ -4005,10 +4174,24 @@ fn launch_cli_agent(
executable: &Path,
label: &str,
working_directory: &Path,
environment: &[(String, String)],
environment_to_remove: &[&str],
) -> Result<(), String> {
let environment_removals = environment_to_remove
.iter()
.map(|key| format!("-u {}", shell_single_quote(key)))
.collect::<Vec<_>>()
.join(" ");
let environment = environment
.iter()
.map(|(key, value)| format!("{key}={}", shell_single_quote(value)))
.collect::<Vec<_>>()
.join(" ");
let command_line = format!(
"cd {} && exec {}",
"cd {} && exec env {} {} {}",
shell_single_quote(&path_to_string(working_directory)),
environment_removals,
environment,
shell_single_quote(&path_to_string(executable))
);
let apple_script_command = command_line.replace('\\', "\\\\").replace('"', "\\\"");
@@ -4039,6 +4222,8 @@ fn launch_cli_agent(
executable: &Path,
label: &str,
working_directory: &Path,
environment: &[(String, String)],
environment_to_remove: &[&str],
) -> Result<(), String> {
let terminals: &[(&str, &[&str])] = &[
("x-terminal-emulator", &["-e"]),
@@ -4058,15 +4243,19 @@ fn launch_cli_agent(
if !available {
continue;
}
match Command::new(terminal)
let mut command = Command::new(terminal);
command
.args(*arguments)
.arg(executable)
.envs(environment.iter().map(|(key, value)| (key, value)))
.current_dir(working_directory)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
{
.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()),
}
@@ -4082,13 +4271,21 @@ fn launch_cli_agent(
executable: &Path,
label: &str,
working_directory: &Path,
environment: &[(String, String)],
environment_to_remove: &[&str],
) -> Result<(), String> {
use std::os::windows::process::CommandExt;
const CREATE_NEW_CONSOLE: u32 = 0x0000_0010;
windows_command_for_executable(executable, true)
let mut command = windows_command_for_executable(executable, true);
command
.envs(environment.iter().map(|(key, value)| (key, value)))
.current_dir(working_directory)
.creation_flags(CREATE_NEW_CONSOLE)
.creation_flags(CREATE_NEW_CONSOLE);
for key in environment_to_remove {
command.env_remove(key);
}
command
.spawn()
.map(|_| ())
.map_err(|error| format!("启动 {label} 失败: {error}"))
@@ -4099,6 +4296,8 @@ fn launch_cli_agent(
_executable: &Path,
label: &str,
_working_directory: &Path,
_environment: &[(String, String)],
_environment_to_remove: &[&str],
) -> Result<(), String> {
Err(format!("当前平台不支持启动 {label}"))
}
@@ -4136,6 +4335,68 @@ fn inspect_claude_agent_config(
Ok((configured, model))
}
fn inspect_claude_code_model_mappings(
path: &Path,
) -> Result<Option<ClaudeDesktopModelMappings>, String> {
if !path.is_file() {
return Ok(None);
}
let root: serde_json::Value = serde_json::from_str(
&fs::read_to_string(path)
.map_err(|error| format!("Failed to read Claude Code configuration: {error}"))?,
)
.map_err(|error| format!("Failed to parse Claude Code configuration: {error}"))?;
let Some(env) = root.get("env").and_then(serde_json::Value::as_object) else {
return Ok(None);
};
let fallback = env
.get("ANTHROPIC_MODEL")
.and_then(serde_json::Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty());
let read_model = |key: &str| {
env.get(key)
.and_then(serde_json::Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
.or(fallback)
.map(str::to_string)
};
let Some(opus) = read_model("ANTHROPIC_DEFAULT_OPUS_MODEL") else {
return Ok(None);
};
let Some(sonnet) = read_model("ANTHROPIC_DEFAULT_SONNET_MODEL") else {
return Ok(None);
};
let Some(haiku) = read_model("ANTHROPIC_DEFAULT_HAIKU_MODEL") else {
return Ok(None);
};
Ok(Some(ClaudeDesktopModelMappings {
opus,
sonnet,
haiku,
}))
}
fn claude_code_launch_environment(
base_url: &str,
api_key: &str,
mappings: &ClaudeDesktopModelMappings,
) -> Vec<(String, String)> {
[
("ANTHROPIC_BASE_URL", base_url),
("ANTHROPIC_AUTH_TOKEN", api_key),
("ANTHROPIC_MODEL", mappings.sonnet.as_str()),
("ANTHROPIC_DEFAULT_HAIKU_MODEL", mappings.haiku.as_str()),
("ANTHROPIC_DEFAULT_SONNET_MODEL", mappings.sonnet.as_str()),
("ANTHROPIC_DEFAULT_OPUS_MODEL", mappings.opus.as_str()),
("ANTHROPIC_DEFAULT_FABLE_MODEL", mappings.sonnet.as_str()),
]
.into_iter()
.map(|(key, value)| (key.to_string(), value.to_string()))
.collect()
}
fn inspect_claude_desktop_agent_config(
paths: &[PathBuf],
port: u16,
@@ -4412,6 +4673,7 @@ fn build_agent_updates(
models,
codex_catalog,
oauth_configuration: false,
claude_code_model_mappings: None,
claude_desktop_model_mappings: None,
},
)
@@ -4429,6 +4691,7 @@ fn build_agent_updates_with_oauth(
models,
codex_catalog,
oauth_configuration,
claude_code_model_mappings,
claude_desktop_model_mappings,
} = options;
let paths = agent_config_paths(client, home);
@@ -4437,8 +4700,22 @@ fn build_agent_updates_with_oauth(
match client {
AgentClient::ClaudeCode => {
let before = read_optional_text(&paths[0])?;
let after = build_claude_agent_config(before.as_deref(), &root_base, api_key, model)
.or_else(|_| build_claude_agent_config(None, &root_base, api_key, model))?;
let after = build_claude_agent_config(
before.as_deref(),
&root_base,
api_key,
model,
claude_code_model_mappings,
)
.or_else(|_| {
build_claude_agent_config(
None,
&root_base,
api_key,
model,
claude_code_model_mappings,
)
})?;
Ok(vec![AgentFileUpdate {
path: paths[0].clone(),
after,
@@ -4580,6 +4857,7 @@ fn build_claude_agent_config(
base_url: &str,
api_key: &str,
model: &str,
mappings: Option<&ClaudeDesktopModelMappings>,
) -> Result<String, String> {
let mut root = match existing.map(str::trim).filter(|value| !value.is_empty()) {
Some(value) => serde_json::from_str::<serde_json::Value>(value)
@@ -4589,16 +4867,19 @@ fn build_claude_agent_config(
let root = root
.as_object_mut()
.ok_or_else(|| "Claude Code settings.json 根节点必须是对象".to_string())?;
let mappings = mappings
.cloned()
.unwrap_or_else(|| ClaudeDesktopModelMappings::all(model));
let env = ensure_json_object_entry(root, "env");
env.remove("ANTHROPIC_API_KEY");
for (key, value) in [
("ANTHROPIC_BASE_URL", base_url),
("ANTHROPIC_API_KEY", api_key),
("ANTHROPIC_AUTH_TOKEN", api_key),
("ANTHROPIC_MODEL", model),
("ANTHROPIC_DEFAULT_HAIKU_MODEL", model),
("ANTHROPIC_DEFAULT_SONNET_MODEL", model),
("ANTHROPIC_DEFAULT_OPUS_MODEL", model),
("ANTHROPIC_DEFAULT_FABLE_MODEL", model),
("ANTHROPIC_MODEL", mappings.sonnet.as_str()),
("ANTHROPIC_DEFAULT_HAIKU_MODEL", mappings.haiku.as_str()),
("ANTHROPIC_DEFAULT_SONNET_MODEL", mappings.sonnet.as_str()),
("ANTHROPIC_DEFAULT_OPUS_MODEL", mappings.opus.as_str()),
("ANTHROPIC_DEFAULT_FABLE_MODEL", mappings.sonnet.as_str()),
] {
env.insert(
key.to_string(),
@@ -4607,7 +4888,7 @@ fn build_claude_agent_config(
}
root.insert(
"model".to_string(),
serde_json::Value::String(model.to_string()),
serde_json::Value::String(mappings.sonnet),
);
let mut rendered = serde_json::to_string_pretty(&serde_json::Value::Object(root.clone()))
.map_err(|error| format!("生成 Claude Code 配置失败: {error}"))?;
@@ -4615,6 +4896,23 @@ fn build_claude_agent_config(
Ok(rendered)
}
fn remove_claude_code_conflicting_api_key(path: &Path) -> Result<bool, String> {
update_agent_json_file(path, "Claude Code configuration", |root| {
let managed = root
.get("env")
.and_then(serde_json::Value::as_object)
.and_then(|env| env.get("ANTHROPIC_BASE_URL"))
.and_then(serde_json::Value::as_str)
.is_some_and(is_managed_agent_base_url);
if !managed {
return false;
}
root.get_mut("env")
.and_then(serde_json::Value::as_object_mut)
.is_some_and(|env| env.remove("ANTHROPIC_API_KEY").is_some())
})
}
fn parse_agent_json_object(
existing: Option<&str>,
label: &str,
@@ -6963,7 +7261,7 @@ fn fresh_agent_contents(
is_alias: false,
context_window: None,
}];
fresh_agent_contents_with_oauth(client, port, api_key, model, &models, false, None)
fresh_agent_contents_with_oauth(client, port, api_key, model, &models, false, None, None)
}
fn fresh_agent_contents_with_oauth(
@@ -6973,13 +7271,18 @@ fn fresh_agent_contents_with_oauth(
model: &str,
models: &[AgentModelOption],
oauth_configuration: bool,
claude_code_model_mappings: Option<&ClaudeDesktopModelMappings>,
claude_desktop_model_mappings: Option<&ClaudeDesktopModelMappings>,
) -> Result<Vec<String>, String> {
let root_base = format!("http://127.0.0.1:{port}");
let openai_base = format!("{root_base}/v1");
match client {
AgentClient::ClaudeCode => Ok(vec![build_claude_agent_config(
None, &root_base, api_key, model,
None,
&root_base,
api_key,
model,
claude_code_model_mappings,
)?]),
AgentClient::ClaudeDesktop => Ok(vec![
build_claude_desktop_deployment_config(None)?,
@@ -7757,6 +8060,7 @@ fn apply_agent_configuration(
models,
codex_catalog,
oauth_configuration: false,
claude_code_model_mappings: None,
claude_desktop_model_mappings: None,
},
)
@@ -7805,6 +8109,7 @@ fn reset_agent_configuration_to_default(
}],
codex_catalog,
oauth_configuration: false,
claude_code_model_mappings: None,
claude_desktop_model_mappings: None,
})
}
@@ -7818,6 +8123,7 @@ struct AgentDefaultConfiguration<'a> {
models: &'a [AgentModelOption],
codex_catalog: Option<&'a str>,
oauth_configuration: bool,
claude_code_model_mappings: Option<&'a ClaudeDesktopModelMappings>,
claude_desktop_model_mappings: Option<&'a ClaudeDesktopModelMappings>,
}
@@ -7833,6 +8139,7 @@ fn reset_agent_configuration_to_default_with_oauth(
models,
codex_catalog,
oauth_configuration,
claude_code_model_mappings,
claude_desktop_model_mappings,
} = request;
let paths = agent_config_paths(client, home);
@@ -7843,6 +8150,7 @@ fn reset_agent_configuration_to_default_with_oauth(
model,
models,
oauth_configuration,
claude_code_model_mappings,
claude_desktop_model_mappings,
)?;
if paths.len() != contents.len() {
@@ -14075,6 +14383,13 @@ fn load_or_create_gui_config() -> Result<GuiConfigFile, String> {
if presence.codex_session_repair_on_launch.is_none() {
changed = true;
}
if presence.claude_code_working_directory.is_none()
|| presence
.claude_code_working_directory_prompt_disabled
.is_none()
{
changed = true;
}
let management_secret_rotated = ensure_strong_management_secret(&mut config)?;
changed |= management_secret_rotated;
changed |= sanitize_gui_config(&mut config)?;
@@ -14355,6 +14670,11 @@ fn sanitize_gui_config(config: &mut GuiConfigFile) -> Result<bool, String> {
config.routing_session_affinity_ttl = routing_session_affinity_ttl;
changed = true;
}
let claude_code_working_directory = config.claude_code_working_directory.trim().to_string();
if config.claude_code_working_directory != claude_code_working_directory {
config.claude_code_working_directory = claude_code_working_directory;
changed = true;
}
let window_size = configured_window_size(config);
let normalized_width = window_size.map(|size| size.width);
let normalized_height = window_size.map(|size| size.height);
@@ -14420,6 +14740,14 @@ fn write_gui_config_to_path(config: &GuiConfigFile, config_path: &Path) -> Resul
"codex-session-repair-on-launch",
value(config.codex_session_repair_on_launch),
),
(
"claude-code-working-directory",
value(config.claude_code_working_directory.as_str()),
),
(
"claude-code-working-directory-prompt-disabled",
value(config.claude_code_working_directory_prompt_disabled),
),
] {
set_codex_table_item(root, key, item);
}
@@ -14505,6 +14833,13 @@ fn validate_gui_config(config: &GuiConfigFile) -> Result<(), String> {
{
return Err("会话粘性 TTL 不能包含控制字符".to_string());
}
if config
.claude_code_working_directory
.chars()
.any(char::is_control)
{
return Err("Claude Code working directory cannot contain control characters".to_string());
}
Ok(())
}
@@ -16706,6 +17041,7 @@ fn main() {
let initial_window_size = configured_window_size(&gui_config);
let app = tauri::Builder::default()
.plugin(tauri_plugin_dialog::init())
.plugin(
tauri_plugin_opener::Builder::new()
.open_js_links_on_click(false)
@@ -17254,6 +17590,7 @@ data: {"delta":{"type":"text_delta","text":"H"}}
"http://127.0.0.1:8317",
DEFAULT_API_KEY,
"gpt-test",
None,
)
.unwrap();
let mut claude_current: serde_json::Value = serde_json::from_str(&claude_managed).unwrap();
@@ -18159,6 +18496,7 @@ model:
"http://127.0.0.1:8317",
DEFAULT_API_KEY,
"model-a",
None,
)
.unwrap();
let claude = serde_json::from_str::<serde_json::Value>(&claude).unwrap();
@@ -18255,6 +18593,8 @@ model:
management_secret_key: "custom-secret".to_string(),
usage_statistics_enabled: false,
codex_session_repair_on_launch: true,
claude_code_working_directory: path_to_string(&home),
claude_code_working_directory_prompt_disabled: true,
..GuiConfigFile::default()
};
@@ -18268,6 +18608,8 @@ model:
assert!(content.contains("management-secret-key = \"custom-secret\""));
assert!(content.contains("usage-statistics-enabled = false"));
assert!(content.contains("codex-session-repair-on-launch = true"));
assert!(content.contains("claude-code-working-directory = "));
assert!(content.contains("claude-code-working-directory-prompt-disabled = true"));
fs::remove_dir_all(home).unwrap();
}
@@ -18395,22 +18737,94 @@ model:
#[test]
fn claude_agent_config_preserves_existing_fields() {
let rendered = build_claude_agent_config(
Some(r#"{"theme":"dark","env":{"KEEP":"yes"}}"#),
Some(r#"{"theme":"dark","env":{"KEEP":"yes","ANTHROPIC_API_KEY":"legacy-key"}}"#),
"http://127.0.0.1:8317",
DEFAULT_API_KEY,
"claude-test",
None,
)
.unwrap();
let value: serde_json::Value = serde_json::from_str(&rendered).unwrap();
assert_eq!(value["theme"], "dark");
assert_eq!(value["env"]["KEEP"], "yes");
assert!(value["env"].get("ANTHROPIC_API_KEY").is_none());
assert_eq!(value["env"]["ANTHROPIC_BASE_URL"], "http://127.0.0.1:8317");
assert_eq!(value["env"]["ANTHROPIC_AUTH_TOKEN"], DEFAULT_API_KEY);
assert_eq!(value["env"]["ANTHROPIC_MODEL"], "claude-test");
assert_eq!(value["model"], "claude-test");
}
#[test]
fn claude_code_role_mappings_drive_settings_and_launch_environment() {
let mappings = ClaudeDesktopModelMappings {
opus: "gpt-opus".to_string(),
sonnet: "gpt-sonnet".to_string(),
haiku: "gpt-haiku".to_string(),
};
let rendered = build_claude_agent_config(
None,
"http://127.0.0.1:8317",
"test-key",
"gpt-sonnet",
Some(&mappings),
)
.unwrap();
let value: serde_json::Value = serde_json::from_str(&rendered).unwrap();
assert_eq!(value["env"]["ANTHROPIC_MODEL"], "gpt-sonnet");
assert_eq!(value["env"]["ANTHROPIC_DEFAULT_OPUS_MODEL"], "gpt-opus");
assert_eq!(value["env"]["ANTHROPIC_DEFAULT_SONNET_MODEL"], "gpt-sonnet");
assert_eq!(value["env"]["ANTHROPIC_DEFAULT_HAIKU_MODEL"], "gpt-haiku");
let environment =
claude_code_launch_environment("http://127.0.0.1:8317", "test-key", &mappings);
let read = |key: &str| {
environment
.iter()
.find(|(candidate, _)| candidate == key)
.map(|(_, value)| value.as_str())
};
assert_eq!(read("ANTHROPIC_BASE_URL"), Some("http://127.0.0.1:8317"));
assert_eq!(read("ANTHROPIC_AUTH_TOKEN"), Some("test-key"));
assert_eq!(read("ANTHROPIC_API_KEY"), None);
assert_eq!(read("ANTHROPIC_DEFAULT_OPUS_MODEL"), Some("gpt-opus"));
assert_eq!(read("ANTHROPIC_DEFAULT_SONNET_MODEL"), Some("gpt-sonnet"));
assert_eq!(read("ANTHROPIC_DEFAULT_HAIKU_MODEL"), Some("gpt-haiku"));
}
#[test]
fn claude_code_launch_migrates_legacy_dual_auth_configuration() {
let directory = agent_test_home("claude-code-dual-auth-migration");
let path = directory.join("settings.json");
fs::write(
&path,
r#"{"env":{"ANTHROPIC_BASE_URL":"http://127.0.0.1:8317","ANTHROPIC_API_KEY":"legacy","ANTHROPIC_AUTH_TOKEN":"token"}}"#,
)
.unwrap();
assert!(remove_claude_code_conflicting_api_key(&path).unwrap());
let value: serde_json::Value =
serde_json::from_str(&fs::read_to_string(&path).unwrap()).unwrap();
assert!(value["env"].get("ANTHROPIC_API_KEY").is_none());
assert_eq!(value["env"]["ANTHROPIC_AUTH_TOKEN"], "token");
fs::remove_dir_all(directory).unwrap();
}
#[test]
fn claude_code_working_directory_must_be_absolute_and_exist() {
let directory = agent_test_home("claude-code-working-directory");
assert_eq!(
validate_claude_code_working_directory(&path_to_string(&directory)).unwrap(),
directory
);
assert!(validate_claude_code_working_directory("relative/path").is_err());
assert!(validate_claude_code_working_directory(&path_to_string(
&directory.join("missing")
))
.is_err());
fs::remove_dir_all(directory).unwrap();
}
#[test]
fn codex_agent_config_uses_managed_provider_without_losing_comments() {
let rendered = build_codex_agent_config(
@@ -18500,6 +18914,7 @@ model:
models: &models,
codex_catalog: Some(&catalog),
oauth_configuration,
claude_code_model_mappings: None,
claude_desktop_model_mappings: None,
},
)
@@ -20759,6 +21174,8 @@ custom_option = "keep-original"
routing_session_affinity: true,
routing_session_affinity_ttl: "1h".to_string(),
codex_session_repair_on_launch: false,
claude_code_working_directory: String::new(),
claude_code_working_directory_prompt_disabled: false,
};
let merged = merge_core_config_yaml(template, Some(current), &config).unwrap();
+2
View File
@@ -2206,6 +2206,8 @@ mod tests {
routing_session_affinity: false,
routing_session_affinity_ttl: String::new(),
codex_session_repair_on_launch: false,
claude_code_working_directory: String::new(),
claude_code_working_directory_prompt_disabled: false,
};
let record = normalize_usage_record(
serde_json::json!({
+20
View File
@@ -552,11 +552,31 @@ export const jaOverrides = {
'agents.modify.oauthConfiguration': 'OAuth 設定方式',
'agents.claudeDesktopMapping.title': 'Claude Desktop モデル → 上流モデル',
'agents.claudeDesktopMapping.description': '各 Claude Desktop モデルが実際に呼び出す上流モデルを選択します。',
'agents.claudeCodeMapping.title': 'Claude Code モデル → 上流モデル',
'agents.claudeCodeMapping.description': '各タスク役割に対して、起動時に設定する上流モデルを選択します。',
'agents.claudeCodeMapping.aliasOnly': 'エイリアスモデルのみ表示',
'agents.claudeCodeMapping.opus': 'Opus',
'agents.claudeCodeMapping.opusHint': '複雑な推論と高難度タスク',
'agents.claudeCodeMapping.sonnet': 'Sonnet',
'agents.claudeCodeMapping.sonnetHint': '既定のメインモデル',
'agents.claudeCodeMapping.haiku': 'Haiku',
'agents.claudeCodeMapping.haikuHint': '高速で軽量なタスク',
'agents.claudeDesktopMapping.customMapping': 'カスタムマッピング',
'agents.claudeDesktopMapping.customMappingHint': '有効にするとエイリアスモデルのみ、無効にするとエイリアス未設定の元モデルのみ表示します。',
'agents.claudeDesktopMapping.opus': 'Claude Opus 5 のマッピング先',
'agents.claudeDesktopMapping.sonnet': 'Claude Sonnet 4.6 のマッピング先',
'agents.claudeDesktopMapping.haiku': 'Claude Haiku 4.5 のマッピング先',
'agents.claudeCodeLaunch.noDirectory': '未選択',
'agents.claudeCodeLaunch.chooseDirectory': 'ディレクトリを選択',
'agents.claudeCodeLaunch.selectedDirectory': '選択した作業ディレクトリ',
'agents.claudeCodeLaunch.choosing': '選択中',
'agents.claudeCodeLaunch.dialogTitle': 'Claude Code の作業ディレクトリを選択',
'agents.claudeCodeLaunch.dialogDescription': '環境変数を設定して Claude Code を起動する前に、作業ディレクトリを選択してください。',
'agents.claudeCodeLaunch.workingDirectory': '作業ディレクトリ',
'agents.claudeCodeLaunch.browse': '参照',
'agents.claudeCodeLaunch.neverAskAgain': '今後この確認を表示しない',
'agents.claudeCodeLaunch.directoryRequired': '先に作業ディレクトリを選択してください',
'agents.claudeCodeLaunch.launch': 'Claude Code を起動',
'agents.oauthLoginRequired.title': 'Codex へのログインが必要です',
'agents.oauthLoginRequired.enableDescription': '有効な Codex のログイン認証情報が見つからないため、OAuth 設定方式を有効にできません。Codex アプリで ChatGPT アカウントにログインしてください。Codex アプリでログインを開始できない場合は、先に',
'agents.oauthLoginRequired.enableClearConfiguration': '設定を消去',
+40
View File
@@ -550,11 +550,31 @@ export const zhCN = {
'agents.pending': '尚未应用',
'agents.claudeDesktopMapping.title': 'Claude Desktop 模型 → 上游模型',
'agents.claudeDesktopMapping.description': '选择每个 Claude Desktop 模型实际调用的上游模型。',
'agents.claudeCodeMapping.title': 'Claude Code 模型 → 上游模型',
'agents.claudeCodeMapping.description': '为不同任务角色选择启动时注入的上游模型。',
'agents.claudeCodeMapping.aliasOnly': '仅显示别名模型',
'agents.claudeCodeMapping.opus': 'Opus',
'agents.claudeCodeMapping.opusHint': '复杂推理与高难度任务',
'agents.claudeCodeMapping.sonnet': 'Sonnet',
'agents.claudeCodeMapping.sonnetHint': '默认主模型',
'agents.claudeCodeMapping.haiku': 'Haiku',
'agents.claudeCodeMapping.haikuHint': '快速轻量任务',
'agents.claudeDesktopMapping.customMapping': '自定义映射',
'agents.claudeDesktopMapping.customMappingHint': '开启后只显示别名模型;关闭后只显示未设置别名的原始模型。',
'agents.claudeDesktopMapping.opus': 'Claude Opus 5 映射到',
'agents.claudeDesktopMapping.sonnet': 'Claude Sonnet 4.6 映射到',
'agents.claudeDesktopMapping.haiku': 'Claude Haiku 4.5 映射到',
'agents.claudeCodeLaunch.noDirectory': '尚未选择',
'agents.claudeCodeLaunch.chooseDirectory': '选择目录',
'agents.claudeCodeLaunch.selectedDirectory': '已选择的工作目录',
'agents.claudeCodeLaunch.choosing': '选择中',
'agents.claudeCodeLaunch.dialogTitle': '选择 Claude Code 工作目录',
'agents.claudeCodeLaunch.dialogDescription': '需要先选择工作目录,然后才能设置环境变量并启动 Claude Code。',
'agents.claudeCodeLaunch.workingDirectory': '工作目录',
'agents.claudeCodeLaunch.browse': '浏览',
'agents.claudeCodeLaunch.neverAskAgain': '永久关闭此提示',
'agents.claudeCodeLaunch.directoryRequired': '请先选择工作目录',
'agents.claudeCodeLaunch.launch': '启动 Claude Code',
'agents.model.readingAvailable': '正在读取可用模型',
'agents.model.current': '当前已应用:{model}',
'agents.model.firstSelection': '{count} 个可用模型,首次默认选择第一项',
@@ -1465,11 +1485,31 @@ export const en: Record<MessageKey, string> = {
'agents.pending': 'Not Applied',
'agents.claudeDesktopMapping.title': 'Claude Desktop Model → Upstream Model',
'agents.claudeDesktopMapping.description': 'Choose the upstream model each Claude Desktop model actually calls.',
'agents.claudeCodeMapping.title': 'Claude Code Model → Upstream Model',
'agents.claudeCodeMapping.description': 'Choose the upstream model injected at launch for each task role.',
'agents.claudeCodeMapping.aliasOnly': 'Show aliases only',
'agents.claudeCodeMapping.opus': 'Opus',
'agents.claudeCodeMapping.opusHint': 'Complex reasoning and difficult tasks',
'agents.claudeCodeMapping.sonnet': 'Sonnet',
'agents.claudeCodeMapping.sonnetHint': 'Default primary model',
'agents.claudeCodeMapping.haiku': 'Haiku',
'agents.claudeCodeMapping.haikuHint': 'Fast lightweight tasks',
'agents.claudeDesktopMapping.customMapping': 'Custom Mapping',
'agents.claudeDesktopMapping.customMappingHint': 'When enabled, only aliased models are shown; when disabled, only original models without aliases are shown.',
'agents.claudeDesktopMapping.opus': 'Map Claude Opus 5 to',
'agents.claudeDesktopMapping.sonnet': 'Map Claude Sonnet 4.6 to',
'agents.claudeDesktopMapping.haiku': 'Map Claude Haiku 4.5 to',
'agents.claudeCodeLaunch.noDirectory': 'Not selected',
'agents.claudeCodeLaunch.chooseDirectory': 'Choose Directory',
'agents.claudeCodeLaunch.selectedDirectory': 'Selected working directory',
'agents.claudeCodeLaunch.choosing': 'Choosing',
'agents.claudeCodeLaunch.dialogTitle': 'Choose a Claude Code Working Directory',
'agents.claudeCodeLaunch.dialogDescription': 'Select a working directory before setting the environment variables and starting Claude Code.',
'agents.claudeCodeLaunch.workingDirectory': 'Working directory',
'agents.claudeCodeLaunch.browse': 'Browse',
'agents.claudeCodeLaunch.neverAskAgain': 'Never show this prompt again',
'agents.claudeCodeLaunch.directoryRequired': 'Select a working directory first',
'agents.claudeCodeLaunch.launch': 'Start Claude Code',
'agents.model.readingAvailable': 'Loading available models',
'agents.model.current': 'Currently applied: {model}',
'agents.model.firstSelection': '{count} models available; the first is selected by default',
+322 -108
View File
@@ -10,6 +10,7 @@ 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,
@@ -70,7 +71,10 @@ type AgentConfigStatus = {
modificationState: AgentModificationState;
backupAvailable: boolean;
appliedModel: string | null;
claudeDesktopModelMappings: ClaudeDesktopModelMappings | null;
claudeCodeModelMappings: ClaudeModelMappings | null;
claudeDesktopModelMappings: ClaudeModelMappings | null;
claudeCodeWorkingDirectory: string | null;
claudeCodeWorkingDirectoryPromptDisabled: boolean;
warnings: string[];
error: string | null;
};
@@ -96,7 +100,7 @@ type ChatGptCloseResult = {
type OAuthLoginRequiredAction = 'enable' | 'apply' | 'launch';
type ClaudeDesktopModelMappings = {
type ClaudeModelMappings = {
opus: string;
sonnet: string;
haiku: string;
@@ -104,21 +108,36 @@ type ClaudeDesktopModelMappings = {
const CODEX_OAUTH_LOGIN_REQUIRED_ERROR = 'CODEX_OAUTH_LOGIN_REQUIRED';
const createClaudeDesktopModelMappings = (model: string): ClaudeDesktopModelMappings => ({
const createClaudeModelMappings = (model: string): ClaudeModelMappings => ({
opus: model,
sonnet: model,
haiku: model,
});
const sameClaudeDesktopModelMappings = (
left: ClaudeDesktopModelMappings,
right: ClaudeDesktopModelMappings,
const sameClaudeModelMappings = (
left: ClaudeModelMappings,
right: ClaudeModelMappings,
) => left.opus === right.opus && left.sonnet === right.sonnet && left.haiku === right.haiku;
const claudeDesktopMappingRoles = [
{ key: 'opus', labelKey: 'agents.claudeDesktopMapping.opus' },
{ key: 'sonnet', labelKey: 'agents.claudeDesktopMapping.sonnet' },
{ key: 'haiku', labelKey: 'agents.claudeDesktopMapping.haiku' },
const claudeMappingRoles = [
{
key: 'opus',
labelKey: 'agents.claudeDesktopMapping.opus',
codeLabelKey: 'agents.claudeCodeMapping.opus',
codeHintKey: 'agents.claudeCodeMapping.opusHint',
},
{
key: 'sonnet',
labelKey: 'agents.claudeDesktopMapping.sonnet',
codeLabelKey: 'agents.claudeCodeMapping.sonnet',
codeHintKey: 'agents.claudeCodeMapping.sonnetHint',
},
{
key: 'haiku',
labelKey: 'agents.claudeDesktopMapping.haiku',
codeLabelKey: 'agents.claudeCodeMapping.haiku',
codeHintKey: 'agents.claudeCodeMapping.haikuHint',
},
] as const;
type AgentDefinition = {
@@ -514,14 +533,18 @@ export function AgentsPage() {
const [modelByClient, setModelByClient] = useState<Partial<Record<AgentClientId, string>>>(
readAgentModelSelections,
);
const [claudeDesktopModelMappingsDraft, setClaudeDesktopModelMappingsDraft] = useState<ClaudeDesktopModelMappings>(
createClaudeDesktopModelMappings(''),
const [claudeModelMappingsDraft, setClaudeModelMappingsDraft] = useState<ClaudeModelMappings>(
createClaudeModelMappings(''),
);
const [claudeDesktopCustomMapping, setClaudeDesktopCustomMapping] = useState(false);
const [claudeCustomMapping, setClaudeCustomMapping] = useState(false);
const [claudeCodeLaunchDialogOpen, setClaudeCodeLaunchDialogOpen] = useState(false);
const [claudeCodeLaunchDirectory, setClaudeCodeLaunchDirectory] = useState('');
const [claudeCodeSuppressDirectoryPrompt, setClaudeCodeSuppressDirectoryPrompt] = useState(false);
const [claudeCodeDirectoryError, setClaudeCodeDirectoryError] = useState('');
const [loading, setLoading] = useState(true);
const [modelLoading, setModelLoading] = useState(false);
const [busyAction, setBusyAction] = useState<
'apply' | 'close-config' | 'default' | 'clear' | 'launch' | 'launch-cli' | 'launch-app' | 'close-app' | 'oauth-check' | null
'apply' | 'close-config' | 'default' | 'clear' | 'launch' | 'launch-cli' | 'launch-app' | 'close-app' | 'oauth-check' | 'directory' | null
>(null);
const busy = busyAction !== null;
const [detectionError, setDetectionError] = useState('');
@@ -540,7 +563,7 @@ export function AgentsPage() {
const [oauthLoginRequiredAction, setOauthLoginRequiredAction] = useState<OAuthLoginRequiredAction | null>(null);
const [oauthConfigurationDraft, setOauthConfigurationDraft] = useState<boolean | null>(null);
const modelRequestRef = useRef(0);
const claudeDesktopModelMappingsDirtyRef = useRef(false);
const claudeModelMappingsDirtyRef = useRef(false);
const loadStatuses = useCallback(async (forceRefresh = false) => {
const command = forceRefresh
@@ -637,6 +660,9 @@ export function AgentsPage() {
setCloseAppNotice('');
setCloseAppConfirmOpen(false);
setOauthLoginRequiredAction(null);
setClaudeCodeLaunchDialogOpen(false);
setClaudeCodeDirectoryError('');
claudeModelMappingsDirtyRef.current = false;
}, [selected]);
const activeDefinition = agentDefinitions.find((agent) => agent.id === selected)
@@ -648,66 +674,78 @@ export function AgentsPage() {
const savedSelectedModel = modelByClient[selected] ?? '';
const selectedModelOption = findAgentModel(models, savedSelectedModel);
const selectedModel = selectedModelOption?.name ?? '';
const claudeDesktopModels = useMemo(
() => filterAgentModelsByAlias(models, claudeDesktopCustomMapping),
[claudeDesktopCustomMapping, models],
const isClaudeModelMappingClient = selected === 'claude-code' || selected === 'claude-desktop';
const claudeMappingModels = useMemo(
() => filterAgentModelsByAlias(models, claudeCustomMapping),
[claudeCustomMapping, models],
);
useEffect(() => {
if (selected !== 'claude-desktop' || !selectedModel) return;
const appliedMappings = activeStatus?.claudeDesktopModelMappings;
if (!claudeDesktopModelMappingsDirtyRef.current && appliedMappings) {
const appliedModels = claudeDesktopMappingRoles
if (!isClaudeModelMappingClient || !selectedModel) return;
const appliedMappings = selected === 'claude-code'
? activeStatus?.claudeCodeModelMappings
: activeStatus?.claudeDesktopModelMappings;
if (!claudeModelMappingsDirtyRef.current && appliedMappings) {
const appliedModels = claudeMappingRoles
.map((role) => findAgentModel(models, appliedMappings[role.key]))
.filter((model): model is ModelOption => model !== null);
if (appliedModels.length === claudeDesktopMappingRoles.length) {
setClaudeDesktopCustomMapping(appliedModels.every((model) => Boolean(model.isAlias)));
if (appliedModels.length === claudeMappingRoles.length) {
setClaudeCustomMapping(appliedModels.every((model) => Boolean(model.isAlias)));
}
}
setClaudeDesktopModelMappingsDraft((current) => {
const source = claudeDesktopModelMappingsDirtyRef.current
setClaudeModelMappingsDraft((current) => {
const source = claudeModelMappingsDirtyRef.current
? current
: appliedMappings ?? current;
const next: ClaudeDesktopModelMappings = {
const next: ClaudeModelMappings = {
opus: findAgentModel(models, source.opus)?.name ?? selectedModel,
sonnet: findAgentModel(models, source.sonnet)?.name ?? selectedModel,
haiku: findAgentModel(models, source.haiku)?.name ?? selectedModel,
};
return sameClaudeDesktopModelMappings(current, next) ? current : next;
return sameClaudeModelMappings(current, next) ? current : next;
});
}, [activeStatus?.claudeDesktopModelMappings, models, selected, selectedModel]);
}, [
activeStatus?.claudeCodeModelMappings,
activeStatus?.claudeDesktopModelMappings,
isClaudeModelMappingClient,
models,
selected,
selectedModel,
]);
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 appliedModel = activeStatus?.appliedModel ?? activeStatus?.currentModel ?? '';
const modelDraftChanged = selected !== 'claude-desktop' && Boolean(
const modelDraftChanged = !isClaudeModelMappingClient && Boolean(
selectedModel.trim()
&& appliedModel.trim()
&& selectedModel.trim() !== appliedModel.trim(),
);
const appliedClaudeDesktopModelMappings = activeStatus?.claudeDesktopModelMappings
?? createClaudeDesktopModelMappings(appliedModel);
const claudeDesktopMappingsReady = selected !== 'claude-desktop'
|| claudeDesktopMappingRoles.every((role) =>
Boolean(findAgentModel(models, claudeDesktopModelMappingsDraft[role.key])),
const appliedClaudeModelMappings = (selected === 'claude-code'
? activeStatus?.claudeCodeModelMappings
: activeStatus?.claudeDesktopModelMappings)
?? createClaudeModelMappings(appliedModel);
const claudeMappingsReady = !isClaudeModelMappingClient
|| claudeMappingRoles.every((role) =>
Boolean(findAgentModel(models, claudeModelMappingsDraft[role.key])),
);
const claudeDesktopMappingDraftChanged = selected === 'claude-desktop'
const claudeMappingDraftChanged = isClaudeModelMappingClient
&& activeStatus?.modificationState === 'applied'
&& !sameClaudeDesktopModelMappings(
claudeDesktopModelMappingsDraft,
appliedClaudeDesktopModelMappings,
&& !sameClaudeModelMappings(
claudeModelMappingsDraft,
appliedClaudeModelMappings,
);
const oauthConfigurationChanged = selected === 'codex'
&& oauthConfiguration !== Boolean(activeStatus?.oauthConfiguration);
const draftChanged = modelDraftChanged || claudeDesktopMappingDraftChanged || oauthConfigurationChanged;
const draftChanged = modelDraftChanged || claudeMappingDraftChanged || oauthConfigurationChanged;
const canEnable = Boolean(
activeStatus?.supportedPlatform
&& activeStatus.installed
&& !modelLoading
&& (selected === 'claude-desktop'
? claudeDesktopMappingsReady
&& (isClaudeModelMappingClient
? claudeMappingsReady
: selectedModelOption),
);
const launchEnabled = Boolean(
@@ -753,28 +791,28 @@ export function AgentsPage() {
});
};
const selectClaudeDesktopModelMapping = (
role: keyof ClaudeDesktopModelMappings,
const selectClaudeModelMapping = (
role: keyof ClaudeModelMappings,
value: string,
) => {
const model = findAgentModel(models, value);
if (!model) return;
claudeDesktopModelMappingsDirtyRef.current = true;
claudeModelMappingsDirtyRef.current = true;
setModelSelectionError('');
setClaudeDesktopModelMappingsDraft((current) => ({ ...current, [role]: model.name }));
setClaudeModelMappingsDraft((current) => ({ ...current, [role]: model.name }));
};
const changeClaudeDesktopCustomMapping = (enabled: boolean) => {
setClaudeDesktopCustomMapping(enabled);
const changeClaudeCustomMapping = (enabled: boolean) => {
setClaudeCustomMapping(enabled);
setModelSelectionError('');
setClaudeDesktopModelMappingsDraft((current) => {
const next: ClaudeDesktopModelMappings = {
setClaudeModelMappingsDraft((current) => {
const next: ClaudeModelMappings = {
opus: resolveAgentModelForAliasMode(models, current.opus, enabled),
sonnet: resolveAgentModelForAliasMode(models, current.sonnet, enabled),
haiku: resolveAgentModelForAliasMode(models, current.haiku, enabled),
};
if (!sameClaudeDesktopModelMappings(current, next)) {
claudeDesktopModelMappingsDirtyRef.current = true;
if (!sameClaudeModelMappings(current, next)) {
claudeModelMappingsDirtyRef.current = true;
}
return next;
});
@@ -798,11 +836,11 @@ export function AgentsPage() {
return model.name;
};
const requireClaudeDesktopModelMappings = (): ClaudeDesktopModelMappings | null => {
if (selected !== 'claude-desktop') return null;
const resolved = {} as ClaudeDesktopModelMappings;
for (const role of claudeDesktopMappingRoles) {
const model = findAgentModel(models, claudeDesktopModelMappingsDraft[role.key]);
const requireClaudeModelMappings = (): ClaudeModelMappings | null => {
if (!isClaudeModelMappingClient) return null;
const resolved = {} as ClaudeModelMappings;
for (const role of claudeMappingRoles) {
const model = findAgentModel(models, claudeModelMappingsDraft[role.key]);
if (!model) {
setModelSelectionError(t('agents.error.mappingSelectionGone'));
return null;
@@ -842,10 +880,10 @@ export function AgentsPage() {
const applyConfigurationChanges = async () => {
setConfigurationError('');
const claudeDesktopModelMappings = requireClaudeDesktopModelMappings();
if (selected === 'claude-desktop' && !claudeDesktopModelMappings) return;
const model = selected === 'claude-desktop'
? claudeDesktopModelMappings?.sonnet ?? null
const claudeModelMappings = requireClaudeModelMappings();
if (isClaudeModelMappingClient && !claudeModelMappings) return;
const model = isClaudeModelMappingClient
? claudeModelMappings?.sonnet ?? null
: requireSelectedModel();
if (!model) return;
setBusyAction('apply');
@@ -854,11 +892,12 @@ export function AgentsPage() {
client: selected,
model,
oauthConfiguration,
claudeDesktopModelMappings,
claudeCodeModelMappings: selected === 'claude-code' ? claudeModelMappings : null,
claudeDesktopModelMappings: selected === 'claude-desktop' ? claudeModelMappings : null,
});
await reloadStatusesAfterAction();
setOauthConfigurationDraft(null);
claudeDesktopModelMappingsDirtyRef.current = false;
claudeModelMappingsDirtyRef.current = false;
} catch (requestError) {
if (!handleOAuthLoginError(requestError, 'apply')) {
setConfigurationError(String(requestError));
@@ -883,10 +922,10 @@ export function AgentsPage() {
};
const resetConfigurationToDefault = async () => {
const claudeDesktopModelMappings = requireClaudeDesktopModelMappings();
if (selected === 'claude-desktop' && !claudeDesktopModelMappings) return;
const model = selected === 'claude-desktop'
? claudeDesktopModelMappings?.sonnet ?? null
const claudeModelMappings = requireClaudeModelMappings();
if (isClaudeModelMappingClient && !claudeModelMappings) return;
const model = isClaudeModelMappingClient
? claudeModelMappings?.sonnet ?? null
: requireSelectedModel();
if (!model) return;
setBusyAction('default');
@@ -896,12 +935,13 @@ export function AgentsPage() {
client: selected,
model,
oauthConfiguration,
claudeDesktopModelMappings,
claudeCodeModelMappings: selected === 'claude-code' ? claudeModelMappings : null,
claudeDesktopModelMappings: selected === 'claude-desktop' ? claudeModelMappings : null,
});
setDefaultConfirmOpen(false);
await reloadStatusesAfterAction();
setOauthConfigurationDraft(null);
claudeDesktopModelMappingsDirtyRef.current = false;
claudeModelMappingsDirtyRef.current = false;
} catch (requestError) {
if (!handleOAuthLoginError(requestError, 'apply')) {
setDefaultError(String(requestError));
@@ -928,7 +968,38 @@ export function AgentsPage() {
}
};
const launchAgent = async (target: AgentLaunchTarget | null) => {
const openClaudeCodeLaunchDialog = () => {
setClaudeCodeLaunchDirectory(activeStatus?.claudeCodeWorkingDirectory ?? '');
setClaudeCodeSuppressDirectoryPrompt(false);
setClaudeCodeDirectoryError('');
setClaudeCodeLaunchDialogOpen(true);
};
const chooseClaudeCodeWorkingDirectory = async () => {
setBusyAction('directory');
setClaudeCodeDirectoryError('');
try {
const selectedDirectory = await open({
directory: true,
multiple: false,
defaultPath: claudeCodeLaunchDirectory || activeStatus?.claudeCodeWorkingDirectory || undefined,
title: t('agents.claudeCodeLaunch.dialogTitle'),
});
if (typeof selectedDirectory === 'string') {
setClaudeCodeLaunchDirectory(selectedDirectory);
}
} catch (requestError) {
setClaudeCodeDirectoryError(String(requestError));
} finally {
setBusyAction(null);
}
};
const invokeAgentLaunch = async (
target: AgentLaunchTarget,
workingDirectory: string | null = null,
suppressWorkingDirectoryPrompt: boolean | null = null,
) => {
if (!target) return;
const launchAction = selected === 'codex'
? target.id === 'cli' ? 'launch-cli' : 'launch-app'
@@ -939,14 +1010,62 @@ export function AgentsPage() {
if (draftChanged) {
throw new Error(t('agents.error.applyFirst'));
}
await invoke('launch_agent', { client: selected, target: target.id });
await invoke('launch_agent', {
client: selected,
target: target.id,
workingDirectory,
suppressWorkingDirectoryPrompt,
});
if (selected === 'claude-code' && workingDirectory) {
setClaudeCodeLaunchDialogOpen(false);
await reloadStatusesAfterAction();
}
} catch (requestError) {
if (!handleOAuthLoginError(requestError, 'launch')) setLaunchError(String(requestError));
const message = String(requestError);
if (
selected === 'claude-code'
&& (message.includes('CLAUDE_CODE_WORKING_DIRECTORY_REQUIRED')
|| message.includes('CLAUDE_CODE_WORKING_DIRECTORY_INVALID'))
) {
openClaudeCodeLaunchDialog();
setClaudeCodeDirectoryError(
message.includes('CLAUDE_CODE_WORKING_DIRECTORY_INVALID') ? message : '',
);
} else if (selected === 'claude-code' && workingDirectory) {
setClaudeCodeDirectoryError(message);
} else if (!handleOAuthLoginError(requestError, 'launch')) {
setLaunchError(message);
}
} finally {
setBusyAction(null);
}
};
const launchAgent = async (target: AgentLaunchTarget | null) => {
if (!target) return;
if (selected === 'claude-code' && !activeStatus?.claudeCodeWorkingDirectoryPromptDisabled) {
openClaudeCodeLaunchDialog();
return;
}
await invokeAgentLaunch(target);
};
const launchClaudeCodeFromDialog = async () => {
const target = defaultLaunchTarget;
if (!target) return;
const workingDirectory = claudeCodeLaunchDirectory.trim();
if (!workingDirectory) {
setClaudeCodeDirectoryError(t('agents.claudeCodeLaunch.directoryRequired'));
return;
}
setClaudeCodeDirectoryError('');
await invokeAgentLaunch(
target,
workingDirectory,
claudeCodeSuppressDirectoryPrompt,
);
};
const closeChatGptApp = async () => {
setBusyAction('close-app');
setCloseAppError('');
@@ -1058,30 +1177,33 @@ export function AgentsPage() {
</aside>
<section className="panel agent-config-panel">
<div className="agent-subpage-tabs" role="tablist" aria-label={t('agents.tabs.label')}>
{availableSubpages.map((subpage) => (
<button
type="button"
id={`agent-subpage-tab-${subpage.id}`}
role="tab"
className={activeSubpage === subpage.id ? 'active' : ''}
aria-selected={activeSubpage === subpage.id}
aria-controls={`agent-subpage-panel-${subpage.id}`}
tabIndex={activeSubpage === subpage.id ? 0 : -1}
key={subpage.id}
onClick={() => setActiveSubpage(subpage.id)}
>
{t(subpage.labelKey)}
</button>
))}
</div>
{selected !== 'claude-code' ? (
<div className="agent-subpage-tabs" role="tablist" aria-label={t('agents.tabs.label')}>
{availableSubpages.map((subpage) => (
<button
type="button"
id={`agent-subpage-tab-${subpage.id}`}
role="tab"
className={activeSubpage === subpage.id ? 'active' : ''}
aria-selected={activeSubpage === subpage.id}
aria-controls={`agent-subpage-panel-${subpage.id}`}
tabIndex={activeSubpage === subpage.id ? 0 : -1}
key={subpage.id}
onClick={() => setActiveSubpage(subpage.id)}
>
{t(subpage.labelKey)}
</button>
))}
</div>
) : null}
{activeSubpage === 'core' ? (
<div
className="agent-core-config"
className={`agent-core-config ${selected === 'claude-code' ? 'agent-claude-code-config' : ''}`}
id="agent-subpage-panel-core"
role="tabpanel"
aria-labelledby="agent-subpage-tab-core"
aria-labelledby={selected === 'claude-code' ? undefined : 'agent-subpage-tab-core'}
aria-label={selected === 'claude-code' ? t('agents.claudeCodeMapping.title') : undefined}
>
<div className="agent-status-grid">
<div>
@@ -1104,7 +1226,7 @@ export function AgentsPage() {
</div>
) : null}
{selected !== 'claude-desktop' ? (
{!isClaudeModelMappingClient ? (
<section className="agent-core-setting-section agent-model-section">
<div className="agent-section-heading">
<div><strong>{t('agents.useModel')}</strong></div>
@@ -1131,43 +1253,52 @@ export function AgentsPage() {
</section>
) : null}
{selected === 'claude-desktop' ? (
<section className="agent-core-setting-section agent-claude-desktop-mapping">
{isClaudeModelMappingClient ? (
<section className={`agent-core-setting-section agent-claude-desktop-mapping ${selected === 'claude-code' ? 'agent-claude-code-mapping' : ''}`}>
<div className="agent-section-heading">
<div>
<strong>{t('agents.claudeDesktopMapping.title')}</strong>
<span>{t('agents.claudeDesktopMapping.description')}</span>
<strong>{t(selected === 'claude-code'
? 'agents.claudeCodeMapping.title'
: 'agents.claudeDesktopMapping.title')}</strong>
<span>{t(selected === 'claude-code'
? 'agents.claudeCodeMapping.description'
: 'agents.claudeDesktopMapping.description')}</span>
</div>
<div className="agent-section-heading-actions">
<label
className="agent-claude-desktop-mapping-filter"
title={t('agents.claudeDesktopMapping.customMappingHint')}
>
<span>{t('agents.claudeDesktopMapping.customMapping')}</span>
<span>{t(selected === 'claude-code'
? 'agents.claudeCodeMapping.aliasOnly'
: 'agents.claudeDesktopMapping.customMapping')}</span>
<span className="switch-control">
<input
type="checkbox"
checked={claudeDesktopCustomMapping}
onChange={(event) => changeClaudeDesktopCustomMapping(event.currentTarget.checked)}
checked={claudeCustomMapping}
onChange={(event) => changeClaudeCustomMapping(event.currentTarget.checked)}
disabled={busy || modelLoading}
/>
<span className="switch-track" />
</span>
</label>
{claudeDesktopMappingDraftChanged ? <span className="agent-pending-badge">{t('agents.pending')}</span> : null}
{claudeMappingDraftChanged ? <span className="agent-pending-badge">{t('agents.pending')}</span> : null}
</div>
</div>
<div className="agent-claude-desktop-mapping-grid">
{claudeDesktopMappingRoles.map((role) => (
<div className="agent-claude-desktop-mapping-row" key={role.key}>
<strong>{t(role.labelKey)}</strong>
{claudeMappingRoles.map((role) => (
<div className={`agent-claude-desktop-mapping-row ${role.key}`} key={role.key}>
<div className="agent-claude-mapping-role">
<strong>{t(selected === 'claude-code' ? role.codeLabelKey : role.labelKey)}</strong>
{selected === 'claude-code' ? <span>{t(role.codeHintKey)}</span> : null}
</div>
<AgentModelPicker
models={claudeDesktopModels}
value={claudeDesktopModelMappingsDraft[role.key]}
models={claudeMappingModels}
value={claudeModelMappingsDraft[role.key]}
loading={modelLoading}
error={modelError}
disabled={busy || !activeStatus?.installed || !activeStatus.supportedPlatform}
onChange={(value) => selectClaudeDesktopModelMapping(role.key, value)}
onChange={(value) => selectClaudeModelMapping(role.key, value)}
onRefresh={refreshModels}
/>
</div>
@@ -1231,7 +1362,9 @@ export function AgentsPage() {
onClick={openDefaultConfirmation}
disabled={busy || !canEnable}
>
{busyAction === 'default' ? <LoaderCircle size={16} className="spin" /> : <RefreshCw size={16} />}
{busyAction === 'default'
? <LoaderCircle size={16} className="spin" />
: selected === 'claude-code' ? null : <RefreshCw size={16} />}
{t('agents.modify.default')}
</button>
{selected === 'codex' ? (
@@ -1301,7 +1434,9 @@ export function AgentsPage() {
? defaultLaunchTarget?.detail
: t('agents.launch.enableFirst')}
>
{busyAction === 'launch' ? <LoaderCircle size={16} className="spin" /> : <Play size={16} />}
{busyAction === 'launch'
? <LoaderCircle size={16} className="spin" />
: selected === 'claude-code' ? null : <Play size={16} />}
{busyAction === 'launch' ? t('agents.launch.starting') : defaultLaunchTarget ? t('agents.launch.start', { target: defaultLaunchTarget.label }) : t('agents.launch.unavailable')}
</button>
)}
@@ -1345,6 +1480,85 @@ export function AgentsPage() {
</section>
</div>
{claudeCodeLaunchDialogOpen ? (
<div className="config-dialog-backdrop" onMouseDown={(event) => {
if (event.currentTarget === event.target && !busy) {
setClaudeCodeLaunchDialogOpen(false);
}
}}>
<section
className="config-dialog agent-claude-code-launch-dialog"
role="dialog"
aria-modal="true"
aria-labelledby="agent-claude-code-launch-title"
>
<div className="config-dialog-heading">
<div>
<h2 id="agent-claude-code-launch-title">
{t('agents.claudeCodeLaunch.dialogTitle')}
</h2>
</div>
</div>
<p>{t('agents.claudeCodeLaunch.dialogDescription')}</p>
<div className="config-dialog-field">
<span>{t('agents.claudeCodeLaunch.workingDirectory')}</span>
<button
type="button"
className="agent-claude-code-directory-picker"
onClick={() => void chooseClaudeCodeWorkingDirectory()}
disabled={busy}
autoFocus
>
<span>
<small>{claudeCodeLaunchDirectory
? t('agents.claudeCodeLaunch.selectedDirectory')
: t('agents.claudeCodeLaunch.noDirectory')}</small>
<strong title={claudeCodeLaunchDirectory || undefined}>
{claudeCodeLaunchDirectory || t('agents.claudeCodeLaunch.chooseDirectory')}
</strong>
</span>
<b>{busyAction === 'directory'
? t('agents.claudeCodeLaunch.choosing')
: t('agents.claudeCodeLaunch.browse')}</b>
</button>
</div>
<label className="agent-claude-code-suppress-prompt">
<input
type="checkbox"
checked={claudeCodeSuppressDirectoryPrompt}
onChange={(event) => setClaudeCodeSuppressDirectoryPrompt(event.currentTarget.checked)}
disabled={busy}
/>
<span>{t('agents.claudeCodeLaunch.neverAskAgain')}</span>
</label>
{claudeCodeDirectoryError ? (
<span className="agent-inline-message error" role="alert" aria-live="polite">
{claudeCodeDirectoryError}
</span>
) : null}
<div className="config-dialog-actions two-actions">
<button
type="button"
className="secondary-button"
onClick={() => setClaudeCodeLaunchDialogOpen(false)}
disabled={busy}
>
{t('common.cancel')}
</button>
<button
type="button"
className="primary-button"
onClick={() => void launchClaudeCodeFromDialog()}
disabled={busy || !claudeCodeLaunchDirectory.trim()}
>
{busyAction === 'launch' ? <LoaderCircle size={16} className="spin" /> : null}
{t('agents.claudeCodeLaunch.launch')}
</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">
+231
View File
@@ -5310,6 +5310,127 @@ textarea:focus-visible {
font-size: var(--font-size-label);
}
.agent-claude-mapping-role {
display: grid;
min-width: 0;
gap: 3px;
}
.agent-claude-mapping-role > strong {
color: var(--theme-5f5a53);
font-size: var(--font-size-label);
}
.agent-claude-mapping-role > span {
overflow: hidden;
color: var(--theme-8a847c);
font-size: var(--font-size-meta);
text-overflow: ellipsis;
white-space: nowrap;
}
.agent-claude-code-config {
gap: 12px;
}
.agent-claude-code-config .agent-status-grid {
min-height: 0;
gap: 0;
padding: 10px 6px;
border: 1px solid var(--theme-e3e1db);
border-radius: 8px;
background: var(--theme-fffdf8);
}
.agent-claude-code-config .agent-status-grid > div {
gap: 3px;
padding: 2px 12px;
border: 0;
border-radius: 0;
background: transparent;
}
.agent-claude-code-config .agent-status-grid > div + div {
border-left: 1px solid var(--theme-e3e1db);
}
.agent-claude-code-mapping {
gap: 14px;
padding: 18px;
border-color: var(--theme-d8d2c8);
background: var(--theme-fffdf8);
}
.agent-claude-code-mapping > .agent-section-heading {
padding-bottom: 13px;
border-bottom: 1px solid var(--theme-e3e1db);
}
.agent-claude-code-mapping .agent-claude-desktop-mapping-grid {
gap: 12px;
}
.agent-claude-code-mapping .agent-claude-desktop-mapping-row {
position: relative;
gap: 10px;
padding: 13px;
overflow: hidden;
border: 1px solid var(--theme-e3e1db);
border-radius: 8px;
background: var(--theme-fff);
}
.agent-claude-code-mapping .agent-claude-desktop-mapping-row::before {
position: absolute;
top: 0;
right: 0;
left: 0;
height: 3px;
background: var(--theme-bdb5a9);
content: '';
}
.agent-claude-code-mapping .agent-claude-desktop-mapping-row.sonnet {
border-color: var(--theme-d8d1eb);
background: var(--theme-f8f5ff);
}
.agent-claude-code-mapping .agent-claude-desktop-mapping-row.sonnet::before {
background: var(--theme-8069ad);
}
.agent-claude-code-mapping .agent-claude-desktop-mapping-row.opus::before {
background: var(--theme-bf8a43);
}
.agent-claude-code-mapping .agent-claude-desktop-mapping-row.haiku::before {
background: var(--theme-4f825c);
}
.agent-claude-code-mapping .agent-claude-desktop-mapping-filter {
padding: 7px 9px;
border-radius: 8px;
}
.agent-claude-code-config .agent-modification-actions {
grid-column: 1 / -1;
background: var(--theme-fffdf8);
}
.agent-claude-code-config .agent-modification-control > .agent-inline-message {
width: 100%;
}
.agent-claude-code-config .agent-config-footer {
padding-top: 0;
border-top: 0;
}
.agent-claude-code-config .agent-launch-button {
min-width: 190px;
min-height: 42px;
}
.agent-section-heading-actions {
display: flex !important;
flex: none;
@@ -5343,6 +5464,81 @@ textarea:focus-visible {
opacity: 0.62;
}
.agent-claude-code-launch-dialog {
grid-template-rows: auto;
width: min(620px, calc(100vw - 32px));
height: auto;
min-height: 0;
}
.agent-claude-code-launch-dialog > p {
margin: 0;
color: var(--theme-5f5a53);
font-size: var(--font-size-label);
line-height: var(--line-height-relaxed);
}
.agent-claude-code-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;
transition: border-color 160ms ease, background-color 160ms ease, box-shadow 160ms ease;
}
.agent-claude-code-directory-picker:hover:not(:disabled) {
border-color: var(--theme-bdb5a9);
background: var(--theme-faf8f4);
}
.agent-claude-code-directory-picker:focus-visible {
border-color: var(--theme-8d8377);
outline: 0;
box-shadow: 0 0 0 3px rgba(45, 42, 38, 0.1);
}
.agent-claude-code-directory-picker > span {
display: grid;
min-width: 0;
gap: 4px;
}
.agent-claude-code-directory-picker small {
color: var(--theme-8a847c);
font-size: var(--font-size-meta);
}
.agent-claude-code-directory-picker strong {
overflow: hidden;
font-size: var(--font-size-label);
text-overflow: ellipsis;
white-space: nowrap;
}
.agent-claude-code-directory-picker > b {
flex: none;
color: var(--theme-8069ad);
font-size: var(--font-size-label);
}
.agent-claude-code-suppress-prompt {
display: flex;
align-items: center;
gap: 8px;
color: var(--theme-5f5a53);
font-size: var(--font-size-label);
cursor: pointer;
}
.agent-section-heading {
display: flex;
align-items: center;
@@ -7097,6 +7293,28 @@ textarea:focus-visible {
grid-column: 2;
height: 100%;
}
.agent-claude-code-config .agent-modification-actions {
grid-column: 1 / -1;
grid-template-columns: minmax(180px, 0.7fr) minmax(0, 1.3fr);
align-items: center;
height: auto;
}
.agent-claude-code-config .agent-modification-control {
justify-items: end;
}
.agent-claude-code-config .agent-modification-buttons {
display: flex;
width: auto;
justify-content: flex-end;
}
.agent-claude-code-config .agent-modification-buttons button {
width: auto;
min-width: 152px;
}
}
@container agent-panel (max-width: 760px) {
@@ -7127,6 +7345,19 @@ textarea:focus-visible {
}
@container agent-panel (max-width: 640px) {
.agent-claude-code-config .agent-status-grid {
padding: 6px 12px;
}
.agent-claude-code-config .agent-status-grid > div {
padding: 8px 0;
}
.agent-claude-code-config .agent-status-grid > div + div {
border-top: 1px solid var(--theme-e3e1db);
border-left: 0;
}
.agent-claude-desktop-mapping > .agent-section-heading {
align-items: stretch;
flex-direction: column;