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:
1 parent
efc8811d0c
commit
faa7e97326
11 files changed
+1208
-136
No files matched your search
@@ -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=="],
|
||||
|
||||
@@ -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"
|
||||
|
||||
Generated
+143
-1
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
@@ -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();
|
||||
|
||||
|
||||
@@ -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!({
|
||||
|
||||
@@ -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': '設定を消去',
|
||||
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user