feat(claude-code): align model mapping UI with Claude Code settings
- Add per-role model rows with inline 1M context controls - Add bulk action to apply Sonnet settings to all roles - Persist startup model and context settings in official settings.json fields - Preserve custom role mappings and Subagent configuration - Add multilingual labels and UI regression coverage - Validate behavior with real Claude Code 2.1.292
This commit is contained in:
1 parent
b564cb384c
commit
b90445143c
23 files changed
+717
-191
No files matched your search
@@ -1043,6 +1043,8 @@ pub(crate) fn resolve_claude_desktop_model_mappings(
|
||||
disable_auto_compact: requested.disable_auto_compact,
|
||||
manage_default_model: requested.manage_default_model,
|
||||
manage_subagent_model: requested.manage_subagent_model,
|
||||
startup_model: requested.startup_model.map(|value| value.trim().to_string()),
|
||||
subagent_model: requested.subagent_model.map(|value| value.trim().to_string()),
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -1062,13 +1064,21 @@ pub(crate) fn resolve_claude_code_model_mappings(
|
||||
if !(1..=100).contains(&requested.auto_compact_pct) {
|
||||
return Err("Claude Code compaction trigger percentage must be between 1 and 100".to_string());
|
||||
}
|
||||
let max_context_tokens = if requested.opus_1m || requested.sonnet_1m || requested.haiku_1m {
|
||||
let explicit_1m = [&requested.startup_model, &requested.subagent_model]
|
||||
.iter().any(|value| value.as_deref().is_some_and(|model|
|
||||
model.trim().to_ascii_lowercase().ends_with("[1m]")));
|
||||
let max_context_tokens = if requested.opus_1m || requested.sonnet_1m || requested.haiku_1m || explicit_1m {
|
||||
CLAUDE_DESKTOP_EXTENDED_CONTEXT_WINDOW
|
||||
} else {
|
||||
requested.max_context_tokens
|
||||
};
|
||||
let resolve =
|
||||
|model: &str| resolve_available_agent_model(models, &validate_agent_model(model)?);
|
||||
let normalize_override = |value: Option<String>| -> Result<Option<String>, String> {
|
||||
value.map(|value| {
|
||||
if value.trim().is_empty() { Ok(String::new()) } else { validate_agent_model(&value) }
|
||||
}).transpose()
|
||||
};
|
||||
Ok(Some(ClaudeDesktopModelMappings {
|
||||
desktop_models: None,
|
||||
opus: resolve(&requested.opus)?,
|
||||
@@ -1082,6 +1092,8 @@ pub(crate) fn resolve_claude_code_model_mappings(
|
||||
disable_auto_compact: requested.disable_auto_compact,
|
||||
manage_default_model: requested.manage_default_model,
|
||||
manage_subagent_model: requested.manage_subagent_model,
|
||||
startup_model: normalize_override(requested.startup_model)?,
|
||||
subagent_model: normalize_override(requested.subagent_model)?,
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
@@ -584,14 +584,17 @@ pub(crate) fn build_claude_agent_config(
|
||||
let max_context_tokens = mappings.max_context_tokens;
|
||||
let model_settings = claude_code_model_settings(&mappings);
|
||||
let subagent_model = model_settings.haiku.clone();
|
||||
let effort_level = claude_code_model_effort_level(models, &mappings.sonnet)?;
|
||||
// Explicit/default session selections may switch roles, while Subagents may use
|
||||
// another model. A global Sonnet-derived effort override is wrong for both.
|
||||
let effort_level = if mappings.startup_model.is_none() && mappings.subagent_model.is_none() {
|
||||
claude_code_model_effort_level(models, &mappings.sonnet)?
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let env = ensure_json_object_entry(root, "env");
|
||||
env.remove("ANTHROPIC_API_KEY");
|
||||
env.remove("CLAUDE_CODE_EFFORT_LEVEL");
|
||||
env.remove(CLAUDE_CODE_MAX_CONTEXT_TOKENS_ENV);
|
||||
env.remove(CLAUDE_CODE_AUTO_MODE_SERVER_ENV);
|
||||
env.remove(CLAUDE_AUTOCOMPACT_PCT_OVERRIDE_ENV);
|
||||
env.remove(DISABLE_AUTO_COMPACT_ENV);
|
||||
for (key, value) in [
|
||||
("ANTHROPIC_BASE_URL", base_url),
|
||||
("ANTHROPIC_AUTH_TOKEN", api_key),
|
||||
@@ -616,13 +619,19 @@ pub(crate) fn build_claude_agent_config(
|
||||
}
|
||||
env.insert(
|
||||
"EASYCLIPROXY_MANAGE_CLAUDE_CODE_DEFAULT_MODEL".to_string(),
|
||||
serde_json::Value::String(if mappings.manage_default_model { "1" } else { "0" }.to_string()),
|
||||
serde_json::Value::String(if mappings.startup_model.as_ref().map_or(mappings.manage_default_model, |value| !value.trim().is_empty()) { "1" } else { "0" }.to_string()),
|
||||
);
|
||||
env.insert(
|
||||
"EASYCLIPROXY_MANAGE_CLAUDE_CODE_SUBAGENT_MODEL".to_string(),
|
||||
serde_json::Value::String(if mappings.manage_subagent_model { "1" } else { "0" }.to_string()),
|
||||
serde_json::Value::String(if mappings.subagent_model.as_ref().map_or(mappings.manage_subagent_model, |value| !value.trim().is_empty()) { "1" } else { "0" }.to_string()),
|
||||
);
|
||||
if mappings.manage_default_model {
|
||||
let explicit_startup_model = mappings.startup_model.as_ref().map(|model| model.trim().to_string());
|
||||
if mappings.startup_model.is_some() {
|
||||
// Claude Code's official persistent startup-model setting is the root
|
||||
// `model` key. Keep ANTHROPIC_MODEL out of the generated file so the
|
||||
// saved choice can still be changed from Claude Code's /model picker.
|
||||
env.remove("ANTHROPIC_MODEL");
|
||||
} else if mappings.manage_default_model {
|
||||
env.insert(
|
||||
"ANTHROPIC_MODEL".to_string(),
|
||||
serde_json::Value::String(model_settings.sonnet.clone()),
|
||||
@@ -634,7 +643,13 @@ pub(crate) fn build_claude_agent_config(
|
||||
{
|
||||
env.remove("ANTHROPIC_MODEL");
|
||||
}
|
||||
if mappings.manage_subagent_model {
|
||||
if let Some(model) = &mappings.subagent_model {
|
||||
if model.trim().is_empty() {
|
||||
env.remove("CLAUDE_CODE_SUBAGENT_MODEL");
|
||||
} else {
|
||||
env.insert("CLAUDE_CODE_SUBAGENT_MODEL".into(), serde_json::Value::String(model.trim().into()));
|
||||
}
|
||||
} else if mappings.manage_subagent_model {
|
||||
env.entry("CLAUDE_CODE_SUBAGENT_MODEL".to_string())
|
||||
.or_insert_with(|| serde_json::Value::String(subagent_model));
|
||||
} else if env
|
||||
@@ -644,10 +659,6 @@ pub(crate) fn build_claude_agent_config(
|
||||
{
|
||||
env.remove("CLAUDE_CODE_SUBAGENT_MODEL");
|
||||
}
|
||||
env.insert(
|
||||
CLAUDE_CODE_MAX_CONTEXT_TOKENS_ENV.to_string(),
|
||||
serde_json::Value::String(max_context_tokens.to_string()),
|
||||
);
|
||||
env.insert(
|
||||
CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY_ENV.to_string(),
|
||||
serde_json::Value::String("1".to_string()),
|
||||
@@ -656,16 +667,6 @@ pub(crate) fn build_claude_agent_config(
|
||||
CLAUDE_CODE_AUTO_MODE_SERVER_ENV.to_string(),
|
||||
serde_json::Value::String("0".to_string()),
|
||||
);
|
||||
env.insert(
|
||||
CLAUDE_AUTOCOMPACT_PCT_OVERRIDE_ENV.to_string(),
|
||||
serde_json::Value::String(mappings.auto_compact_pct.to_string()),
|
||||
);
|
||||
if mappings.disable_auto_compact {
|
||||
env.insert(
|
||||
DISABLE_AUTO_COMPACT_ENV.to_string(),
|
||||
serde_json::Value::String("1".to_string()),
|
||||
);
|
||||
}
|
||||
for (key, value) in claude_code_model_presentation_environment(&mappings, models)? {
|
||||
env.insert(key, serde_json::Value::String(value));
|
||||
}
|
||||
@@ -675,12 +676,49 @@ pub(crate) fn build_claude_agent_config(
|
||||
serde_json::Value::String(effort_level),
|
||||
);
|
||||
}
|
||||
if mappings.manage_default_model {
|
||||
// Claude Code 2.1.288+ supports a per-model auto-compaction window in
|
||||
// settings.json. Keep the existing environment override for compatibility
|
||||
// with older Claude Code versions and custom gateway IDs, but also emit the
|
||||
// official settings so newer versions can apply the value per model.
|
||||
root.insert("autoCompactWindow".into(), serde_json::Value::Number(max_context_tokens.into()));
|
||||
root.insert("autoCompactEnabled".into(), serde_json::Value::Bool(!mappings.disable_auto_compact));
|
||||
let model_settings_json = ensure_json_object_entry(root, "modelSettings");
|
||||
for (model_id, window) in [
|
||||
(&mappings.opus, if mappings.opus_1m { CLAUDE_DESKTOP_EXTENDED_CONTEXT_WINDOW } else { mappings.max_context_tokens }),
|
||||
(&mappings.sonnet, if mappings.sonnet_1m { CLAUDE_DESKTOP_EXTENDED_CONTEXT_WINDOW } else { mappings.max_context_tokens }),
|
||||
(&mappings.haiku, if mappings.haiku_1m { CLAUDE_DESKTOP_EXTENDED_CONTEXT_WINDOW } else { mappings.max_context_tokens }),
|
||||
] {
|
||||
let model_id = strip_claude_code_context_suffix(model_id).trim();
|
||||
if model_id.is_empty() { continue; }
|
||||
let entry = ensure_json_object_entry(model_settings_json, model_id);
|
||||
let current = entry.get("autoCompactWindow").and_then(serde_json::Value::as_u64).unwrap_or(0);
|
||||
entry.insert("autoCompactWindow".into(), serde_json::Value::Number(current.max(window).into()));
|
||||
}
|
||||
for model in [&mappings.startup_model, &mappings.subagent_model] {
|
||||
let Some(model) = model.as_deref().map(str::trim).filter(|value| !value.is_empty()) else { continue; };
|
||||
let model_id = strip_claude_code_context_suffix(model).trim();
|
||||
let window = if model.to_ascii_lowercase().ends_with("[1m]") {
|
||||
CLAUDE_DESKTOP_EXTENDED_CONTEXT_WINDOW
|
||||
} else {
|
||||
mappings.max_context_tokens
|
||||
};
|
||||
let entry = ensure_json_object_entry(model_settings_json, model_id);
|
||||
let current = entry.get("autoCompactWindow").and_then(serde_json::Value::as_u64).unwrap_or(0);
|
||||
entry.insert("autoCompactWindow".into(), serde_json::Value::Number(current.max(window).into()));
|
||||
}
|
||||
if let Some(model) = explicit_startup_model {
|
||||
if model.is_empty() {
|
||||
root.remove("model");
|
||||
} else {
|
||||
root.insert("model".into(), serde_json::Value::String(model));
|
||||
}
|
||||
}
|
||||
if mappings.startup_model.is_none() && mappings.manage_default_model {
|
||||
root.insert(
|
||||
"model".to_string(),
|
||||
serde_json::Value::String(model_settings.sonnet),
|
||||
);
|
||||
} else if root
|
||||
} else if mappings.startup_model.is_none() && root
|
||||
.get("model")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.is_some_and(|value| value == model_settings.opus || value == model_settings.sonnet || value == model_settings.haiku)
|
||||
|
||||
@@ -2925,7 +2925,8 @@ pub(crate) fn inspect_claude_code_model_mappings(
|
||||
.get("ANTHROPIC_MODEL")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty());
|
||||
.filter(|value| !value.is_empty())
|
||||
.or_else(|| root.get("model").and_then(serde_json::Value::as_str).map(str::trim).filter(|value| !value.is_empty()));
|
||||
let read_model = |key: &str| {
|
||||
let value = env
|
||||
.get(key)
|
||||
@@ -2946,10 +2947,13 @@ pub(crate) fn inspect_claude_code_model_mappings(
|
||||
return Ok(None);
|
||||
};
|
||||
let legacy_1m = opus_had_1m || sonnet_had_1m || haiku_had_1m;
|
||||
let max_context_tokens = env
|
||||
.get(CLAUDE_CODE_MAX_CONTEXT_TOKENS_ENV)
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.and_then(|value| value.trim().parse::<u64>().ok())
|
||||
let max_context_tokens = root
|
||||
.get("autoCompactWindow")
|
||||
.and_then(serde_json::Value::as_u64)
|
||||
.or_else(|| env
|
||||
.get(CLAUDE_CODE_MAX_CONTEXT_TOKENS_ENV)
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.and_then(|value| value.trim().parse::<u64>().ok()))
|
||||
.unwrap_or_else(|| {
|
||||
if legacy_1m {
|
||||
CLAUDE_DESKTOP_EXTENDED_CONTEXT_WINDOW
|
||||
@@ -2962,10 +2966,14 @@ pub(crate) fn inspect_claude_code_model_mappings(
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.and_then(|value| value.trim().parse::<u8>().ok())
|
||||
.unwrap_or_else(default_claude_auto_compact_pct);
|
||||
let disable_auto_compact = env
|
||||
.get(DISABLE_AUTO_COMPACT_ENV)
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.is_some_and(|value| value == "1" || value.eq_ignore_ascii_case("true"));
|
||||
let disable_auto_compact = root
|
||||
.get("autoCompactEnabled")
|
||||
.and_then(serde_json::Value::as_bool)
|
||||
.map(|enabled| !enabled)
|
||||
.unwrap_or_else(|| env
|
||||
.get(DISABLE_AUTO_COMPACT_ENV)
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.is_some_and(|value| value == "1" || value.eq_ignore_ascii_case("true")));
|
||||
Ok(Some(ClaudeDesktopModelMappings {
|
||||
desktop_models: None,
|
||||
opus,
|
||||
@@ -2987,6 +2995,17 @@ pub(crate) fn inspect_claude_code_model_mappings(
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.map(|value| value != "0" && !value.eq_ignore_ascii_case("false"))
|
||||
.unwrap_or(true),
|
||||
startup_model: Some(env
|
||||
.get("ANTHROPIC_MODEL")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.or_else(|| root.get("model").and_then(serde_json::Value::as_str))
|
||||
.unwrap_or_default()
|
||||
.to_string()),
|
||||
subagent_model: Some(env
|
||||
.get("CLAUDE_CODE_SUBAGENT_MODEL")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.unwrap_or_default()
|
||||
.to_string()),
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -3115,6 +3134,8 @@ pub(crate) fn claude_code_model_settings(
|
||||
disable_auto_compact: mappings.disable_auto_compact,
|
||||
manage_default_model: mappings.manage_default_model,
|
||||
manage_subagent_model: mappings.manage_subagent_model,
|
||||
startup_model: mappings.startup_model.clone(),
|
||||
subagent_model: mappings.subagent_model.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -840,6 +840,15 @@ fn validate_unmanaged_preserved(
|
||||
}
|
||||
}
|
||||
if client == "claude-code" {
|
||||
if let Some(root) = value.as_object_mut() {
|
||||
// These are CPA-managed Claude Code settings. Exclude them
|
||||
// when comparing user-owned configuration so a model update
|
||||
// does not get rejected as an unrelated custom edit.
|
||||
root.remove("model");
|
||||
root.remove("modelSettings");
|
||||
root.remove("autoCompactWindow");
|
||||
root.remove("autoCompactEnabled");
|
||||
}
|
||||
if let Some(env) = value.get_mut("env").and_then(Value::as_object_mut) {
|
||||
env.retain(|key, _| {
|
||||
!matches!(
|
||||
|
||||
@@ -1163,6 +1163,12 @@ struct ClaudeDesktopModelMappings {
|
||||
/// Defaults to true for backwards compatibility with existing profiles.
|
||||
#[serde(default = "default_true")]
|
||||
manage_subagent_model: bool,
|
||||
/// Optional explicit model written to ANTHROPIC_MODEL.
|
||||
#[serde(default)]
|
||||
startup_model: Option<String>,
|
||||
/// Optional explicit model written to CLAUDE_CODE_SUBAGENT_MODEL.
|
||||
#[serde(default)]
|
||||
subagent_model: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
|
||||
@@ -1227,6 +1233,8 @@ impl ClaudeDesktopModelMappings {
|
||||
disable_auto_compact: false,
|
||||
manage_default_model: true,
|
||||
manage_subagent_model: true,
|
||||
startup_model: None,
|
||||
subagent_model: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,73 @@
|
||||
use super::support::*;
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn claude_code_explicit_model_inputs_replace_clear_and_round_trip() {
|
||||
let directory = agent_test_home("claude-code-explicit-models");
|
||||
let path = directory.join("settings.json");
|
||||
let models = test_agent_models(&["route-model"]);
|
||||
let mut mappings = ClaudeDesktopModelMappings::all("route-model");
|
||||
mappings.startup_model = Some(" custom-startup[1m] ".into());
|
||||
mappings.subagent_model = Some("custom-worker".into());
|
||||
let resolved = resolve_claude_code_model_mappings(
|
||||
AgentClient::ClaudeCode, &models, "route-model", Some(mappings),
|
||||
).unwrap().unwrap();
|
||||
let rendered = build_claude_agent_config(
|
||||
Some(r#"{"model":"user-choice","env":{"KEEP":"yes","ANTHROPIC_MODEL":"old-startup","CLAUDE_CODE_SUBAGENT_MODEL":"old-worker"}}"#),
|
||||
"http://127.0.0.1:8317", "test-key", "route-model", &models, Some(&resolved),
|
||||
).unwrap();
|
||||
let value: serde_json::Value = serde_json::from_str(&rendered).unwrap();
|
||||
assert_eq!(value["model"], "custom-startup[1m]");
|
||||
assert_eq!(value["env"]["CLAUDE_CODE_SUBAGENT_MODEL"], "custom-worker");
|
||||
assert_eq!(value["env"]["ANTHROPIC_DEFAULT_SONNET_MODEL"], "route-model");
|
||||
assert_eq!(value["modelSettings"]["route-model"]["autoCompactWindow"], 1_000_000);
|
||||
assert_eq!(value["model"], "custom-startup[1m]");
|
||||
fs::write(&path, &rendered).unwrap();
|
||||
let mut inspected = inspect_claude_code_model_mappings(&path).unwrap().unwrap();
|
||||
assert_eq!(inspected.startup_model.as_deref(), Some("custom-startup[1m]"));
|
||||
assert_eq!(inspected.subagent_model.as_deref(), Some("custom-worker"));
|
||||
inspected.startup_model = Some(String::new());
|
||||
inspected.subagent_model = Some(String::new());
|
||||
let cleared = build_claude_agent_config(
|
||||
Some(&rendered), "http://127.0.0.1:8317", "test-key", "route-model", &models, Some(&inspected),
|
||||
).unwrap();
|
||||
let value: serde_json::Value = serde_json::from_str(&cleared).unwrap();
|
||||
assert!(value["env"].get("ANTHROPIC_MODEL").is_none());
|
||||
assert!(value["env"].get("CLAUDE_CODE_SUBAGENT_MODEL").is_none());
|
||||
assert!(value.get("model").is_none());
|
||||
assert_eq!(value["env"]["KEEP"], "yes");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn claude_code_role_selection_survives_mapping_changes_without_global_effort_override() {
|
||||
let models = test_agent_models(&["route-a", "route-b"]);
|
||||
let mut mappings = ClaudeDesktopModelMappings::all("route-a");
|
||||
mappings.startup_model = Some("sonnet".into());
|
||||
mappings.subagent_model = Some("haiku".into());
|
||||
for route in ["route-a", "route-b"] {
|
||||
mappings.sonnet = route.into();
|
||||
mappings.sonnet_1m = true;
|
||||
let rendered = build_claude_agent_config(
|
||||
Some(r#"{"model":"user-choice","env":{"CLAUDE_CODE_EFFORT_LEVEL":"max"}}"#),
|
||||
"http://127.0.0.1:8317", "test-key", route, &models, Some(&mappings),
|
||||
).unwrap();
|
||||
let value: serde_json::Value = serde_json::from_str(&rendered).unwrap();
|
||||
assert_eq!(value["model"], "sonnet");
|
||||
assert_eq!(value["env"]["CLAUDE_CODE_SUBAGENT_MODEL"], "haiku");
|
||||
assert_eq!(value["env"]["ANTHROPIC_DEFAULT_SONNET_MODEL"], format!("{route}[1m]"));
|
||||
assert_eq!(value["modelSettings"][route]["autoCompactWindow"], 1_000_000);
|
||||
assert!(value["env"].get("CLAUDE_CODE_EFFORT_LEVEL").is_none());
|
||||
assert_eq!(value["model"], "sonnet");
|
||||
}
|
||||
mappings.sonnet_1m = false;
|
||||
mappings.startup_model = Some("route-a[1m]".into());
|
||||
let resolved = resolve_claude_code_model_mappings(
|
||||
AgentClient::ClaudeCode, &models, "route-a", Some(mappings),
|
||||
).unwrap().unwrap();
|
||||
assert_eq!(resolved.max_context_tokens, 1_000_000);
|
||||
assert_eq!(resolved.startup_model.as_deref(), Some("route-a[1m]"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn claude_agent_config_preserves_existing_fields() {
|
||||
let rendered = build_claude_agent_config(
|
||||
@@ -27,9 +94,8 @@ fn claude_agent_config_preserves_existing_fields() {
|
||||
"1"
|
||||
);
|
||||
assert_eq!(value["env"][CLAUDE_CODE_AUTO_MODE_SERVER_ENV], "0");
|
||||
assert_eq!(value["env"][CLAUDE_CODE_MAX_CONTEXT_TOKENS_ENV], "200000");
|
||||
assert_eq!(value["env"][CLAUDE_AUTOCOMPACT_PCT_OVERRIDE_ENV], "90");
|
||||
assert!(value["env"].get(DISABLE_AUTO_COMPACT_ENV).is_none());
|
||||
assert_eq!(value["autoCompactWindow"], 200_000);
|
||||
assert_eq!(value["autoCompactEnabled"], true);
|
||||
assert!(value["env"].get("CLAUDE_CODE_EFFORT_LEVEL").is_none());
|
||||
assert_eq!(value["env"]["CLAUDE_CODE_SUBAGENT_MODEL"], "claude-test");
|
||||
assert_eq!(
|
||||
@@ -156,6 +222,8 @@ fn claude_code_role_mappings_drive_settings() {
|
||||
disable_auto_compact: false,
|
||||
manage_default_model: true,
|
||||
manage_subagent_model: true,
|
||||
startup_model: None,
|
||||
subagent_model: None,
|
||||
};
|
||||
let models = vec![
|
||||
AgentModelOption {
|
||||
@@ -205,7 +273,7 @@ fn claude_code_role_mappings_drive_settings() {
|
||||
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");
|
||||
assert_eq!(value["autoCompactWindow"], 272_000);
|
||||
assert_eq!(
|
||||
value["env"]["ANTHROPIC_CUSTOM_MODEL_OPTION_NAME"],
|
||||
"gpt-sonnet (272K context)"
|
||||
@@ -274,6 +342,8 @@ fn claude_code_runtime_settings_keep_per_role_1m_suffixes() {
|
||||
disable_auto_compact: true,
|
||||
manage_default_model: true,
|
||||
manage_subagent_model: true,
|
||||
startup_model: None,
|
||||
subagent_model: None,
|
||||
};
|
||||
let models = vec![
|
||||
AgentModelOption {
|
||||
@@ -304,9 +374,8 @@ fn claude_code_runtime_settings_keep_per_role_1m_suffixes() {
|
||||
.unwrap();
|
||||
let value: serde_json::Value = serde_json::from_str(&rendered).unwrap();
|
||||
|
||||
assert_eq!(value["env"][CLAUDE_CODE_MAX_CONTEXT_TOKENS_ENV], "1000000");
|
||||
assert_eq!(value["env"][CLAUDE_AUTOCOMPACT_PCT_OVERRIDE_ENV], "75");
|
||||
assert_eq!(value["env"][DISABLE_AUTO_COMPACT_ENV], "1");
|
||||
assert_eq!(value["autoCompactWindow"], 1_000_000);
|
||||
assert_eq!(value["autoCompactEnabled"], false);
|
||||
assert_eq!(value["env"]["ANTHROPIC_MODEL"], "custom-pro[1m]");
|
||||
assert_eq!(
|
||||
value["env"]["ANTHROPIC_DEFAULT_OPUS_MODEL"],
|
||||
|
||||
@@ -28,6 +28,8 @@ export type AgentModelPickerProps = {
|
||||
allowCustomValue?: boolean;
|
||||
editable?: { label: string; placeholder: string; maxLength: number };
|
||||
menuClassName?: string;
|
||||
emptyOption?: string;
|
||||
preserveValue?: boolean;
|
||||
};
|
||||
|
||||
type AgentModelDropdownLayout = {
|
||||
@@ -48,6 +50,8 @@ export function AgentModelPicker({
|
||||
allowCustomValue = false,
|
||||
editable,
|
||||
menuClassName = '',
|
||||
emptyOption,
|
||||
preserveValue = false,
|
||||
}: AgentModelPickerProps) {
|
||||
const { t } = useI18n();
|
||||
const [open, setOpen] = useState(false);
|
||||
@@ -66,13 +70,16 @@ export function AgentModelPicker({
|
||||
);
|
||||
const choices = useMemo(() => {
|
||||
const options = visibleModels.map((model) => ({ name: model.name, alias: model.alias ?? '' }));
|
||||
if (emptyOption && !search.trim()) options.unshift({ name: '', alias: emptyOption });
|
||||
if (allowCustomValue && search.trim() && !findAgentModel(models, search)) {
|
||||
options.push({ name: search.trim(), alias: t('agents.model.useCustom') });
|
||||
}
|
||||
return options;
|
||||
}, [visibleModels, allowCustomValue, search, models, t]);
|
||||
}, [visibleModels, allowCustomValue, search, models, t, emptyOption]);
|
||||
const selectedModel = findAgentModel(models, value);
|
||||
const selectedName = selectedModel?.name ?? (allowCustomValue || editable ? value.trim() : '');
|
||||
const selectedName = (selectedModel?.name
|
||||
?? (allowCustomValue || editable || preserveValue ? value.trim() : ''))
|
||||
|| (emptyOption && !value.trim() ? emptyOption : '');
|
||||
const selectedAlias = selectedName ? agentModelAlias(models, selectedName) : '';
|
||||
|
||||
const updateDropdownLayout = useCallback(() => {
|
||||
@@ -158,7 +165,7 @@ export function AgentModelPicker({
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
if (!editable) setSearch('');
|
||||
const selectedIndex = (editable ? visibleModels : filterAgentModels(models, '')).findIndex(
|
||||
const selectedIndex = choices.findIndex(
|
||||
(model) => model.name.toLocaleLowerCase() === value.trim().toLocaleLowerCase(),
|
||||
);
|
||||
setActiveIndex(selectedIndex >= 0 ? selectedIndex : 0);
|
||||
@@ -322,8 +329,8 @@ export function AgentModelPicker({
|
||||
onClick={() => choose(choice.name)}
|
||||
>
|
||||
<span>
|
||||
<strong title={choice.name}>{choice.name}</strong>
|
||||
<small>{choice.alias || t('agents.model.available')}</small>
|
||||
<strong title={choice.name}>{choice.name || emptyOption}</strong>
|
||||
{choice.name ? <small>{choice.alias || t('agents.model.available')}</small> : null}
|
||||
</span>
|
||||
{selected ? <Check size={16} aria-hidden /> : null}
|
||||
</button>
|
||||
|
||||
@@ -98,14 +98,13 @@
|
||||
|
||||
.credential-quota-dense .credential-quota-label span {
|
||||
min-width: 0;
|
||||
padding: 2px 5px;
|
||||
overflow: hidden;
|
||||
padding: 2px 7px;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
font-size: 10px;
|
||||
font-weight: 550;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
@@ -137,21 +136,22 @@
|
||||
.credential-quota-dense .credential-quota-empty {
|
||||
margin: 0;
|
||||
color: var(--text-tertiary);
|
||||
font-size: 11px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.credential-quota-dense .credential-quota-label strong {
|
||||
font-size: 12px;
|
||||
font-size: 13px;
|
||||
line-height: 1;
|
||||
font-weight: 750;
|
||||
}
|
||||
|
||||
.credential-quota-dense .credential-quota-track {
|
||||
height: 5px;
|
||||
height: 7px;
|
||||
}
|
||||
|
||||
.credential-quota-dense .credential-quota-row small {
|
||||
color: var(--text-tertiary);
|
||||
font-size: 10px;
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
@@ -167,11 +167,11 @@
|
||||
}
|
||||
|
||||
.credential-quota-dense .credential-quota-heading .credential-quota-reset {
|
||||
min-height: 21px;
|
||||
padding: 2px 6px;
|
||||
min-height: 26px;
|
||||
padding: 3px 8px;
|
||||
margin: 0;
|
||||
border-radius: 4px;
|
||||
font-size: 10px;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import { useI18n } from '../../i18n';
|
||||
import { claudeCodeRoles, claudeCodeRole, claudeCodeModelBase, type ClaudeCodeRoutes } from '../../services/claudeCodeModels';
|
||||
|
||||
type Props = {
|
||||
routes: ClaudeCodeRoutes;
|
||||
startupModel: string;
|
||||
subagentModel: string;
|
||||
disabled: boolean;
|
||||
onRoleChange: (field: 'opus1m' | 'sonnet1m' | 'haiku1m', enabled: boolean) => void;
|
||||
onModelChange: (field: 'startupModel' | 'subagentModel', value: string) => void;
|
||||
};
|
||||
|
||||
export function ClaudeCodeContextOptions({ routes, startupModel, subagentModel, disabled, onRoleChange, onModelChange }: Props) {
|
||||
const { t } = useI18n();
|
||||
return (
|
||||
<fieldset className="claude-code-context-options">
|
||||
<legend>{t('agents.claudeCodeRuntime.extendedContext')}</legend>
|
||||
<p>{t('agents.claudeCodeRuntime.selectionHint')}</p>
|
||||
<div>
|
||||
{claudeCodeRoles.map(role => (
|
||||
<label key={role}>
|
||||
<input type="checkbox" checked={routes[`${role}1m`]} disabled={disabled}
|
||||
onChange={event => onRoleChange(`${role}1m`, event.currentTarget.checked)} />
|
||||
<span>{role[0].toUpperCase() + role.slice(1)}</span>
|
||||
</label>
|
||||
))}
|
||||
{(['startupModel', 'subagentModel'] as const).map(field => {
|
||||
const value = field === 'startupModel' ? startupModel : subagentModel;
|
||||
if (!value.trim() || claudeCodeRole(value)) return null;
|
||||
return <label key={field}>
|
||||
<input type="checkbox" checked={/\[1m\]$/i.test(value.trim())} disabled={disabled}
|
||||
onChange={event => onModelChange(field, claudeCodeModelBase(value) + (event.currentTarget.checked ? '[1m]' : ''))} />
|
||||
<span>{t(`agents.claudeCodeRuntime.${field}`)}</span>
|
||||
</label>;
|
||||
})}
|
||||
</div>
|
||||
</fieldset>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
.claude-code-model-inputs {
|
||||
grid-column: 1 / -1;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.claude-code-model-field {
|
||||
display: grid;
|
||||
align-content: start;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
@container agent-panel (max-width: 560px) {
|
||||
.claude-code-model-inputs {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.claude-code-model-field small { color: var(--theme-817b72); line-height: 1.5; }
|
||||
.claude-code-model-field .agent-model-trigger { width: 100%; border: 1px solid var(--theme-d3d0c9); border-radius: 7px; background: var(--theme-fff); }
|
||||
.claude-code-model-field .agent-model-trigger input { min-width: 0; width: 100%; }
|
||||
.claude-code-model-error { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 12px; border: 1px solid var(--theme-d3d0c9); border-radius: 8px; }
|
||||
.claude-code-model-error p { margin: 4px 0 0; }
|
||||
|
||||
.claude-code-direct-context { display: flex; align-items: center; gap: 8px; font-size: var(--font-size-caption); }
|
||||
.claude-code-selection-hint { grid-column: 1 / -1; color: var(--theme-817b72); line-height: 1.5; }
|
||||
.claude-code-model-inputs .claude-code-model-field .agent-model-trigger {
|
||||
border: 1px solid var(--theme-d3d0c9, #d3d0c9);
|
||||
padding: 6px 10px;
|
||||
border-radius: 7px;
|
||||
}
|
||||
|
||||
.claude-code-context-settings { border-top: 1px solid var(--theme-d3d0c9, #d3d0c9); padding-top: 14px; }
|
||||
.claude-code-context-settings summary { cursor: pointer; font-weight: 600; padding: 6px 0; }
|
||||
.claude-code-context-settings > p, .claude-code-context-options p { font-size: var(--font-size-caption); line-height: 1.5; color: var(--theme-817b72); }
|
||||
.claude-code-context-options { margin: 14px 0 0; padding: 12px; border: 1px solid var(--theme-d3d0c9, #d3d0c9); border-radius: 8px; }
|
||||
.claude-code-context-options > div { display: flex; flex-wrap: wrap; gap: 14px 24px; }
|
||||
.claude-code-context-options label { display: flex; gap: 8px; align-items: center; }
|
||||
.claude-code-group-heading { margin-top: 4px; font-size: var(--font-size-label); }
|
||||
.claude-role-table { display: grid; gap: 10px; min-width: 0; }
|
||||
.claude-role-toolbar { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; }
|
||||
.claude-role-table > p { margin: 0; font-size: var(--font-size-caption); color: var(--theme-817b72); line-height: 1.5; }
|
||||
.claude-role-row { display: grid; grid-template-columns: 80px minmax(0, 1fr) 65px; align-items: center; gap: 12px; padding: 10px; border: 1px solid var(--theme-d3d0c9); border-radius: 8px; }
|
||||
.claude-role-row .agent-model-trigger { width: 100%; }
|
||||
.claude-model-with-context { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; gap: 10px; }
|
||||
.claude-inline-context { display: flex; align-items: center; gap: 6px; white-space: nowrap; }
|
||||
@container agent-panel (max-width: 420px) {
|
||||
.claude-role-row { grid-template-columns: minmax(0, 1fr) auto; }
|
||||
.claude-role-row > strong { grid-column: 1 / -1; }
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import { claudeCodeModelBase, claudeCodeRoles, claudeCodeModelPreview, isKnownClaudeCodeModel, type ClaudeCodeRoutes } from '../../services/claudeCodeModels';
|
||||
import { useI18n } from '../../i18n';
|
||||
import { AgentModelPicker } from '../AgentModelPicker';
|
||||
import type { ModelOption } from '../../services/modelService';
|
||||
import './ClaudeCodeModelInputs.css';
|
||||
|
||||
type Props = {
|
||||
startupModel: string;
|
||||
subagentModel: string;
|
||||
models: ModelOption[];
|
||||
routes: ClaudeCodeRoutes;
|
||||
loading: boolean;
|
||||
disabled: boolean;
|
||||
onChange: (field: 'startupModel' | 'subagentModel', value: string) => void;
|
||||
};
|
||||
|
||||
export function ClaudeCodeModelInputs({ startupModel, subagentModel, models, routes, loading, disabled, onChange }: Props) {
|
||||
const { t } = useI18n();
|
||||
const options = [
|
||||
...claudeCodeRoles.map(role => ({ name: role, alias: t('agents.claudeCodeRuntime.followRole', {
|
||||
role, model: claudeCodeModelPreview(role, routes).model || '—',
|
||||
}) })),
|
||||
...models.filter(model => !claudeCodeRoles.some(role => role === model.name.toLowerCase()))
|
||||
.map(model => ({ ...model, alias: t('agents.claudeCodeRuntime.directOption') })),
|
||||
];
|
||||
return (
|
||||
<div className="claude-code-model-inputs">
|
||||
{(['startupModel', 'subagentModel'] as const).map((field) => {
|
||||
const value = field === 'startupModel' ? startupModel : subagentModel;
|
||||
const preview = claudeCodeModelPreview(value, routes);
|
||||
return (
|
||||
<div className="claude-code-model-field" key={field}>
|
||||
<strong>{t(`agents.claudeCodeRuntime.${field}`)}</strong>
|
||||
<div className="claude-model-with-context">
|
||||
<AgentModelPicker
|
||||
models={options}
|
||||
value={value}
|
||||
onChange={(value) => onChange(field, value)}
|
||||
disabled={disabled}
|
||||
loading={loading}
|
||||
allowCustomValue
|
||||
emptyOption={t(`agents.claudeCodeRuntime.${field}Placeholder`)}
|
||||
editable={{ label: t(`agents.claudeCodeRuntime.${field}`),
|
||||
placeholder: t(`agents.claudeCodeRuntime.${field}Placeholder`), maxLength: 240 }}
|
||||
/>
|
||||
<label className="claude-inline-context">
|
||||
<input type="checkbox" aria-label={`${t(`agents.claudeCodeRuntime.${field}`)} 1M`}
|
||||
checked={preview.context1m} disabled={disabled || !value.trim() || Boolean(preview.role)}
|
||||
onChange={event => onChange(field, claudeCodeModelBase(value) + (event.currentTarget.checked ? '[1m]' : ''))} />
|
||||
1M
|
||||
</label>
|
||||
</div>
|
||||
<small className="claude-code-model-summary" aria-live="polite">
|
||||
{value.trim() ? preview.role
|
||||
? t('agents.claudeCodeRuntime.followRole', { role: preview.role, model: preview.model || '—' })
|
||||
: t('agents.claudeCodeRuntime.directModel', { model: preview.model })
|
||||
: t(`agents.claudeCodeRuntime.${field}Hint`)}
|
||||
</small>
|
||||
{!loading && !isKnownClaudeCodeModel(value, models) ? (
|
||||
<small role="status">{t('agents.claudeCodeRuntime.unverifiedModel')}</small>
|
||||
) : null}
|
||||
</div>
|
||||
); })}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { useI18n } from '../../i18n';
|
||||
import { AgentModelPicker } from '../AgentModelPicker';
|
||||
import type { ModelOption } from '../../services/modelService';
|
||||
import { claudeCodeRoles, type ClaudeCodeRoutes } from '../../services/claudeCodeModels';
|
||||
import './ClaudeCodeModelInputs.css';
|
||||
|
||||
type Role = typeof claudeCodeRoles[number];
|
||||
type Props = {
|
||||
routes: ClaudeCodeRoutes;
|
||||
models: ModelOption[];
|
||||
loading: boolean;
|
||||
error: string;
|
||||
disabled: boolean;
|
||||
onModelChange: (role: Role, value: string) => void;
|
||||
onContextChange: (field: `${Role}1m`, value: boolean) => void;
|
||||
onRefresh: () => void;
|
||||
onApplyAll: () => void;
|
||||
};
|
||||
|
||||
export function ClaudeCodeRoleRows({ routes, models, loading, error, disabled, onModelChange, onContextChange, onRefresh, onApplyAll }: Props) {
|
||||
const { t } = useI18n();
|
||||
return <section className="claude-role-table" aria-label={t('agents.claudeCodeRuntime.roleMappings')}>
|
||||
<div className="claude-role-toolbar">
|
||||
<strong>{t('agents.claudeCodeRuntime.roleMappings')}</strong>
|
||||
<button type="button" className="secondary-button" disabled={disabled || !routes.sonnet}
|
||||
onClick={onApplyAll}>{t('agents.claudeCodeRuntime.applySonnetToRoles')}</button>
|
||||
</div>
|
||||
<p>{t('agents.claudeCodeRuntime.roleRowsHint')}</p>
|
||||
{(['sonnet', 'opus', 'haiku'] as const).map(role => <div className="claude-role-row" key={role} data-role={role}>
|
||||
<strong>{role[0].toUpperCase() + role.slice(1)}</strong>
|
||||
<AgentModelPicker preserveValue models={models} value={routes[role]} loading={loading} error={error}
|
||||
disabled={disabled} onChange={value => onModelChange(role, value)} onRefresh={onRefresh} />
|
||||
<label className="claude-inline-context">
|
||||
<input type="checkbox" checked={routes[`${role}1m`]} disabled={disabled}
|
||||
aria-label={`${role} 1M`} onChange={event => onContextChange(`${role}1m`, event.currentTarget.checked)} />
|
||||
1M
|
||||
</label>
|
||||
</div>)}
|
||||
</section>;
|
||||
}
|
||||
+24
-1
@@ -1455,7 +1455,7 @@ export const jaOverrides = {
|
||||
'agents.claudeDesktopMapping.error.duplicate': 'エイリアスまたは元モデル ID が重複しています。各行に異なる ID を指定してください。',
|
||||
'agents.claudeCodeMapping.title': 'Claude Code モデル → 上流モデル',
|
||||
'agents.claudeCodeMapping.description': 'タスク役割ごとに上流モデルを選び、共通のコンテキストと自動圧縮ポリシーを設定します。',
|
||||
'agents.claudeCodeRuntime.maxContextTokens': '最大コンテキスト',
|
||||
'agents.claudeCodeRuntime.maxContextTokens': '全体のコンテキスト上書き',
|
||||
'agents.claudeCodeRuntime.maxContextTokensHint': 'Claude Code の既定値は 200000 です。上流モデルの上限を超えないでください。',
|
||||
'agents.claudeCodeRuntime.context1mHint': 'このマッピングで [1m] モデルを使用し、最大コンテキストを 1000000 にします。3 つすべてを無効にすると 200000 に戻ります。',
|
||||
'agents.claudeCodeRuntime.autoCompactPct': '自動圧縮の開始率',
|
||||
@@ -1466,6 +1466,29 @@ export const jaOverrides = {
|
||||
'agents.claudeCodeRuntime.manageDefaultModelHint': '無効にすると ANTHROPIC_MODEL と model を上書きせず、/model で既定モデルを保存できます。',
|
||||
'agents.claudeCodeRuntime.manageSubagentModel': 'Subagent モデルを CPA が固定',
|
||||
'agents.claudeCodeRuntime.manageSubagentModelHint': '無効にすると CLAUDE_CODE_SUBAGENT_MODEL を設定せず、未指定の Subagent はメインモデルを継承します。',
|
||||
'agents.claudeCodeRuntime.connectionHint': 'CPA のモデル一覧を更新できません。カーネルと接続を確認して再試行してください。保存済みモデルと編集内容は保持されます。',
|
||||
'agents.claudeCodeRuntime.followRole': '{role} に追従 → {model}',
|
||||
'agents.claudeCodeRuntime.directOption': 'CPA モデルを直接指定',
|
||||
'agents.claudeCodeRuntime.directModel': '{model} に固定し、ロールの変更には追従しません。',
|
||||
'agents.claudeCodeRuntime.direct1m': 'このモデルで 1M コンテキストを使用',
|
||||
'agents.claudeCodeRuntime.unverifiedModel': '現在の CPA 一覧にないモデルです。ID またはエイリアスが利用可能か確認してください。',
|
||||
'agents.claudeCodeRuntime.selectionHint': 'ロール選択はロールの 1M 設定に従います。直接指定では個別に 1M を設定します。上流モデルの対応が必要です。',
|
||||
'agents.claudeCodeRuntime.advancedContext': 'コンテキストと自動圧縮(詳細)',
|
||||
'agents.claudeCodeRuntime.applySonnetToRoles': 'Sonnet を全ロールに適用',
|
||||
'agents.claudeCodeRuntime.roleRowsHint': '各行でリクエストモデルと 1M を設定します。起動・サブエージェントでロールを選ぶと、その行の設定に従います。',
|
||||
'agents.claudeCodeRuntime.roleMappings': 'ロールのマッピング',
|
||||
'agents.claudeCodeRuntime.sharedRoleModel': '共通ロールモデル',
|
||||
'agents.claudeCodeRuntime.sharedRoleModelHint': '通常は Opus、Sonnet、Haiku に同じ CPA モデルを使用します。個別ルートが必要な場合だけ展開してください。',
|
||||
'agents.claudeCodeRuntime.showRoleMappings': 'ロールを個別設定',
|
||||
'agents.claudeCodeRuntime.hideRoleMappings': '個別設定を閉じる',
|
||||
'agents.claudeCodeRuntime.extendedContext': '拡張コンテキスト指定(1M)',
|
||||
'agents.claudeCodeRuntime.sharedContextHint': 'ウィンドウ値は全体の上書きで、モデル別の制限ではありません。1M は拡張コンテキストの指定であり、上流モデルの容量を増やしません。',
|
||||
'agents.claudeCodeRuntime.startupModel': '起動モデル',
|
||||
'agents.claudeCodeRuntime.startupModelPlaceholder': 'Claude Code の既定モデルを使用',
|
||||
'agents.claudeCodeRuntime.startupModelHint': 'CPA のモデルを選択するか ID を入力します。空欄では Claude Code の既定の選択を維持します。',
|
||||
'agents.claudeCodeRuntime.subagentModel': 'Subagent モデル',
|
||||
'agents.claudeCodeRuntime.subagentModelPlaceholder': '既定の継承動作を使用',
|
||||
'agents.claudeCodeRuntime.subagentModelHint': 'CPA のモデルを選択するか ID を入力します。空欄ではサブエージェントの既定動作を維持します。',
|
||||
'agents.claudeDesktopMapping.customMapping': 'モデルエイリアスを使用',
|
||||
'agents.claudeDesktopMapping.customMappingHint': '有効にするとエイリアスモデルのみ、無効にするとエイリアス未設定の元モデルのみ表示します。',
|
||||
'agents.claudeDesktopMapping.opus': 'Opus',
|
||||
|
||||
+24
-1
@@ -1421,7 +1421,7 @@ export const en: Record<MessageKey, string> = {
|
||||
'agents.claudeDesktopMapping.error.duplicate': 'Duplicate alias or original model ID. Use a different ID for each row.',
|
||||
'agents.claudeCodeMapping.title': 'Claude Code Model → Upstream Model',
|
||||
'agents.claudeCodeMapping.description': 'Choose the upstream model for each task role and configure one shared context and auto-compact policy.',
|
||||
'agents.claudeCodeRuntime.maxContextTokens': 'Maximum context',
|
||||
'agents.claudeCodeRuntime.maxContextTokens': 'Global context override',
|
||||
'agents.claudeCodeRuntime.maxContextTokensHint': 'Claude Code defaults to 200000. Do not exceed the upstream model limit.',
|
||||
'agents.claudeCodeRuntime.context1mHint': 'Uses the [1m] model for this mapping and sets the maximum context to 1000000. It returns to 200000 after all three mappings are off.',
|
||||
'agents.claudeCodeRuntime.autoCompactPct': 'Auto-compact trigger',
|
||||
@@ -1432,6 +1432,29 @@ export const en: Record<MessageKey, string> = {
|
||||
'agents.claudeCodeRuntime.manageDefaultModelHint': 'When off, CPA leaves ANTHROPIC_MODEL and the root model untouched so /model can save the default.',
|
||||
'agents.claudeCodeRuntime.manageSubagentModel': 'Let CPA pin the Subagent model',
|
||||
'agents.claudeCodeRuntime.manageSubagentModelHint': 'When off, CPA leaves CLAUDE_CODE_SUBAGENT_MODEL unset so unspecified Subagents inherit the main model.',
|
||||
'agents.claudeCodeRuntime.connectionHint': 'Could not refresh CPA models. Check the kernel and connection, then retry. Saved models and edits are preserved.',
|
||||
'agents.claudeCodeRuntime.followRole': 'Follow {role} → {model}',
|
||||
'agents.claudeCodeRuntime.directOption': 'Direct CPA model',
|
||||
'agents.claudeCodeRuntime.directModel': 'Pinned to {model}; does not follow role mapping changes.',
|
||||
'agents.claudeCodeRuntime.direct1m': 'Use 1M context for this model',
|
||||
'agents.claudeCodeRuntime.unverifiedModel': 'This model is not in the current CPA list. Check that the ID or alias is available.',
|
||||
'agents.claudeCodeRuntime.selectionHint': 'Role selections follow their role’s 1M setting. Direct models have a separate 1M setting. The upstream must support the context size.',
|
||||
'agents.claudeCodeRuntime.advancedContext': 'Context and auto-compaction (advanced)',
|
||||
'agents.claudeCodeRuntime.applySonnetToRoles': 'Apply Sonnet to all roles',
|
||||
'agents.claudeCodeRuntime.roleRowsHint': 'Set each role’s request model and 1M flag together. Session and subagent role selections follow the corresponding row.',
|
||||
'agents.claudeCodeRuntime.roleMappings': 'Role mappings',
|
||||
'agents.claudeCodeRuntime.sharedRoleModel': 'Shared role model',
|
||||
'agents.claudeCodeRuntime.sharedRoleModelHint': 'Use one CPA model for Opus, Sonnet, and Haiku by default. Expand this only when you need separate routes.',
|
||||
'agents.claudeCodeRuntime.showRoleMappings': 'Configure roles separately',
|
||||
'agents.claudeCodeRuntime.hideRoleMappings': 'Hide separate role settings',
|
||||
'agents.claudeCodeRuntime.extendedContext': 'Extended context flag (1M)',
|
||||
'agents.claudeCodeRuntime.sharedContextHint': 'The window value is a global override, not a per-model limit. The 1M options send an extended-context flag; they do not increase upstream capacity.',
|
||||
'agents.claudeCodeRuntime.startupModel': 'Startup model',
|
||||
'agents.claudeCodeRuntime.startupModelPlaceholder': 'Use Claude Code default',
|
||||
'agents.claudeCodeRuntime.startupModelHint': 'Select a CPA model or enter a model ID. Leave empty to keep the Claude Code default.',
|
||||
'agents.claudeCodeRuntime.subagentModel': 'Subagent model',
|
||||
'agents.claudeCodeRuntime.subagentModelPlaceholder': 'Use default inheritance',
|
||||
'agents.claudeCodeRuntime.subagentModelHint': 'Select a CPA model or enter a model ID. Leave empty to keep the default subagent behavior.',
|
||||
'agents.claudeDesktopMapping.customMapping': 'Use Model Aliases',
|
||||
'agents.claudeDesktopMapping.customMappingHint': 'When enabled, only aliased models are shown; when disabled, only original models without aliases are shown.',
|
||||
'agents.claudeDesktopMapping.opus': 'Opus',
|
||||
|
||||
@@ -1420,7 +1420,7 @@ export const zhCN = {
|
||||
'agents.claudeDesktopMapping.error.duplicate': '别名或原模型 ID 重复,请为每一行使用不同的 ID。',
|
||||
'agents.claudeCodeMapping.title': 'Claude Code 模型 → 上游模型',
|
||||
'agents.claudeCodeMapping.description': '分别为不同任务角色选择上游模型,并统一配置上下文窗口与自动压缩。',
|
||||
'agents.claudeCodeRuntime.maxContextTokens': '最大窗口',
|
||||
'agents.claudeCodeRuntime.maxContextTokens': '全局窗口覆盖',
|
||||
'agents.claudeCodeRuntime.maxContextTokensHint': 'Claude Code 默认 200000;请勿超过上游模型实际支持的窗口。',
|
||||
'agents.claudeCodeRuntime.context1mHint': '为此映射使用 [1m] 模型并将最大窗口设为 1000000;三个映射均关闭后恢复为 200000。',
|
||||
'agents.claudeCodeRuntime.autoCompactPct': '触发压缩百分比',
|
||||
@@ -1431,6 +1431,29 @@ export const zhCN = {
|
||||
'agents.claudeCodeRuntime.manageDefaultModelHint': '关闭后不再写入 ANTHROPIC_MODEL 或根级 model,Claude Code 可通过 /model 保存默认模型。',
|
||||
'agents.claudeCodeRuntime.manageSubagentModel': '由 CPA 固定 Subagent 模型',
|
||||
'agents.claudeCodeRuntime.manageSubagentModelHint': '关闭后不再写入 CLAUDE_CODE_SUBAGENT_MODEL,未指定模型的 Subagent 继承主会话模型。',
|
||||
'agents.claudeCodeRuntime.connectionHint': '无法刷新 CPA 模型列表。请检查内核运行状态与连接后重试;已保存的模型和编辑内容会保留。',
|
||||
'agents.claudeCodeRuntime.followRole': '跟随 {role} → {model}',
|
||||
'agents.claudeCodeRuntime.directOption': '直接指定 CPA 模型',
|
||||
'agents.claudeCodeRuntime.directModel': '固定使用 {model},不随角色映射变化。',
|
||||
'agents.claudeCodeRuntime.direct1m': '此模型使用 1M 上下文',
|
||||
'agents.claudeCodeRuntime.unverifiedModel': '当前 CPA 列表中未找到此模型,请确认 ID 或别名可用。',
|
||||
'agents.claudeCodeRuntime.selectionHint': '跟随角色时使用该角色的 1M 设置;直接指定模型时单独设置 1M。上游须支持对应窗口。',
|
||||
'agents.claudeCodeRuntime.advancedContext': '上下文与自动压缩(高级)',
|
||||
'agents.claudeCodeRuntime.applySonnetToRoles': '将 Sonnet 应用到全部角色',
|
||||
'agents.claudeCodeRuntime.roleRowsHint': '每行设置一个角色的请求模型和 1M 标记;启动模型或子代理选择角色时,跟随对应行的设置。',
|
||||
'agents.claudeCodeRuntime.roleMappings': '角色映射',
|
||||
'agents.claudeCodeRuntime.sharedRoleModel': '统一角色模型',
|
||||
'agents.claudeCodeRuntime.sharedRoleModelHint': '默认让 Opus、Sonnet、Haiku 共用一个 CPA 模型;需要分流时再展开分别设置。',
|
||||
'agents.claudeCodeRuntime.showRoleMappings': '分别设置角色',
|
||||
'agents.claudeCodeRuntime.hideRoleMappings': '收起分别设置',
|
||||
'agents.claudeCodeRuntime.extendedContext': '扩展上下文标记(1M)',
|
||||
'agents.claudeCodeRuntime.sharedContextHint': '这里的窗口数值是全局覆盖,不是每个模型的独立窗口。1M 选项发送扩展上下文标记;它不会增加上游模型的实际容量。',
|
||||
'agents.claudeCodeRuntime.startupModel': '启动模型',
|
||||
'agents.claudeCodeRuntime.startupModelPlaceholder': '使用 Claude Code 默认模型',
|
||||
'agents.claudeCodeRuntime.startupModelHint': '选择 CPA 模型或输入模型 ID;留空保留 Claude Code 的默认选择。',
|
||||
'agents.claudeCodeRuntime.subagentModel': 'Subagent 模型',
|
||||
'agents.claudeCodeRuntime.subagentModelPlaceholder': '使用默认继承行为',
|
||||
'agents.claudeCodeRuntime.subagentModelHint': '选择 CPA 模型或输入模型 ID;留空不覆盖子代理的默认模型。',
|
||||
'agents.claudeDesktopMapping.customMapping': '使用模型别名',
|
||||
'agents.claudeDesktopMapping.customMappingHint': '开启后只显示别名模型;关闭后只显示未设置别名的原始模型。',
|
||||
'agents.claudeDesktopMapping.opus': 'Opus',
|
||||
|
||||
+59
-91
@@ -1,3 +1,5 @@
|
||||
import { ClaudeCodeRoleRows } from '../components/claude-code/ClaudeCodeRoleRows';
|
||||
import { ClaudeCodeModelInputs } from '../components/claude-code/ClaudeCodeModelInputs';
|
||||
import { MessageNotice } from '../appNotice';
|
||||
import {
|
||||
useCallback,
|
||||
@@ -159,6 +161,8 @@ type ClaudeModelMappings = {
|
||||
disableAutoCompact: boolean;
|
||||
manageDefaultModel: boolean;
|
||||
manageSubagentModel: boolean;
|
||||
startupModel: string;
|
||||
subagentModel: string;
|
||||
};
|
||||
|
||||
type AgentFormValues = {
|
||||
@@ -191,6 +195,8 @@ const createClaudeModelMappings = (model: string): ClaudeModelMappings => ({
|
||||
disableAutoCompact: false,
|
||||
manageDefaultModel: true,
|
||||
manageSubagentModel: true,
|
||||
startupModel: model,
|
||||
subagentModel: model,
|
||||
});
|
||||
|
||||
const createClaudeModelMappingsByClient = (): Record<
|
||||
@@ -862,12 +868,12 @@ export function AgentsPage({ embedded = false, onConfigurationApplied }: AgentsP
|
||||
: activeStatus?.pluginVersion ?? undefined;
|
||||
|
||||
useEffect(() => {
|
||||
if (!isClaudeModelMappingClient || (selected !== 'claude-desktop' && !selectedModel)) return;
|
||||
if (!isClaudeModelMappingClient) return;
|
||||
const appliedMappings = selected === 'claude-code'
|
||||
? activeStatus?.claudeCodeModelMappings
|
||||
: activeStatus?.claudeDesktopModelMappings;
|
||||
const dirty = claudeModelMappingsDirtyRef.current[selected];
|
||||
if (!dirty && selected === 'claude-code') {
|
||||
if (!dirty && selected === 'claude-code' && models.length > 0) {
|
||||
const appliedModels = appliedMappings
|
||||
? claudeMappingRoles
|
||||
.map((role) => findAgentModel(models, appliedMappings[role.key]))
|
||||
@@ -891,9 +897,9 @@ export function AgentsPage({ embedded = false, onConfigurationApplied }: AgentsP
|
||||
const next: ClaudeModelMappings = {
|
||||
...(selected === 'claude-desktop' ? { desktopModels: dirty && source.desktopModels
|
||||
? source.desktopModels : appliedMappings ? desktopModelEntries(source) : createDefaultDesktopModels() } : {}),
|
||||
opus: findAgentModel(models, source.opus)?.name ?? (selected === 'claude-desktop' ? '' : selectedModel),
|
||||
sonnet: findAgentModel(models, source.sonnet)?.name ?? (selected === 'claude-desktop' ? '' : selectedModel),
|
||||
haiku: findAgentModel(models, source.haiku)?.name ?? (selected === 'claude-desktop' ? '' : selectedModel),
|
||||
opus: findAgentModel(models, source.opus)?.name ?? (selected === 'claude-code' ? source.opus || selectedModel : ''),
|
||||
sonnet: findAgentModel(models, source.sonnet)?.name ?? (selected === 'claude-code' ? source.sonnet || selectedModel : ''),
|
||||
haiku: findAgentModel(models, source.haiku)?.name ?? (selected === 'claude-code' ? source.haiku || selectedModel : ''),
|
||||
opus1m: Boolean(source.opus1m),
|
||||
sonnet1m: Boolean(source.sonnet1m),
|
||||
haiku1m: Boolean(source.haiku1m),
|
||||
@@ -902,6 +908,8 @@ export function AgentsPage({ embedded = false, onConfigurationApplied }: AgentsP
|
||||
disableAutoCompact: Boolean(source.disableAutoCompact),
|
||||
manageDefaultModel: source.manageDefaultModel !== false,
|
||||
manageSubagentModel: source.manageSubagentModel !== false,
|
||||
startupModel: source.startupModel ?? (source.manageDefaultModel === false ? '' : source.sonnet || ''),
|
||||
subagentModel: source.subagentModel ?? (source.manageSubagentModel === false ? '' : source.haiku || ''),
|
||||
};
|
||||
return sameAgentModelMappings(currentClientDraft, next)
|
||||
? current
|
||||
@@ -1088,7 +1096,8 @@ export function AgentsPage({ embedded = false, onConfigurationApplied }: AgentsP
|
||||
editClaudeModelMappings((current) => {
|
||||
const next = { ...current, [preference]: enabled };
|
||||
if (selected === 'claude-code') {
|
||||
const any1mEnabled = next.opus1m || next.sonnet1m || next.haiku1m;
|
||||
const any1mEnabled = next.opus1m || next.sonnet1m || next.haiku1m
|
||||
|| [next.startupModel, next.subagentModel].some(value => /\[1m\]$/i.test(value.trim()));
|
||||
next.maxContextTokens = any1mEnabled
|
||||
? 1_000_000
|
||||
: DEFAULT_CLAUDE_CODE_MAX_CONTEXT_TOKENS;
|
||||
@@ -1170,8 +1179,10 @@ export function AgentsPage({ embedded = false, onConfigurationApplied }: AgentsP
|
||||
resolved.maxContextTokens = claudeModelMappingsDraft.maxContextTokens;
|
||||
resolved.autoCompactPct = claudeModelMappingsDraft.autoCompactPct;
|
||||
resolved.disableAutoCompact = claudeModelMappingsDraft.disableAutoCompact;
|
||||
resolved.manageDefaultModel = claudeModelMappingsDraft.manageDefaultModel;
|
||||
resolved.manageSubagentModel = claudeModelMappingsDraft.manageSubagentModel;
|
||||
resolved.startupModel = claudeModelMappingsDraft.startupModel.trim();
|
||||
resolved.subagentModel = claudeModelMappingsDraft.subagentModel.trim();
|
||||
resolved.manageDefaultModel = resolved.startupModel.length > 0;
|
||||
resolved.manageSubagentModel = resolved.subagentModel.length > 0;
|
||||
return resolved;
|
||||
};
|
||||
|
||||
@@ -1673,7 +1684,7 @@ export function AgentsPage({ embedded = false, onConfigurationApplied }: AgentsP
|
||||
status={modelLoading || !activeStatus ? t('agents.modify.checking')
|
||||
: activeStatus.modificationState === 'invalid' ? t('agents.modify.invalidState') : ''}
|
||||
description={modificationDescription} />;
|
||||
const configurationErrorMessage = configurationError || (!nativeOauth ? modelSelectionError || modelError : '');
|
||||
const configurationErrorMessage = configurationError || (!nativeOauth ? modelSelectionError || (selected === 'claude-code' ? '' : modelError) : '');
|
||||
|
||||
const editDesktopEntries = (entries: ClaudeDesktopModelMapping[]) => {
|
||||
setModelSelectionError('');
|
||||
@@ -2047,7 +2058,44 @@ export function AgentsPage({ embedded = false, onConfigurationApplied }: AgentsP
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
{selected === 'claude-code' ? (
|
||||
<ClaudeCodeModelInputs
|
||||
routes={claudeModelMappingsDraft}
|
||||
models={models}
|
||||
loading={modelLoading}
|
||||
startupModel={claudeModelMappingsDraft.startupModel}
|
||||
subagentModel={claudeModelMappingsDraft.subagentModel}
|
||||
disabled={busy || loading}
|
||||
onChange={(field, value) => editClaudeModelMappings((current) => ({
|
||||
...current,
|
||||
[field]: value,
|
||||
maxContextTokens: /\[1m\]$/i.test(value.trim()) ? 1_000_000
|
||||
: /\[1m\]$/i.test(current[field].trim()) && current.maxContextTokens === 1_000_000
|
||||
&& !current.opus1m && !current.sonnet1m && !current.haiku1m
|
||||
&& !/\[1m\]$/i.test(current[field === 'startupModel' ? 'subagentModel' : 'startupModel'].trim())
|
||||
? DEFAULT_CLAUDE_CODE_MAX_CONTEXT_TOKENS : current.maxContextTokens,
|
||||
[field === 'startupModel' ? 'manageDefaultModel' : 'manageSubagentModel']: value.trim().length > 0,
|
||||
}))}
|
||||
/>
|
||||
{modelError ? (
|
||||
<div className="claude-code-model-error" role="status">
|
||||
<div><strong>{t('agents.model.loadFailed')}</strong><p>{t('agents.claudeCodeRuntime.connectionHint')}</p></div>
|
||||
<button type="button" className="secondary-button" onClick={refreshModels} disabled={modelLoading}>
|
||||
{t('agents.model.refresh')}
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
<ClaudeCodeRoleRows routes={claudeModelMappingsDraft} models={claudeMappingModels}
|
||||
loading={modelLoading} error={modelError}
|
||||
disabled={busy || !activeStatus?.installed || !activeStatus.supportedPlatform}
|
||||
onModelChange={selectClaudeModelMapping} onContextChange={changeClaude1mPreference}
|
||||
onRefresh={refreshModels}
|
||||
onApplyAll={() => editClaudeModelMappings(current => ({ ...current,
|
||||
opus: current.sonnet, haiku: current.sonnet,
|
||||
opus1m: current.sonnet1m, haiku1m: current.sonnet1m,
|
||||
}))} />
|
||||
<details className="claude-code-context-settings">
|
||||
<summary>{t('agents.claudeCodeRuntime.advancedContext')}</summary>
|
||||
<p>{t('agents.claudeCodeRuntime.sharedContextHint')}</p>
|
||||
<div className="agent-claude-code-runtime-settings">
|
||||
<div className="agent-claude-code-number-field">
|
||||
<span>{t('agents.claudeCodeRuntime.maxContextTokens')}</span>
|
||||
@@ -2111,88 +2159,8 @@ export function AgentsPage({ embedded = false, onConfigurationApplied }: AgentsP
|
||||
<span className="switch-track" />
|
||||
</span>
|
||||
</label>
|
||||
<label
|
||||
className="agent-claude-code-disable-compact"
|
||||
title={t('agents.claudeCodeRuntime.manageDefaultModelHint')}
|
||||
>
|
||||
<span>
|
||||
<strong>{t('agents.claudeCodeRuntime.manageDefaultModel')}</strong>
|
||||
<small>{t('agents.claudeCodeRuntime.manageDefaultModelHint')}</small>
|
||||
</span>
|
||||
<span className="switch-control">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={claudeModelMappingsDraft.manageDefaultModel}
|
||||
onChange={(event) => editClaudeModelMappings((current) => ({
|
||||
...current,
|
||||
manageDefaultModel: event.currentTarget.checked,
|
||||
}))}
|
||||
disabled={busy || loading || modelLoading}
|
||||
/>
|
||||
<span className="switch-track" />
|
||||
</span>
|
||||
</label>
|
||||
<label
|
||||
className="agent-claude-code-disable-compact"
|
||||
title={t('agents.claudeCodeRuntime.manageSubagentModelHint')}
|
||||
>
|
||||
<span>
|
||||
<strong>{t('agents.claudeCodeRuntime.manageSubagentModel')}</strong>
|
||||
<small>{t('agents.claudeCodeRuntime.manageSubagentModelHint')}</small>
|
||||
</span>
|
||||
<span className="switch-control">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={claudeModelMappingsDraft.manageSubagentModel}
|
||||
onChange={(event) => editClaudeModelMappings((current) => ({
|
||||
...current,
|
||||
manageSubagentModel: event.currentTarget.checked,
|
||||
}))}
|
||||
disabled={busy || loading || modelLoading}
|
||||
/>
|
||||
<span className="switch-track" />
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="agent-claude-desktop-mapping-grid">
|
||||
{claudeMappingRoles.map((role) => (
|
||||
<div className="agent-claude-desktop-mapping-row" key={role.key}>
|
||||
<div className="agent-claude-mapping-card-heading">
|
||||
<strong>{t(role.labelKey)}</strong>
|
||||
<label
|
||||
className="agent-claude-context-toggle"
|
||||
title={t(selected === 'claude-code'
|
||||
? 'agents.claudeCodeRuntime.context1mHint'
|
||||
: 'agents.claudeMapping.context1mHint')}
|
||||
>
|
||||
<span>{t('agents.claudeMapping.context1m')}</span>
|
||||
<span className="switch-control">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={claudeModelMappingsDraft[role.contextKey]}
|
||||
onChange={(event) => changeClaude1mPreference(
|
||||
role.contextKey,
|
||||
event.currentTarget.checked,
|
||||
)}
|
||||
disabled={busy || loading || modelLoading}
|
||||
/>
|
||||
<span className="switch-track" />
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<AgentModelPicker
|
||||
models={claudeMappingModels}
|
||||
value={claudeModelMappingsDraft[role.key]}
|
||||
loading={modelLoading}
|
||||
error={modelError}
|
||||
disabled={busy || !activeStatus?.installed || !activeStatus.supportedPlatform}
|
||||
onChange={(value) => selectClaudeModelMapping(role.key, value)}
|
||||
onRefresh={refreshModels}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</details>
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
.auth-files-heading-actions > .auth-toolbar-quiet { border-color: transparent; background: transparent; color: var(--auth-muted); }
|
||||
.auth-files-heading-actions > .auth-toolbar-quiet:hover:not(:disabled) { background: var(--bg-hover); color: var(--auth-text); }
|
||||
.auth-files-page .auth-files-panel {
|
||||
--auth-list-columns: minmax(160px, 1.15fr) 66px 88px 110px 152px minmax(220px, 3fr) 166px;
|
||||
--auth-list-columns: minmax(168px, 1.1fr) 72px 92px 116px 156px minmax(240px, 2.4fr) 248px;
|
||||
padding: 0;
|
||||
min-height: 0;
|
||||
border: 0;
|
||||
@@ -41,36 +41,36 @@
|
||||
.auth-files-page .auth-files-toolbar select:focus-visible { outline: 2px solid var(--auth-accent); outline-offset: 1px; }
|
||||
.auth-file-table-scroll { min-width: 0; overflow-x: auto; padding: 0 0 2px; }
|
||||
.auth-file-list-head, .auth-credential-row { display: grid; grid-template-columns: var(--auth-list-columns); align-items: center; column-gap: 10px; }
|
||||
.auth-file-list-head { min-width: 1148px; min-height: 36px; padding: 2px 13px 0; color: var(--auth-muted); font-size: 12px; font-weight: 700; }
|
||||
.auth-file-list-head { min-width: 1280px; min-height: 38px; padding: 4px 14px 0; color: var(--auth-muted); font-size: 13px; font-weight: 700; }
|
||||
.auth-file-list-head > span { min-width: 0; line-height: 1.25; text-align: center; }
|
||||
.auth-file-list-head > :first-child, .auth-file-list-head > :nth-child(6) { text-align: left; }
|
||||
.auth-file-card-grid { display: grid; grid-template-columns: minmax(0, 1fr); min-width: 1148px; gap: 8px; }
|
||||
.auth-file-card { min-width: 0; padding: 0 12px; border: 1px solid var(--auth-border); border-radius: 8px; background: var(--bg-card); }
|
||||
.auth-file-card-grid { display: grid; grid-template-columns: minmax(0, 1fr); min-width: 1280px; gap: 10px; }
|
||||
.auth-file-card { min-width: 0; padding: 0 14px; border: 1px solid var(--auth-border); border-radius: 10px; background: var(--bg-card); }
|
||||
.auth-file-card:hover { border-color: color-mix(in srgb, var(--auth-accent) 24%, var(--auth-border)); }
|
||||
.auth-credential-row { min-height: 102px; padding: 10px 0; }
|
||||
.auth-credential-row { min-height: 108px; padding: 12px 0; }
|
||||
.auth-list-cell { min-width: 0; }
|
||||
.auth-card-header { display: flex; gap: 10px; align-items: center; min-width: 0; }
|
||||
.auth-card-header .provider-logo { width: 35px; height: 35px; object-fit: contain; flex-shrink: 0; }
|
||||
[data-theme="dark"] .auth-card-header .provider-logo[data-provider="xai"] { filter: invert(1); }
|
||||
.auth-card-identity { min-width: 0; flex: 1; display: grid; gap: 5px; }
|
||||
.auth-card-identity strong { color: var(--auth-text); font-size: 13px; font-weight: 750; line-height: 1.3; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.auth-card-filename, .auth-card-provider { color: var(--auth-muted); font-size: 11px; line-height: 1.35; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.auth-card-identity strong { color: var(--auth-text); font-size: 14px; font-weight: 750; line-height: 1.3; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.auth-card-filename, .auth-card-provider { color: var(--auth-muted); font-size: 12px; line-height: 1.35; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.auth-list-plan, .auth-list-status { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 8px; text-align: center; }
|
||||
.auth-list-plan > span { max-width: 100%; color: var(--auth-text); font-size: 12px; font-weight: 600; text-transform: capitalize; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.auth-list-plan small, .auth-list-status small { color: var(--auth-muted); font-size: 11px; line-height: 1.25; }
|
||||
.auth-list-plan > span { max-width: 100%; color: var(--auth-text); font-size: 13px; font-weight: 600; text-transform: capitalize; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.auth-list-plan small, .auth-list-status small { color: var(--auth-muted); font-size: 12px; line-height: 1.3; }
|
||||
.auth-list-status small { font-weight: 600; }
|
||||
.auth-status-badge { display: block; max-width: 100%; padding: 4px 8px; border-radius: 7px; background: var(--bg-secondary); color: var(--auth-muted); font-size: 12px; line-height: 1.4; font-weight: 700; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.auth-status-badge { display: block; max-width: 100%; padding: 4px 9px; border-radius: 7px; background: var(--bg-secondary); color: var(--auth-muted); font-size: 13px; line-height: 1.4; font-weight: 700; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.auth-status-badge.success { color: var(--success-color); background: color-mix(in srgb, var(--success-color) 8%, var(--bg-card)); }
|
||||
.auth-status-badge.warning { color: var(--amber-text); background: color-mix(in srgb, var(--amber-text) 8%, var(--bg-card)); }
|
||||
.auth-status-badge.error { color: var(--failure-badge-text); background: color-mix(in srgb, var(--failure-badge-text) 7%, var(--bg-card)); }
|
||||
.auth-status-badge.info { color: var(--auth-accent); background: color-mix(in srgb, var(--auth-accent) 8%, var(--bg-card)); }
|
||||
.auth-list-muted { color: var(--auth-muted); font-size: 11px; }
|
||||
.auth-list-muted { color: var(--auth-muted); font-size: 12px; }
|
||||
.auth-list-recent .auth-file-requests { gap: 9px; }
|
||||
.auth-list-recent .auth-file-requests-latest { color: var(--auth-muted); font-size: 11px; font-weight: 650; text-align: center; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.auth-list-recent .auth-file-requests-latest { color: var(--auth-muted); font-size: 12px; font-weight: 650; text-align: center; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.auth-list-recent .auth-file-requests-blocks { gap: 2px; padding: 0; }
|
||||
.auth-list-recent .auth-file-request-block { height: 16px; border-radius: 2px; }
|
||||
.auth-list-recent .auth-file-requests-timeline { justify-content: center; gap: 5px; font-size: 10px; }
|
||||
.auth-list-recent .auth-file-requests-unavailable { font-size: 10px; line-height: 1.4; text-align: center; color: var(--auth-muted); }
|
||||
.auth-list-recent .auth-file-requests-timeline { justify-content: center; gap: 5px; font-size: 11px; }
|
||||
.auth-list-recent .auth-file-requests-unavailable { font-size: 12px; line-height: 1.4; text-align: center; color: var(--auth-muted); }
|
||||
.auth-list-usage .auth-file-usage { color: var(--auth-text); }
|
||||
.auth-list-usage .auth-file-usage-metrics { grid-template-columns: max-content max-content; grid-template-rows: repeat(2, 20px); gap: 0 18px; justify-content: center; align-content: center; }
|
||||
.auth-list-usage .auth-file-usage-metric { gap: 5px; }
|
||||
@@ -78,22 +78,22 @@
|
||||
.auth-list-usage .auth-file-usage-metric svg { width: 12px; height: 12px; padding: 2px; }
|
||||
.auth-list-usage .auth-file-usage-metric svg { width: 12px; height: 12px; padding: 2px; border-radius: 4px; }
|
||||
.auth-list-usage .auth-file-usage-metric.total svg { color: var(--auth-accent); background: color-mix(in srgb, var(--auth-accent) 9%, var(--bg-card)); }
|
||||
.auth-list-usage .auth-file-usage-metric strong { font-size: 12.75px; line-height: 1.2; font-weight: 750; }
|
||||
.auth-list-usage .auth-file-usage-metric strong { font-size: 13px; line-height: 1.2; font-weight: 750; }
|
||||
.auth-list-usage .auth-file-usage-scope { margin-top: 6px; color: var(--auth-muted); text-align: right; font-size: 10px; line-height: 1.2; }
|
||||
.auth-card-actions { display: grid; grid-template-columns: 98px 54px; align-items: center; justify-content: center; gap: 12px; }
|
||||
.auth-list-icon-actions { display: grid; grid-template-columns: repeat(3, 30px); gap: 4px; }
|
||||
.auth-card-actions .auth-list-action { display: inline-flex; align-items: center; justify-content: center; flex: none; width: 30px; min-width: 0; height: 30px; padding: 0; border: 1px solid transparent; border-radius: 8px; color: var(--auth-accent); background: color-mix(in srgb, var(--auth-accent) 10%, var(--bg-card)); cursor: pointer; }
|
||||
.auth-card-actions .auth-list-action.muted { color: var(--auth-muted); background: var(--bg-secondary); }
|
||||
.auth-card-actions .auth-list-action.danger { color: #ef4444; background: color-mix(in srgb, #ef4444 9%, var(--bg-card)); }
|
||||
.auth-card-actions .auth-list-action:disabled { opacity: .4; cursor: default; }
|
||||
.auth-card-actions .auth-list-action:not(:disabled):hover { border-color: currentColor; }
|
||||
.auth-list-main-actions { display: grid; justify-items: center; align-items: center; grid-template-rows: repeat(2, 30px); gap: 4px; border-left: 1px solid var(--auth-border); }
|
||||
.auth-list-switch { display: flex; align-items: center; flex: none; width: 38px; height: 22px; padding: 2px; border: 1px solid color-mix(in srgb, var(--auth-muted) 18%, var(--bg-card)); border-radius: 20px; background: color-mix(in srgb, var(--auth-muted) 24%, var(--bg-card)); cursor: pointer; }
|
||||
.auth-list-switch > span { display: block; width: 16px; height: 16px; flex-shrink: 0; border-radius: 50%; background: #fff; box-shadow: 0 1px 3px #0002; transition: transform .15s; }
|
||||
.auth-card-actions { display: flex; align-items: center; justify-content: flex-end; gap: 10px; min-width: 0; }
|
||||
.auth-list-icon-actions { display: flex; align-items: center; gap: 6px; }
|
||||
.auth-card-actions .auth-list-action { display: inline-flex; align-items: center; justify-content: center; flex: none; width: 32px; min-width: 32px; height: 32px; padding: 0; border: 1px solid color-mix(in srgb, var(--auth-accent) 16%, var(--auth-border)); border-radius: 8px; color: var(--auth-accent); background: var(--bg-card); cursor: pointer; }
|
||||
.auth-card-actions .auth-list-action.muted { color: var(--auth-text); border-color: var(--auth-border); background: var(--bg-card); }
|
||||
.auth-card-actions .auth-list-action.danger { color: #e11d48; border-color: color-mix(in srgb, #e11d48 22%, var(--auth-border)); background: var(--bg-card); }
|
||||
.auth-card-actions .auth-list-action:disabled { opacity: .38; cursor: default; }
|
||||
.auth-card-actions .auth-list-action:not(:disabled):hover { background: color-mix(in srgb, currentColor 8%, var(--bg-card)); border-color: currentColor; }
|
||||
.auth-list-main-actions { display: flex; align-items: center; justify-content: flex-end; gap: 8px; padding-left: 10px; border-left: 1px solid var(--auth-border); }
|
||||
.auth-list-switch { display: flex; align-items: center; flex: none; width: 40px; height: 24px; padding: 2px; border: 1px solid color-mix(in srgb, var(--auth-muted) 22%, var(--bg-card)); border-radius: 20px; background: color-mix(in srgb, var(--auth-muted) 28%, var(--bg-card)); cursor: pointer; }
|
||||
.auth-list-switch > span { display: block; width: 18px; height: 18px; flex-shrink: 0; border-radius: 50%; background: #fff; box-shadow: 0 1px 3px #0002; transition: transform .15s; }
|
||||
.auth-list-switch[aria-checked="true"] { background: var(--auth-accent); border-color: var(--auth-accent); }
|
||||
.auth-list-switch[aria-checked="true"] > span { transform: translateX(16px); }
|
||||
.auth-list-switch:disabled { opacity: .5; cursor: default; }
|
||||
.auth-list-details-button { width: 50px; min-height: 30px; padding: 4px; border: 0; border-radius: 8px; background: color-mix(in srgb, var(--auth-accent) 7%, var(--bg-card)); color: var(--auth-accent); font: inherit; font-size: 11px; font-weight: 650; cursor: pointer; }
|
||||
.auth-list-details-button { width: auto; min-width: 52px; min-height: 32px; padding: 4px 10px; border: 1px solid color-mix(in srgb, var(--auth-accent) 18%, var(--auth-border)); border-radius: 8px; background: var(--bg-card); color: var(--auth-accent); font: inherit; font-size: 13px; font-weight: 650; cursor: pointer; white-space: nowrap; }
|
||||
.auth-list-details-button:hover { background: color-mix(in srgb, var(--auth-accent) 14%, var(--bg-card)); }
|
||||
.auth-list-action:focus-visible, .auth-list-switch:focus-visible, .auth-list-details-button:focus-visible { outline: 2px solid var(--auth-accent); outline-offset: 3px; }
|
||||
.auth-list-details { display: grid; gap: 12px; border-top: 1px solid var(--auth-border); padding: 12px 0; }
|
||||
@@ -105,7 +105,7 @@
|
||||
.auth-card-note > span { flex-shrink: 0; color: var(--auth-muted); }
|
||||
.auth-card-note p { min-width: 0; margin: 0; color: var(--auth-text); white-space: pre-wrap; overflow-wrap: anywhere; }
|
||||
.auth-file-card .auth-health-body, .auth-file-card .auth-health-summary { overflow-wrap: anywhere; }
|
||||
.auth-list-pagination { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; padding: 12px 0 0; color: var(--auth-muted); font-size: 11px; }
|
||||
.auth-list-pagination { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; width: 100%; max-width: 100%; margin-left: auto; padding: 12px 0 0; color: var(--auth-muted); font-size: 12px; position: sticky; left: 0; }
|
||||
.auth-list-pagination > span { margin-right: auto; }
|
||||
.auth-list-pagination label { display: flex; align-items: center; gap: 6px; }
|
||||
.auth-list-pagination select { min-height: 30px; border: 1px solid var(--auth-border); border-radius: 6px; background: var(--bg-card); color: var(--auth-muted); padding: 3px 6px; font: inherit; }
|
||||
@@ -153,14 +153,13 @@
|
||||
@media (prefers-reduced-motion: reduce) { .credential-quota-track.indeterminate i { animation: none; width: 100%; opacity: .3; } }
|
||||
|
||||
|
||||
@container credential-files (min-width: 701px) and (max-width: 1147px) {
|
||||
.auth-file-list-head, .auth-credential-row { grid-template-columns: minmax(140px, 1fr) 60px 78px 96px 136px minmax(180px, 2fr) 142px; column-gap: 8px; }
|
||||
.auth-file-card-grid, .auth-file-list-head { min-width: 906px; }
|
||||
.auth-card-actions { grid-template-columns: 86px 46px; gap: 8px; }
|
||||
.auth-list-icon-actions { grid-template-columns: repeat(3, 26px); }
|
||||
.auth-card-actions .auth-list-action { width: 26px; height: 28px; border-radius: 6px; }
|
||||
.auth-list-main-actions { grid-template-rows: repeat(2, 28px); }
|
||||
.auth-list-details-button { width: 42px; min-height: 28px; }
|
||||
@container credential-files (min-width: 701px) and (max-width: 1360px) {
|
||||
.auth-file-list-head, .auth-credential-row { grid-template-columns: minmax(132px, 1fr) 58px 74px 92px 128px minmax(168px, 1.6fr) 214px;; column-gap: 8px; }
|
||||
.auth-file-card-grid, .auth-file-list-head { min-width: 980px; }
|
||||
.auth-card-actions { gap: 8px; }
|
||||
.auth-list-icon-actions { gap: 4px; }
|
||||
.auth-card-actions .auth-list-action { width: 30px; min-width: 30px; height: 30px; }
|
||||
.auth-list-details-button { min-width: 48px; min-height: 30px; padding-inline: 8px; font-size: 12px; }
|
||||
.auth-list-usage .auth-file-usage-metric strong { font-size: 12px; }
|
||||
}
|
||||
@container credential-files (max-width: 700px) {
|
||||
|
||||
@@ -31,6 +31,8 @@ export type AgentModelMappings = {
|
||||
disableAutoCompact?: boolean;
|
||||
manageDefaultModel?: boolean;
|
||||
manageSubagentModel?: boolean;
|
||||
startupModel?: string;
|
||||
subagentModel?: string;
|
||||
};
|
||||
|
||||
type ResolveAgentConfigurationActionOptions = {
|
||||
@@ -72,7 +74,9 @@ export const sameAgentModelMappings = (
|
||||
&& (left.autoCompactPct ?? 90) === (right.autoCompactPct ?? 90)
|
||||
&& Boolean(left.disableAutoCompact) === Boolean(right.disableAutoCompact)
|
||||
&& (left.manageDefaultModel ?? true) === (right.manageDefaultModel ?? true)
|
||||
&& (left.manageSubagentModel ?? true) === (right.manageSubagentModel ?? true);
|
||||
&& (left.manageSubagentModel ?? true) === (right.manageSubagentModel ?? true)
|
||||
&& (left.startupModel ?? '') === (right.startupModel ?? '')
|
||||
&& (left.subagentModel ?? '') === (right.subagentModel ?? '');
|
||||
|
||||
export const resolveAgentModelMappingsDraftSource = <T>(
|
||||
current: T,
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import type { ModelOption } from './modelService';
|
||||
|
||||
export const claudeCodeRoles = ['opus', 'sonnet', 'haiku'] as const;
|
||||
export type ClaudeCodeRole = typeof claudeCodeRoles[number];
|
||||
export type ClaudeCodeRoutes = Record<ClaudeCodeRole, string> & {
|
||||
opus1m: boolean; sonnet1m: boolean; haiku1m: boolean;
|
||||
};
|
||||
|
||||
export function claudeCodeModelBase(value: string): string {
|
||||
return value.trim().replace(/(?:\[1m\])+$/i, '');
|
||||
}
|
||||
|
||||
export function claudeCodeRole(value: string): ClaudeCodeRole | null {
|
||||
const base = claudeCodeModelBase(value).toLowerCase();
|
||||
return claudeCodeRoles.find(role => role === base) ?? null;
|
||||
}
|
||||
|
||||
export function claudeCodeModelPreview(value: string, routes: ClaudeCodeRoutes) {
|
||||
const role = claudeCodeRole(value);
|
||||
const base = role ? claudeCodeModelBase(routes[role]) : claudeCodeModelBase(value);
|
||||
const context1m = /\[1m\]$/i.test(value.trim()) || Boolean(role && routes[`${role}1m`]);
|
||||
return { role, model: base ? base + (context1m ? '[1m]' : '') : '', context1m };
|
||||
}
|
||||
|
||||
export function isKnownClaudeCodeModel(value: string, models: ModelOption[]): boolean {
|
||||
const base = claudeCodeModelBase(value).toLowerCase();
|
||||
return !base || Boolean(claudeCodeRole(value)) || models.some(model => model.name.toLowerCase() === base);
|
||||
}
|
||||
@@ -7418,6 +7418,7 @@ textarea:focus-visible {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
|
||||
.agent-claude-context-toggle {
|
||||
display: flex;
|
||||
flex: none;
|
||||
|
||||
@@ -44,7 +44,7 @@ const fs = require('node:fs/promises');
|
||||
className: node.className, width: node.clientWidth, scroll: node.scrollWidth, overflowX: getComputedStyle(node).overflowX,
|
||||
})));
|
||||
assert.ok(dimensions.every(size => size.scroll <= size.width + 1 || (
|
||||
size.className === 'auth-file-table-scroll' && size.width > 700 && size.width < 906 && size.overflowX === 'auto'
|
||||
size.className === 'auth-file-table-scroll' && size.width > 700 && size.width < 980 && size.overflowX === 'auto'
|
||||
)), `${label}: only intermediate widths may scroll inside the table; cards must fit ${JSON.stringify(dimensions)}`);
|
||||
assert.equal(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth + 1), true, `${label}: page overflow`);
|
||||
};
|
||||
@@ -93,7 +93,7 @@ const fs = require('node:fs/promises');
|
||||
const resetTitle = await resetTimes.first().getAttribute('title');
|
||||
assert.ok(resetTitle.startsWith(await resetTimes.first().innerText()) && resetTitle.includes('小时后'), 'Compact timestamps retain their exact time and relative countdown on hover');
|
||||
await next().click();
|
||||
await card(11).waitFor();
|
||||
await card(7).waitFor();
|
||||
assert.equal(await cards().count(), 2);
|
||||
assert.equal(await next().isDisabled(), true);
|
||||
assert.ok((await pagination().innerText()).includes('2 / 2'));
|
||||
@@ -235,10 +235,10 @@ const fs = require('node:fs/promises');
|
||||
await page.setViewportSize({ width: 1800, height: 1200 });
|
||||
await open('theme=light&locale=zh-CN');
|
||||
const tableBounds = await page.locator('.auth-file-table-scroll').boundingBox();
|
||||
const seventhBounds = await card(7).boundingBox();
|
||||
const lastBounds = await cards().last().boundingBox();
|
||||
await page.screenshot({ path: path.join(screenshotDir, 'reference-dense-preview.png'), clip: {
|
||||
x: tableBounds.x, y: tableBounds.y, width: tableBounds.width,
|
||||
height: seventhBounds.y + seventhBounds.height - tableBounds.y,
|
||||
height: lastBounds.y + lastBounds.height - tableBounds.y,
|
||||
} });
|
||||
await open('theme=light&locale=zh-CN&small=1');
|
||||
await page.locator('.auth-file-table-scroll').screenshot({ path: path.join(screenshotDir, 'reference-small-idle-preview.png') });
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
const { chromium } = require(process.env.PLAYWRIGHT_MODULE || 'playwright');
|
||||
const assert = require('node:assert/strict');
|
||||
const path = require('node:path');
|
||||
const os = require('node:os');
|
||||
|
||||
(async () => {
|
||||
const browser = await chromium.launch({ channel: 'msedge', headless: true });
|
||||
try {
|
||||
const page = await browser.newPage({ viewport: { width: 1280, height: 900 } });
|
||||
await page.goto('http://127.0.0.1:1421/tests/fixtures/agent-backups.html?client=claude-code&reset-selections');
|
||||
const startup = page.getByRole('combobox', { name: '启动模型', exact: true });
|
||||
const subagent = page.getByRole('combobox', { name: 'Subagent 模型', exact: true });
|
||||
await startup.click();
|
||||
await page.getByRole('option', { name: /^sonnet / }).click();
|
||||
assert.equal(await startup.inputValue(), 'sonnet');
|
||||
await page.locator('.claude-role-row .agent-model-trigger').first().click();
|
||||
await page.getByRole('option', { name: /gpt-two/ }).click();
|
||||
await page.getByText('跟随 sonnet → gpt-two', { exact: true }).waitFor();
|
||||
assert.equal(await startup.inputValue(), 'sonnet');
|
||||
await startup.click();
|
||||
await page.getByRole('option', { name: /^gpt-two / }).click();
|
||||
await page.locator('.claude-code-context-settings summary').click();
|
||||
const context = page.getByRole('checkbox', { name: '启动模型 1M', exact: true });
|
||||
await context.check();
|
||||
assert.equal(await startup.inputValue(), 'gpt-two[1m]');
|
||||
assert.equal(await page.getByRole('spinbutton', { name: '全局窗口覆盖', exact: true }).inputValue(), '1000000');
|
||||
await context.uncheck();
|
||||
assert.equal(await page.getByRole('spinbutton', { name: '全局窗口覆盖', exact: true }).inputValue(), '200000');
|
||||
assert.equal(await startup.inputValue(), 'gpt-two');
|
||||
await page.locator('.claude-code-context-settings summary').click();
|
||||
assert.equal(await page.locator('.claude-role-row input[type=checkbox]').count(), 3);
|
||||
await subagent.fill('custom-worker');
|
||||
await subagent.press('Enter');
|
||||
assert.equal(await subagent.inputValue(), 'custom-worker');
|
||||
await page.getByRole('button', { name: '更新配置', exact: true }).click();
|
||||
await page.waitForFunction(() => window.fixtureCalls.some(c => c.args?.claudeCodeModelMappings?.startupModel === 'gpt-two' && c.args?.claudeCodeModelMappings?.subagentModel === 'custom-worker'));
|
||||
await subagent.fill('unsaved-worker');
|
||||
await subagent.press('Escape');
|
||||
await page.evaluate(() => { window.fixtureFailedCommands = ['get_agent_models']; });
|
||||
await page.locator('.claude-role-row .agent-model-trigger').first().click();
|
||||
await page.locator('.agent-model-dropdown:visible button').filter({ has: page.locator('svg.lucide-refresh-cw') }).click();
|
||||
await page.locator('.claude-code-model-error').waitFor();
|
||||
assert.equal(await startup.inputValue(), 'gpt-two');
|
||||
assert.equal(await subagent.inputValue(), 'unsaved-worker');
|
||||
assert.match(await page.locator('.claude-role-row .agent-model-trigger').first().innerText(), /gpt-two/);
|
||||
await subagent.press('Escape');
|
||||
await page.evaluate(() => { window.fixtureFailedCommands = []; });
|
||||
await page.locator('.claude-code-model-error button').click();
|
||||
await page.locator('.claude-code-model-error').waitFor({ state: 'hidden' });
|
||||
assert.equal(await subagent.inputValue(), 'unsaved-worker');
|
||||
await startup.click();
|
||||
await page.getByRole('option', { name: '使用 Claude Code 默认模型', exact: true }).click();
|
||||
await subagent.click();
|
||||
await page.getByRole('option', { name: '使用默认继承行为', exact: true }).click();
|
||||
await page.getByRole('button', { name: '更新配置', exact: true }).click();
|
||||
await page.waitForFunction(() => window.fixtureCalls.some(c => c.args?.claudeCodeModelMappings?.startupModel === '' && c.args?.claudeCodeModelMappings?.subagentModel === ''));
|
||||
await page.getByRole('checkbox', { name: 'sonnet 1M', exact: true }).check();
|
||||
await page.getByRole('button', { name: '将 Sonnet 应用到全部角色', exact: true }).click();
|
||||
for (const role of ['sonnet', 'opus', 'haiku']) {
|
||||
assert.equal(await page.getByRole('checkbox', { name: `${role} 1M`, exact: true }).isChecked(), true);
|
||||
assert.match(await page.locator(`.claude-role-row[data-role="${role}"] .agent-model-trigger`).innerText(), /gpt-two/);
|
||||
}
|
||||
await page.getByRole('button', { name: '更新配置', exact: true }).click();
|
||||
await page.waitForFunction(() => window.fixtureCalls.some(c => {
|
||||
const m = c.args?.claudeCodeModelMappings;
|
||||
return m && m.opus === 'gpt-two' && m.haiku === 'gpt-two' && m.opus1m && m.haiku1m && m.sonnet1m;
|
||||
}));
|
||||
for (const width of [1280, 760]) {
|
||||
await page.setViewportSize({ width, height: 1000 });
|
||||
await page.screenshot({ path: path.join(os.tmpdir(), `claude-code-models-${width}.png`), fullPage: true });
|
||||
for (const field of await page.locator('.claude-code-model-field').all()) {
|
||||
assert(await field.evaluate(el => el.scrollWidth <= el.clientWidth + 1));
|
||||
}
|
||||
}
|
||||
console.log('PASS: select, custom input, defaults, connection failure/retry, draft retention and responsive layout');
|
||||
} finally { await browser.close(); }
|
||||
})().catch(error => { console.error(error); process.exitCode = 1; });
|
||||
@@ -0,0 +1,18 @@
|
||||
import { expect, test } from 'bun:test';
|
||||
import { claudeCodeModelPreview, isKnownClaudeCodeModel } from '../src/services/claudeCodeModels';
|
||||
|
||||
const routes = { opus: 'model-a', sonnet: 'model-b', haiku: 'model-c', opus1m: true, sonnet1m: false, haiku1m: false };
|
||||
test('role selections follow changed mappings and role context flags', () => {
|
||||
expect(claudeCodeModelPreview('opus', routes).model).toBe('model-a[1m]');
|
||||
expect(claudeCodeModelPreview('sonnet', { ...routes, sonnet: 'model-d', sonnet1m: true }).model).toBe('model-d[1m]');
|
||||
});
|
||||
test('direct model stays pinned and has its own context flag', () => {
|
||||
expect(claudeCodeModelPreview('model-a', routes)).toEqual({ role: null, model: 'model-a', context1m: false });
|
||||
expect(claudeCodeModelPreview('model-a[1m]', { ...routes, opus: 'other' }).model).toBe('model-a[1m]');
|
||||
});
|
||||
test('role aliases do not need to appear in CPA model list', () => {
|
||||
expect(isKnownClaudeCodeModel('sonnet[1m]', [])).toBeTrue();
|
||||
expect(isKnownClaudeCodeModel('', [])).toBeTrue();
|
||||
expect(isKnownClaudeCodeModel('model-a[1m]', [{ name: 'model-a' }])).toBeTrue();
|
||||
expect(isKnownClaudeCodeModel('unknown', [{ name: 'model-a' }])).toBeFalse();
|
||||
});
|
||||
Reference in new issue
Block a user