feat: improve Claude agent mapping and context handling

This commit is contained in:
lzt404 committed 2026-08-02 17:49:43 +08:00
1 parent faa7e97326
commit a7e9cc356d
5 files changed
+672 -256

No files matched your search

+633 -40
View File
@@ -88,7 +88,14 @@ const CLAUDE_DESKTOP_PROFILE_ID: &str = "00000000-0000-4000-8000-000000831700";
const CLAUDE_DESKTOP_OPUS_MODEL_ID: &str = "claude-opus-5";
const CLAUDE_DESKTOP_SONNET_MODEL_ID: &str = "claude-sonnet-4-6";
const CLAUDE_DESKTOP_HAIKU_MODEL_ID: &str = "claude-haiku-4-5";
const MANAGED_CLAUDE_OPUS_ALIAS_DISPLAY_NAME: &str = "EasyCLIProxyAPI managed Claude Opus mapping";
const MANAGED_CLAUDE_SONNET_ALIAS_DISPLAY_NAME: &str =
"EasyCLIProxyAPI managed Claude Sonnet mapping";
const MANAGED_CLAUDE_HAIKU_ALIAS_DISPLAY_NAME: &str =
"EasyCLIProxyAPI managed Claude Haiku mapping";
const DEFAULT_CLAUDE_CONTEXT_WINDOW: u64 = 200_000;
const CLAUDE_DESKTOP_EXTENDED_CONTEXT_WINDOW: u64 = 1_000_000;
const CLAUDE_CODE_MAX_CONTEXT_TOKENS_ENV: &str = "CLAUDE_CODE_MAX_CONTEXT_TOKENS";
const MODEL_ALIAS_CONFIG_SECTIONS: &[&str] = &[
"codex-api-key",
"openai-compatibility",
@@ -2143,8 +2150,13 @@ async fn fetch_prepared_agent_models(
let runtime_models = fetch_codex_runtime_models(config.port, api_key).await?;
prepare_codex_agent_models(&runtime_models)
} else {
let mut models = fetch_agent_models(config.port, api_key).await?;
if matches!(client, AgentClient::ClaudeCode | AgentClient::ClaudeDesktop) {
let content = fetch_management_config_yaml(config).await?;
mark_configured_agent_model_aliases(&mut models, &content)?;
}
Ok(PreparedAgentModels {
models: fetch_agent_models(config.port, api_key).await?,
models,
codex_catalog: None,
})
}
@@ -2581,10 +2593,19 @@ async fn launch_agent(
.map(ClaudeDesktopModelMappings::all)
})
.ok_or_else(|| "Claude Code model environment is not configured".to_string())?;
let prepared = fetch_prepared_agent_models(client, &config).await?;
let max_context_tokens = claude_code_max_context_tokens(&mappings, &prepared.models)?;
{
let _guard = AGENT_CONFIG_FILE_LOCK
.lock()
.map_err(|_| "Claude Code configuration lock is poisoned".to_string())?;
update_claude_code_context_window(&claude_config_path, max_context_tokens)?;
}
let environment = claude_code_launch_environment(
&format!("http://127.0.0.1:{port}"),
effective_agent_api_key(&config),
&mappings,
max_context_tokens,
);
return launch_cli_agent(
&executable,
@@ -2719,6 +2740,10 @@ fn parse_agent_model_options(payload: &serde_json::Value) -> Result<Vec<AgentMod
"ContextLength",
"context_window",
"contextWindow",
"max_input_tokens",
"maxInputTokens",
"input_token_limit",
"inputTokenLimit",
]
.into_iter()
.find_map(|key| item.get(key).and_then(json_positive_u64));
@@ -2763,6 +2788,73 @@ fn append_agent_model_option(
});
}
fn mark_configured_agent_model_aliases(
models: &mut [AgentModelOption],
content: &str,
) -> Result<(), String> {
let document = serde_norway::from_str::<serde_norway::Value>(content)
.map_err(|error| format!("解析内核 YAML 配置失败: {error}"))?;
let root = document
.as_mapping()
.ok_or_else(|| "内核配置顶层必须是 YAML 映射".to_string())?;
for section in MODEL_ALIAS_CONFIG_SECTIONS {
let Some(providers) = yaml_mapping_value(root, section) else {
continue;
};
let providers = providers
.as_sequence()
.ok_or_else(|| format!("{section} 必须是数组"))?;
for provider in providers {
let Some(provider) = provider.as_mapping() else {
continue;
};
let Some(configured_models) = yaml_mapping_value(provider, "models") else {
continue;
};
let configured_models = configured_models
.as_sequence()
.ok_or_else(|| format!("{section}.models 必须是数组"))?;
mark_agent_model_aliases_from_sequence(models, configured_models);
}
}
if let Some(oauth_aliases) = yaml_mapping_value(root, "oauth-model-alias") {
let oauth_aliases = oauth_aliases
.as_mapping()
.ok_or_else(|| "oauth-model-alias 必须是 YAML 映射".to_string())?;
for entries in oauth_aliases.values() {
let Some(entries) = entries.as_sequence() else {
continue;
};
mark_agent_model_aliases_from_sequence(models, entries);
}
}
Ok(())
}
fn mark_agent_model_aliases_from_sequence(
models: &mut [AgentModelOption],
configured_models: &[serde_norway::Value],
) {
for configured in configured_models {
let Some((source_model, client_model, _)) = configured_model_identity(configured) else {
continue;
};
if source_model.eq_ignore_ascii_case(&client_model) {
continue;
}
let Some(model) = models
.iter_mut()
.find(|model| model.name.eq_ignore_ascii_case(&client_model))
else {
continue;
};
model.is_alias = true;
model.alias = Some(source_model);
}
}
fn parse_codex_model_definitions(
payload: &serde_json::Value,
) -> Result<Vec<CodexModelDefinition>, String> {
@@ -4378,12 +4470,58 @@ fn inspect_claude_code_model_mappings(
}))
}
fn agent_model_context_window(models: &[AgentModelOption], model_name: &str) -> Option<u64> {
let selected = models
.iter()
.find(|model| model.name.eq_ignore_ascii_case(model_name))?;
if !selected.is_alias {
return Some(
selected
.context_window
.unwrap_or(DEFAULT_CLAUDE_CONTEXT_WINDOW),
);
}
selected
.alias
.as_deref()
.and_then(|source| {
models
.iter()
.find(|model| model.name.eq_ignore_ascii_case(source))
})
.and_then(|model| model.context_window)
.or(selected.context_window)
.or(Some(DEFAULT_CLAUDE_CONTEXT_WINDOW))
}
fn claude_code_max_context_tokens(
mappings: &ClaudeDesktopModelMappings,
models: &[AgentModelOption],
) -> Result<u64, String> {
[
("Opus", mappings.opus.as_str()),
("Sonnet", mappings.sonnet.as_str()),
("Haiku", mappings.haiku.as_str()),
]
.into_iter()
.map(|(role, model)| {
agent_model_context_window(models, model).ok_or_else(|| {
format!("CPA 模型 API 未返回 Claude Code {role} 映射模型 {model} 的上下文窗口")
})
})
.collect::<Result<Vec<_>, _>>()?
.into_iter()
.min()
.ok_or_else(|| "CPA 模型 API 未返回 Claude Code 上下文窗口".to_string())
}
fn claude_code_launch_environment(
base_url: &str,
api_key: &str,
mappings: &ClaudeDesktopModelMappings,
max_context_tokens: u64,
) -> Vec<(String, String)> {
[
let mut environment = [
("ANTHROPIC_BASE_URL", base_url),
("ANTHROPIC_AUTH_TOKEN", api_key),
("ANTHROPIC_MODEL", mappings.sonnet.as_str()),
@@ -4394,7 +4532,12 @@ fn claude_code_launch_environment(
]
.into_iter()
.map(|(key, value)| (key.to_string(), value.to_string()))
.collect()
.collect::<Vec<_>>();
environment.push((
CLAUDE_CODE_MAX_CONTEXT_TOKENS_ENV.to_string(),
max_context_tokens.to_string(),
));
environment
}
fn inspect_claude_desktop_agent_config(
@@ -4705,6 +4848,7 @@ fn build_agent_updates_with_oauth(
&root_base,
api_key,
model,
models,
claude_code_model_mappings,
)
.or_else(|_| {
@@ -4713,6 +4857,7 @@ fn build_agent_updates_with_oauth(
&root_base,
api_key,
model,
models,
claude_code_model_mappings,
)
})?;
@@ -4857,6 +5002,7 @@ fn build_claude_agent_config(
base_url: &str,
api_key: &str,
model: &str,
models: &[AgentModelOption],
mappings: Option<&ClaudeDesktopModelMappings>,
) -> Result<String, String> {
let mut root = match existing.map(str::trim).filter(|value| !value.is_empty()) {
@@ -4870,6 +5016,7 @@ fn build_claude_agent_config(
let mappings = mappings
.cloned()
.unwrap_or_else(|| ClaudeDesktopModelMappings::all(model));
let max_context_tokens = claude_code_max_context_tokens(&mappings, models)?;
let env = ensure_json_object_entry(root, "env");
env.remove("ANTHROPIC_API_KEY");
for (key, value) in [
@@ -4886,6 +5033,10 @@ fn build_claude_agent_config(
serde_json::Value::String(value.to_string()),
);
}
env.insert(
CLAUDE_CODE_MAX_CONTEXT_TOKENS_ENV.to_string(),
serde_json::Value::String(max_context_tokens.to_string()),
);
root.insert(
"model".to_string(),
serde_json::Value::String(mappings.sonnet),
@@ -4913,6 +5064,34 @@ fn remove_claude_code_conflicting_api_key(path: &Path) -> Result<bool, String> {
})
}
fn update_claude_code_context_window(path: &Path, max_context_tokens: u64) -> 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;
}
let expected = max_context_tokens.to_string();
let env = ensure_json_object_entry(root, "env");
if env
.get(CLAUDE_CODE_MAX_CONTEXT_TOKENS_ENV)
.and_then(serde_json::Value::as_str)
== Some(expected.as_str())
{
return false;
}
env.insert(
CLAUDE_CODE_MAX_CONTEXT_TOKENS_ENV.to_string(),
serde_json::Value::String(expected),
);
true
})
}
fn parse_agent_json_object(
existing: Option<&str>,
label: &str,
@@ -5062,21 +5241,7 @@ fn claude_desktop_inference_model(
.iter()
.find(|model| model.name.eq_ignore_ascii_case(source_model));
let direct_alias = selected.is_some_and(|model| model.is_alias);
let context_model = selected.and_then(|model| {
if !model.is_alias {
return Some(model);
}
model
.alias
.as_deref()
.and_then(|source| {
models
.iter()
.find(|candidate| candidate.name.eq_ignore_ascii_case(source))
})
.or(Some(model))
});
let context_window = context_model.and_then(|model| model.context_window);
let context_window = agent_model_context_window(models, source_model);
let mut entry = serde_json::Map::new();
entry.insert(
"name".to_string(),
@@ -5258,6 +5423,7 @@ fn remove_claude_code_managed_configuration(paths: &[PathBuf]) -> Result<Vec<Str
"ANTHROPIC_DEFAULT_SONNET_MODEL",
"ANTHROPIC_DEFAULT_OPUS_MODEL",
"ANTHROPIC_DEFAULT_FABLE_MODEL",
CLAUDE_CODE_MAX_CONTEXT_TOKENS_ENV,
] {
env.remove(key);
}
@@ -5618,6 +5784,7 @@ fn build_restored_claude_code_config(
"ANTHROPIC_DEFAULT_SONNET_MODEL",
"ANTHROPIC_DEFAULT_OPUS_MODEL",
"ANTHROPIC_DEFAULT_FABLE_MODEL",
CLAUDE_CODE_MAX_CONTEXT_TOKENS_ENV,
] {
restore_json_key(env, original_env, key);
}
@@ -7259,7 +7426,7 @@ fn fresh_agent_contents(
name: model.to_string(),
alias: None,
is_alias: false,
context_window: None,
context_window: Some(200_000),
}];
fresh_agent_contents_with_oauth(client, port, api_key, model, &models, false, None, None)
}
@@ -7282,6 +7449,7 @@ fn fresh_agent_contents_with_oauth(
&root_base,
api_key,
model,
models,
claude_code_model_mappings,
)?]),
AgentClient::ClaudeDesktop => Ok(vec![
@@ -7925,15 +8093,17 @@ fn restore_agent_applied_state_configuration(
Ok(())
}
fn restore_all_agent_session_configurations(home: &Path) {
for client in [
AgentClient::ClaudeCode,
AgentClient::ClaudeDesktop,
fn agent_clients_restored_on_exit() -> [AgentClient; 4] {
[
AgentClient::Codex,
AgentClient::OpenCode,
AgentClient::OpenClaw,
AgentClient::Hermes,
] {
]
}
fn restore_all_agent_session_configurations(home: &Path) {
for client in agent_clients_restored_on_exit() {
if let Err(error) = restore_agent_session_configuration(client, home) {
eprintln!("关闭时恢复 {} 配置失败: {error}", client.name());
}
@@ -8105,7 +8275,7 @@ fn reset_agent_configuration_to_default(
name: model.to_string(),
alias: None,
is_alias: false,
context_window: None,
context_window: Some(200_000),
}],
codex_catalog,
oauth_configuration: false,
@@ -12123,6 +12293,15 @@ async fn ensure_claude_desktop_model_aliases(
Ok(())
}
async fn remove_managed_claude_model_aliases(config: &GuiConfigFile) -> Result<(), String> {
let content = fetch_management_config_yaml(config).await?;
let updated = remove_managed_claude_model_aliases_in_yaml(&content)?;
if updated != content {
put_management_config_yaml(config, &updated).await?;
}
Ok(())
}
fn ensure_claude_desktop_model_aliases_in_yaml(
content: &str,
mappings: &ClaudeDesktopModelMappings,
@@ -12145,7 +12324,7 @@ fn ensure_claude_desktop_model_aliases_in_yaml(
.any(|model| model.name.eq_ignore_ascii_case(source_model) && model.is_alias);
if direct_alias {
if !source_model.eq_ignore_ascii_case(alias) {
remove_claude_desktop_model_alias(root, alias)?;
remove_managed_claude_model_alias(root, alias)?;
}
} else {
ensure_claude_desktop_model_alias(root, source_model, alias)?;
@@ -12154,10 +12333,32 @@ fn ensure_claude_desktop_model_aliases_in_yaml(
render_updated_core_yaml(&mut document, updated)
}
fn remove_claude_desktop_model_alias(
fn remove_managed_claude_model_aliases_in_yaml(content: &str) -> Result<String, String> {
let mut document = yaml_serde_edit::YamlValue::parse(content)
.map_err(|error| format!("解析内核 YAML 配置失败: {error}"))?;
let mut updated = document.get().clone();
let root = updated
.as_mapping_mut()
.ok_or_else(|| "内核配置顶层必须是 YAML 映射".to_string())?;
let mut changed = false;
for alias in [
CLAUDE_DESKTOP_OPUS_MODEL_ID,
CLAUDE_DESKTOP_SONNET_MODEL_ID,
CLAUDE_DESKTOP_HAIKU_MODEL_ID,
] {
changed |= remove_managed_claude_model_alias(root, alias)?;
}
if !changed {
return Ok(content.to_string());
}
render_updated_core_yaml(&mut document, updated)
}
fn remove_managed_claude_model_alias(
root: &mut serde_norway::Mapping,
alias: &str,
) -> Result<(), String> {
) -> Result<bool, String> {
let mut changed = false;
for section in MODEL_ALIAS_CONFIG_SECTIONS {
let Some(providers) = yaml_mapping_value_mut(root, section) else {
continue;
@@ -12175,13 +12376,84 @@ fn remove_claude_desktop_model_alias(
let models = models
.as_sequence_mut()
.ok_or_else(|| format!("{section}.models 必须是数组"))?;
let before = models.len();
models.retain(|model| !is_managed_claude_model_alias(model, alias));
changed |= models.len() != before;
}
}
Ok(changed)
}
fn remove_existing_claude_model_alias(
root: &mut serde_norway::Mapping,
alias: &str,
) -> Result<bool, String> {
let mut changed = false;
for section in MODEL_ALIAS_CONFIG_SECTIONS {
let Some(providers) = yaml_mapping_value_mut(root, section) else {
continue;
};
let providers = providers
.as_sequence_mut()
.ok_or_else(|| format!("{section} 必须是数组"))?;
for provider in providers {
let Some(provider) = provider.as_mapping_mut() else {
continue;
};
let Some(models) = yaml_mapping_value_mut(provider, "models") else {
continue;
};
let models = models
.as_sequence_mut()
.ok_or_else(|| format!("{section}.models 必须是数组"))?;
let before = models.len();
models.retain(|model| {
configured_model_identity(model)
.is_none_or(|(_, client_model, _)| !client_model.eq_ignore_ascii_case(alias))
});
changed |= models.len() != before;
}
}
Ok(())
Ok(changed)
}
fn managed_claude_alias_display_name(alias: &str) -> Option<&'static str> {
if alias.eq_ignore_ascii_case(CLAUDE_DESKTOP_OPUS_MODEL_ID) {
Some(MANAGED_CLAUDE_OPUS_ALIAS_DISPLAY_NAME)
} else if alias.eq_ignore_ascii_case(CLAUDE_DESKTOP_SONNET_MODEL_ID) {
Some(MANAGED_CLAUDE_SONNET_ALIAS_DISPLAY_NAME)
} else if alias.eq_ignore_ascii_case(CLAUDE_DESKTOP_HAIKU_MODEL_ID) {
Some(MANAGED_CLAUDE_HAIKU_ALIAS_DISPLAY_NAME)
} else {
None
}
}
fn is_managed_claude_model_alias(model: &serde_norway::Value, alias: &str) -> bool {
let Some(expected_display_name) = managed_claude_alias_display_name(alias) else {
return false;
};
let Some((_, client_model, display_name)) = configured_model_identity(model) else {
return false;
};
client_model.eq_ignore_ascii_case(alias)
&& display_name
.as_deref()
.is_some_and(|value| value == expected_display_name)
}
fn configured_managed_claude_alias_exists(root: &serde_norway::Mapping, alias: &str) -> bool {
MODEL_ALIAS_CONFIG_SECTIONS.iter().any(|section| {
yaml_mapping_value(root, section)
.and_then(serde_norway::Value::as_sequence)
.into_iter()
.flatten()
.filter_map(serde_norway::Value::as_mapping)
.filter_map(|provider| yaml_mapping_value(provider, "models"))
.filter_map(serde_norway::Value::as_sequence)
.flatten()
.any(|model| is_managed_claude_model_alias(model, alias))
})
}
fn ensure_claude_desktop_model_alias(
@@ -12190,12 +12462,12 @@ fn ensure_claude_desktop_model_alias(
alias: &str,
) -> Result<(), String> {
if let Some((existing_source, _)) = configured_model_client_identity(root, alias) {
if existing_source.eq_ignore_ascii_case(source_model) {
if existing_source.eq_ignore_ascii_case(source_model)
&& configured_managed_claude_alias_exists(root, alias)
{
return Ok(());
}
return Err(format!(
"Claude Desktop 别名 {alias} 已被模型 {existing_source} 使用,请先修改冲突模型的别名"
));
remove_existing_claude_model_alias(root, alias)?;
}
if append_claude_desktop_model_alias(root, source_model, alias)? {
return Ok(());
@@ -12285,6 +12557,12 @@ fn append_claude_desktop_model_alias(
yaml_key("alias"),
serde_norway::Value::String(alias.to_string()),
);
let display_name = managed_claude_alias_display_name(alias)
.ok_or_else(|| format!("不支持的 Claude 托管别名: {alias}"))?;
alias_model.insert(
yaml_key("display-name"),
serde_norway::Value::String(display_name.to_string()),
);
models.push(serde_norway::Value::Mapping(alias_model));
return Ok(true);
}
@@ -13496,12 +13774,123 @@ fn render_updated_core_yaml(
updated: serde_norway::Value,
) -> Result<String, String> {
document.set(updated);
let rendered = document.get_string();
let rendered = expand_top_level_flow_style_collections(&document.get_string(), document.get())?;
let rendered = indent_indentationless_yaml_sequences(&rendered);
serde_norway::from_str::<serde_norway::Value>(&rendered)
.map_err(|error| format!("验证更新后的内核配置失败: {error}"))?;
Ok(rendered)
}
fn expand_top_level_flow_style_collections(
content: &str,
document: &serde_norway::Value,
) -> Result<String, String> {
let root = document
.as_mapping()
.ok_or_else(|| "内核配置顶层必须是 YAML 映射".to_string())?;
let mut rendered = content.to_string();
for (key, value) in root {
let Some(key) = key.as_str() else {
continue;
};
let is_non_empty_collection = match value {
serde_norway::Value::Mapping(mapping) => !mapping.is_empty(),
serde_norway::Value::Sequence(sequence) => !sequence.is_empty(),
_ => false,
};
if !is_non_empty_collection || !top_level_yaml_entry_uses_flow_style(&rendered, key) {
continue;
}
let mut wrapper = serde_norway::Mapping::new();
wrapper.insert(yaml_key(key), value.clone());
let block = serde_norway::to_string(&serde_norway::Value::Mapping(wrapper))
.map_err(|error| format!("格式化内核 YAML 配置失败: {error}"))?;
rendered = replace_top_level_yaml_block(&rendered, key, &block);
}
Ok(rendered)
}
fn top_level_yaml_entry_uses_flow_style(content: &str, key: &str) -> bool {
let prefix = format!("{key}:");
yaml_line_ranges(content).into_iter().any(|range| {
let line = yaml_line_content(content, range);
if line.chars().next().is_some_and(char::is_whitespace) {
return false;
}
line.strip_prefix(&prefix)
.map(str::trim_start)
.is_some_and(|value| value.starts_with('[') || value.starts_with('{'))
})
}
fn indent_indentationless_yaml_sequences(content: &str) -> String {
let mut lines = content
.split_inclusive('\n')
.map(str::to_string)
.collect::<Vec<_>>();
if !content.is_empty() && !content.ends_with('\n') && lines.is_empty() {
lines.push(content.to_string());
}
loop {
let mut range_to_indent = None;
for key_index in 0..lines.len() {
let key_line = lines[key_index].trim_end_matches(['\r', '\n']);
let key_trimmed = key_line.trim_start_matches(' ');
if key_trimmed.is_empty()
|| key_trimmed.starts_with('#')
|| key_trimmed.starts_with('-')
|| !key_trimmed.ends_with(':')
{
continue;
}
let parent_indent = key_line.len() - key_trimmed.len();
let Some(first_item) = ((key_index + 1)..lines.len()).find(|index| {
let line = lines[*index].trim_end_matches(['\r', '\n']).trim();
!line.is_empty() && !line.starts_with('#')
}) else {
continue;
};
let first_line = lines[first_item].trim_end_matches(['\r', '\n']);
let first_trimmed = first_line.trim_start_matches(' ');
let first_indent = first_line.len() - first_trimmed.len();
if first_indent != parent_indent
|| !is_indentationless_yaml_sequence_item(first_trimmed)
{
continue;
}
let mut end = first_item;
while end < lines.len() {
let line = lines[end].trim_end_matches(['\r', '\n']);
let trimmed = line.trim_start_matches(' ');
if !trimmed.is_empty() {
let indent = line.len() - trimmed.len();
if indent < parent_indent
|| (indent == parent_indent
&& !is_indentationless_yaml_sequence_item(trimmed))
{
break;
}
}
end += 1;
}
range_to_indent = Some((first_item, end));
break;
}
let Some((start, end)) = range_to_indent else {
break;
};
for line in &mut lines[start..end] {
if !line.trim().is_empty() {
line.insert_str(0, " ");
}
}
}
lines.concat()
}
fn management_http_client() -> Result<reqwest::Client, String> {
reqwest::Client::builder()
.connect_timeout(Duration::from_secs(10))
@@ -17284,12 +17673,24 @@ fn main() {
}
tauri::RunEvent::Exit => {
usage::stop_usage_collector(app_handle);
let gui_config_state = app_handle.state::<GuiConfigState>();
match gui_config_state.snapshot() {
Ok(config) => {
if let Err(error) =
tauri::async_runtime::block_on(remove_managed_claude_model_aliases(&config))
{
eprintln!("退出时清理 EasyCLIProxyAPI 托管的 Claude 模型别名失败: {error}");
}
}
Err(error) => {
eprintln!("退出时读取 GUI 配置失败,无法清理 Claude 模型别名: {error}");
}
}
if let Ok(home) = app_handle.path().home_dir() {
let _guard = AGENT_CONFIG_FILE_LOCK.lock();
restore_all_agent_session_configurations(&home);
}
let process_state = app_handle.state::<CoreProcessState>();
let gui_config_state = app_handle.state::<GuiConfigState>();
shutdown_managed_core(process_state.inner(), gui_config_state.inner());
}
_ => {}
@@ -17447,7 +17848,7 @@ data: {"delta":{"type":"text_delta","text":"H"}}
name: (*name).to_string(),
alias: None,
is_alias: false,
context_window: None,
context_window: Some(200_000),
})
.collect()
}
@@ -17590,6 +17991,7 @@ data: {"delta":{"type":"text_delta","text":"H"}}
"http://127.0.0.1:8317",
DEFAULT_API_KEY,
"gpt-test",
&test_agent_models(&["gpt-test"]),
None,
)
.unwrap();
@@ -18496,6 +18898,7 @@ model:
"http://127.0.0.1:8317",
DEFAULT_API_KEY,
"model-a",
&test_agent_models(&["model-a"]),
None,
)
.unwrap();
@@ -18741,6 +19144,7 @@ model:
"http://127.0.0.1:8317",
DEFAULT_API_KEY,
"claude-test",
&test_agent_models(&["claude-test"]),
None,
)
.unwrap();
@@ -18762,11 +19166,38 @@ model:
sonnet: "gpt-sonnet".to_string(),
haiku: "gpt-haiku".to_string(),
};
let models = vec![
AgentModelOption {
name: "gpt-opus-base".to_string(),
alias: None,
is_alias: false,
context_window: Some(1_000_000),
},
AgentModelOption {
name: mappings.opus.clone(),
alias: Some("gpt-opus-base".to_string()),
is_alias: true,
context_window: Some(128_000),
},
AgentModelOption {
name: mappings.sonnet.clone(),
alias: None,
is_alias: false,
context_window: Some(272_000),
},
AgentModelOption {
name: mappings.haiku.clone(),
alias: None,
is_alias: false,
context_window: Some(512_000),
},
];
let rendered = build_claude_agent_config(
None,
"http://127.0.0.1:8317",
"test-key",
"gpt-sonnet",
&models,
Some(&mappings),
)
.unwrap();
@@ -18775,9 +19206,14 @@ model:
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");
assert_eq!(value["env"][CLAUDE_CODE_MAX_CONTEXT_TOKENS_ENV], "272000");
let environment =
claude_code_launch_environment("http://127.0.0.1:8317", "test-key", &mappings);
let environment = claude_code_launch_environment(
"http://127.0.0.1:8317",
"test-key",
&mappings,
claude_code_max_context_tokens(&mappings, &models).unwrap(),
);
let read = |key: &str| {
environment
.iter()
@@ -18790,6 +19226,23 @@ model:
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"));
assert_eq!(read(CLAUDE_CODE_MAX_CONTEXT_TOKENS_ENV), Some("272000"));
}
#[test]
fn claude_code_uses_200k_when_cpa_context_metadata_is_missing() {
let mappings = ClaudeDesktopModelMappings::all("custom-model");
let models = vec![AgentModelOption {
name: "custom-model".to_string(),
alias: None,
is_alias: false,
context_window: None,
}];
assert_eq!(
claude_code_max_context_tokens(&mappings, &models).unwrap(),
DEFAULT_CLAUDE_CONTEXT_WINDOW
);
}
#[test]
@@ -18803,10 +19256,13 @@ model:
.unwrap();
assert!(remove_claude_code_conflicting_api_key(&path).unwrap());
assert!(update_claude_code_context_window(&path, 272_000).unwrap());
assert!(!update_claude_code_context_window(&path, 272_000).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");
assert_eq!(value["env"][CLAUDE_CODE_MAX_CONTEXT_TOKENS_ENV], "272000");
fs::remove_dir_all(directory).unwrap();
}
@@ -19274,7 +19730,7 @@ model:
"data": [
{"id": "gpt-5", "display_name": "GPT 5", "context_length": 272000},
{"name": "claude-sonnet", "alias": "claude-sonnet-xhigh", "fork": true, "contextLength": "1000000"},
{"name": "hidden-original", "alias": "visible-alias", "context_window": 128000},
{"name": "hidden-original", "alias": "visible-alias", "ContextLength": 128000},
"deepseek-chat",
{"id": "GPT-5"},
{"id": ""}
@@ -19319,6 +19775,52 @@ model:
);
}
#[test]
fn claude_model_lists_mark_yaml_aliases_when_api_returns_only_model_ids() {
let mut models = test_agent_models(&[
"gpt-original",
"gpt-high",
"claude-opus-5",
"oauth-original",
"oauth-fast",
]);
let yaml = r#"
codex-api-key:
- api-key: test
models:
- name: gpt-original
- name: gpt-original
alias: gpt-high
- name: gpt-original
alias: claude-opus-5
oauth-model-alias:
codex:
- name: oauth-original
alias: oauth-fast
fork: true
"#;
mark_configured_agent_model_aliases(&mut models, yaml).unwrap();
let find = |name: &str| models.iter().find(|model| model.name == name).unwrap();
assert!(!find("gpt-original").is_alias);
assert_eq!(find("gpt-high").alias.as_deref(), Some("gpt-original"));
assert!(find("gpt-high").is_alias);
assert_eq!(find("claude-opus-5").alias.as_deref(), Some("gpt-original"));
assert!(find("claude-opus-5").is_alias);
assert_eq!(find("oauth-fast").alias.as_deref(), Some("oauth-original"));
assert!(find("oauth-fast").is_alias);
}
#[test]
fn exit_preserves_claude_client_configurations() {
let restored = agent_clients_restored_on_exit();
assert!(!restored.contains(&AgentClient::ClaudeCode));
assert!(!restored.contains(&AgentClient::ClaudeDesktop));
assert!(restored.contains(&AgentClient::Codex));
assert!(restored.contains(&AgentClient::OpenCode));
}
#[test]
fn claude_desktop_aliases_expose_role_routes_only() {
let input = "openai-compatibility:\n - name: CPA\n base-url: https://example.com/v1\n models:\n - name: gpt-5.6-sol\n";
@@ -19353,6 +19855,23 @@ model:
configured_model_identity(&models[3]).unwrap().1,
CLAUDE_DESKTOP_HAIKU_MODEL_ID
);
assert_eq!(
configured_model_identity(&models[1]).unwrap().2.as_deref(),
Some(MANAGED_CLAUDE_OPUS_ALIAS_DISPLAY_NAME)
);
assert_eq!(
configured_model_identity(&models[2]).unwrap().2.as_deref(),
Some(MANAGED_CLAUDE_SONNET_ALIAS_DISPLAY_NAME)
);
assert_eq!(
configured_model_identity(&models[3]).unwrap().2.as_deref(),
Some(MANAGED_CLAUDE_HAIKU_ALIAS_DISPLAY_NAME)
);
assert!(!rendered.contains("models: [{"));
assert!(rendered.contains("\n - name: gpt-5.6-sol\n"));
assert!(rendered.contains("\n alias: claude-opus-5\n"));
assert!(rendered
.contains("\n display-name: EasyCLIProxyAPI managed Claude Opus mapping\n"));
assert_eq!(
ensure_claude_desktop_model_aliases_in_yaml(&rendered, &mappings, &available_models,)
.unwrap(),
@@ -19360,9 +19879,83 @@ model:
);
}
#[test]
fn claude_managed_aliases_expand_flow_yaml_and_are_removed_selectively() {
let compact = "# keep this comment\ncodex-api-key: [{api-key: test, models: [{name: gpt-5.6-sol, alias: ''}]}]\ncredential-concurrency: {max-limit: 1000000, cleanup-interval: 5s}\n";
let mappings = ClaudeDesktopModelMappings::all("gpt-5.6-sol");
let available_models = test_agent_models(&["gpt-5.6-sol"]);
let rendered =
ensure_claude_desktop_model_aliases_in_yaml(compact, &mappings, &available_models)
.unwrap();
assert!(rendered.starts_with("# keep this comment\n"));
assert!(!rendered.contains("codex-api-key: ["));
assert!(!rendered.contains("credential-concurrency: {"));
assert!(rendered.contains("codex-api-key:\n - api-key: test\n"));
assert!(rendered.contains(" models:\n - name: gpt-5.6-sol\n"));
let cleaned = remove_managed_claude_model_aliases_in_yaml(&rendered).unwrap();
let value: serde_norway::Value = serde_norway::from_str(&cleaned).unwrap();
let root = value.as_mapping().unwrap();
let providers = yaml_mapping_value(root, "codex-api-key")
.and_then(serde_norway::Value::as_sequence)
.unwrap();
let models = yaml_mapping_value(providers[0].as_mapping().unwrap(), "models")
.and_then(serde_norway::Value::as_sequence)
.unwrap();
assert_eq!(models.len(), 1);
assert_eq!(
configured_model_identity(&models[0]).unwrap().0,
"gpt-5.6-sol"
);
}
#[test]
fn claude_alias_cleanup_does_not_delete_user_owned_routes() {
let input = format!(
"codex-api-key:\n - api-key: test\n models:\n - name: user-opus\n alias: {opus}\n display-name: User managed route\n - name: app-sonnet\n alias: {sonnet}\n display-name: {managed_sonnet}\n",
opus = CLAUDE_DESKTOP_OPUS_MODEL_ID,
sonnet = CLAUDE_DESKTOP_SONNET_MODEL_ID,
managed_sonnet = MANAGED_CLAUDE_SONNET_ALIAS_DISPLAY_NAME,
);
let cleaned = remove_managed_claude_model_aliases_in_yaml(&input).unwrap();
let value: serde_norway::Value = serde_norway::from_str(&cleaned).unwrap();
let root = value.as_mapping().unwrap();
assert_eq!(
configured_model_client_identity(root, CLAUDE_DESKTOP_OPUS_MODEL_ID)
.unwrap()
.0,
"user-opus"
);
assert!(configured_model_client_identity(root, CLAUDE_DESKTOP_SONNET_MODEL_ID).is_none());
}
#[test]
fn claude_legacy_aliases_are_adopted_and_moved_to_the_selected_model() {
let input = format!(
"codex-api-key:\n - api-key: test\n models:\n - name: old-model\n - name: new-model\n - name: old-model\n alias: {opus}\n",
opus = CLAUDE_DESKTOP_OPUS_MODEL_ID,
);
let mappings = ClaudeDesktopModelMappings::all("new-model");
let models = test_agent_models(&["old-model", "new-model"]);
let rendered =
ensure_claude_desktop_model_aliases_in_yaml(&input, &mappings, &models).unwrap();
let value: serde_norway::Value = serde_norway::from_str(&rendered).unwrap();
let root = value.as_mapping().unwrap();
assert_eq!(
configured_model_client_identity(root, CLAUDE_DESKTOP_OPUS_MODEL_ID)
.unwrap()
.0,
"new-model"
);
assert!(rendered.contains(MANAGED_CLAUDE_OPUS_ALIAS_DISPLAY_NAME));
}
#[test]
fn claude_desktop_uses_selected_alias_directly_with_original_context() {
let input = "openai-compatibility:\n - name: CPA\n base-url: https://example.com/v1\n models:\n - name: gpt-original\n - name: gpt-original\n alias: gpt-high\n - name: gpt-original\n alias: claude-opus-5\n";
let input = "openai-compatibility:\n - name: CPA\n base-url: https://example.com/v1\n models:\n - name: gpt-original\n - name: gpt-original\n alias: gpt-high\n - name: gpt-original\n alias: claude-opus-5\n display-name: EasyCLIProxyAPI managed Claude Opus mapping\n";
let mappings = ClaudeDesktopModelMappings {
opus: "gpt-high".to_string(),
sonnet: "gpt-high".to_string(),
-7
View File
@@ -554,13 +554,6 @@ export const jaOverrides = {
'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 のマッピング先',
-14
View File
@@ -552,13 +552,6 @@ export const zhCN = {
'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 映射到',
@@ -1487,13 +1480,6 @@ export const en: Record<MessageKey, string> = {
'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',
+25 -39
View File
@@ -123,20 +123,14 @@ 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;
@@ -1177,33 +1171,30 @@ export function AgentsPage() {
</aside>
<section className="panel agent-config-panel">
{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}
<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>
{activeSubpage === 'core' ? (
<div
className={`agent-core-config ${selected === 'claude-code' ? 'agent-claude-code-config' : ''}`}
className="agent-core-config"
id="agent-subpage-panel-core"
role="tabpanel"
aria-labelledby={selected === 'claude-code' ? undefined : 'agent-subpage-tab-core'}
aria-label={selected === 'claude-code' ? t('agents.claudeCodeMapping.title') : undefined}
aria-labelledby="agent-subpage-tab-core"
>
<div className="agent-status-grid">
<div>
@@ -1254,7 +1245,7 @@ export function AgentsPage() {
) : null}
{isClaudeModelMappingClient ? (
<section className={`agent-core-setting-section agent-claude-desktop-mapping ${selected === 'claude-code' ? 'agent-claude-code-mapping' : ''}`}>
<section className="agent-core-setting-section agent-claude-desktop-mapping">
<div className="agent-section-heading">
<div>
<strong>{t(selected === 'claude-code'
@@ -1269,9 +1260,7 @@ export function AgentsPage() {
className="agent-claude-desktop-mapping-filter"
title={t('agents.claudeDesktopMapping.customMappingHint')}
>
<span>{t(selected === 'claude-code'
? 'agents.claudeCodeMapping.aliasOnly'
: 'agents.claudeDesktopMapping.customMapping')}</span>
<span>{t('agents.claudeDesktopMapping.customMapping')}</span>
<span className="switch-control">
<input
type="checkbox"
@@ -1287,11 +1276,8 @@ export function AgentsPage() {
</div>
<div className="agent-claude-desktop-mapping-grid">
{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>
<div className="agent-claude-desktop-mapping-row" key={role.key}>
<strong>{t(role.labelKey)}</strong>
<AgentModelPicker
models={claudeMappingModels}
value={claudeModelMappingsDraft[role.key]}
@@ -1364,7 +1350,7 @@ export function AgentsPage() {
>
{busyAction === 'default'
? <LoaderCircle size={16} className="spin" />
: selected === 'claude-code' ? null : <RefreshCw size={16} />}
: <RefreshCw size={16} />}
{t('agents.modify.default')}
</button>
{selected === 'codex' ? (
@@ -1436,7 +1422,7 @@ export function AgentsPage() {
>
{busyAction === 'launch'
? <LoaderCircle size={16} className="spin" />
: selected === 'claude-code' ? null : <Play size={16} />}
: <Play size={16} />}
{busyAction === 'launch' ? t('agents.launch.starting') : defaultLaunchTarget ? t('agents.launch.start', { target: defaultLaunchTarget.label }) : t('agents.launch.unavailable')}
</button>
)}
+14 -156
View File
@@ -3456,7 +3456,7 @@ textarea:focus-visible {
.real-provider-row {
display: grid;
grid-template-columns: minmax(280px, 1.6fr) minmax(120px, 0.5fr) auto;
grid-template-columns: minmax(280px, 1.6fr) minmax(120px, 0.5fr) max-content;
align-items: start;
gap: 16px;
min-height: 88px;
@@ -3559,10 +3559,15 @@ textarea:focus-visible {
}
.provider-row-actions .provider-health-button {
min-width: max-content;
width: auto;
flex: 0 0 auto;
min-height: 30px;
overflow: visible;
padding: 0 10px;
font-size: var(--font-size-label);
text-overflow: clip;
white-space: nowrap;
}
.provider-enabled-control {
@@ -5310,127 +5315,6 @@ 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;
@@ -7294,27 +7178,6 @@ textarea:focus-visible {
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) {
@@ -7345,19 +7208,6 @@ 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;
@@ -7455,6 +7305,14 @@ textarea:focus-visible {
height: 520px;
}
.real-provider-row {
grid-template-columns: minmax(0, 1fr) max-content;
}
.provider-row-meta {
display: none;
}
.agent-workbench {
grid-template-columns: 200px minmax(0, 1fr);
}