diff --git a/src-tauri/src/agents/commands.rs b/src-tauri/src/agents/commands.rs index 27a5efd..29e0054 100644 --- a/src-tauri/src/agents/commands.rs +++ b/src-tauri/src/agents/commands.rs @@ -688,7 +688,7 @@ pub(crate) async fn fetch_prepared_agent_models( prepare_codex_agent_models(&runtime_models) } else { let mut models = fetch_agent_models(config.port, api_key).await?; - if client == AgentClient::ZCode { + if agent_uses_cpa_runtime_context_windows(client) { let runtime_models = fetch_codex_runtime_models(config.port, api_key).await?; codex_catalog::merge_runtime_context_windows(&mut models, &runtime_models); } @@ -714,6 +714,13 @@ pub(crate) async fn fetch_prepared_agent_models( } } +pub(crate) fn agent_uses_cpa_runtime_context_windows(client: AgentClient) -> bool { + matches!( + client, + AgentClient::ZCode | AgentClient::KimiCode | AgentClient::GrokBuild + ) +} + pub(crate) fn resolve_claude_desktop_model_mappings( client: AgentClient, models: &[AgentModelOption], diff --git a/src-tauri/src/agents/configuration.rs b/src-tauri/src/agents/configuration.rs index c2283a4..c38d99d 100644 --- a/src-tauri/src/agents/configuration.rs +++ b/src-tauri/src/agents/configuration.rs @@ -525,6 +525,10 @@ pub(crate) fn build_claude_agent_config( 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()), + ); env.insert( CLAUDE_AUTOCOMPACT_PCT_OVERRIDE_ENV.to_string(), serde_json::Value::String(mappings.auto_compact_pct.to_string()), @@ -922,6 +926,7 @@ pub(crate) fn remove_claude_code_managed_configuration( "ANTHROPIC_DEFAULT_SONNET_MODEL", "ANTHROPIC_DEFAULT_OPUS_MODEL", "ANTHROPIC_DEFAULT_FABLE_MODEL", + CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY_ENV, CLAUDE_CODE_MAX_CONTEXT_TOKENS_ENV, CLAUDE_AUTOCOMPACT_PCT_OVERRIDE_ENV, DISABLE_AUTO_COMPACT_ENV, @@ -1582,6 +1587,7 @@ pub(crate) fn build_restored_claude_code_config( "ANTHROPIC_DEFAULT_SONNET_MODEL", "ANTHROPIC_DEFAULT_OPUS_MODEL", "ANTHROPIC_DEFAULT_FABLE_MODEL", + CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY_ENV, CLAUDE_CODE_MAX_CONTEXT_TOKENS_ENV, CLAUDE_AUTOCOMPACT_PCT_OVERRIDE_ENV, DISABLE_AUTO_COMPACT_ENV, diff --git a/src-tauri/src/agents/discovery.rs b/src-tauri/src/agents/discovery.rs index c06b0b3..131d80f 100644 --- a/src-tauri/src/agents/discovery.rs +++ b/src-tauri/src/agents/discovery.rs @@ -368,6 +368,7 @@ pub(crate) fn inspect_pi_provider_status( config_exists, config_valid, configured, + configuration_synchronized: configured, current_model: current_model.clone(), oauth_configuration: false, modification_enabled: configured, @@ -849,6 +850,7 @@ pub(crate) fn inspect_agent_config( warnings.push(message.clone()); } let modification = inspect_agent_application(client, home); + let configuration_synchronized = agent_configuration_is_synchronized(client, home, configured); warnings.extend(modification.warnings.iter().cloned()); AgentConfigStatus { @@ -865,6 +867,7 @@ pub(crate) fn inspect_agent_config( config_exists, config_valid, configured, + configuration_synchronized, current_model, oauth_configuration, modification_enabled: modification.enabled, @@ -880,6 +883,23 @@ pub(crate) fn inspect_agent_config( } } +pub(crate) fn agent_configuration_is_synchronized( + client: AgentClient, + home: &Path, + configured: bool, +) -> bool { + if !configured { + return false; + } + if !matches!(client, AgentClient::KimiCode | AgentClient::GrokBuild) { + return true; + } + load_agent_applied_state(client, home) + .ok() + .flatten() + .is_some_and(|state| state.configuration_revision >= AGENT_CONFIGURATION_REVISION) +} + pub(crate) fn inspect_agent_application( client: AgentClient, home: &Path, @@ -2063,7 +2083,11 @@ pub(crate) fn inspect_claude_agent_config( && env .and_then(|env| env.get("ANTHROPIC_AUTH_TOKEN")) .and_then(serde_json::Value::as_str) - == Some(api_key); + == Some(api_key) + && env + .and_then(|env| env.get(CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY_ENV)) + .and_then(serde_json::Value::as_str) + == Some("1"); let model = env .and_then(|env| env.get("ANTHROPIC_MODEL")) .and_then(serde_json::Value::as_str) diff --git a/src-tauri/src/agents/state.rs b/src-tauri/src/agents/state.rs index 4600f69..d1eed97 100644 --- a/src-tauri/src/agents/state.rs +++ b/src-tauri/src/agents/state.rs @@ -161,6 +161,7 @@ pub(crate) fn recover_codex_applied_state_from_backups( version: AGENT_APPLIED_STATE_VERSION, client: AgentClient::Codex.id().to_string(), model, + configuration_revision: AGENT_CONFIGURATION_REVISION, claude_desktop_model_mappings: None, backup_files, updated_at_unix: SystemTime::now() @@ -284,6 +285,7 @@ pub(crate) fn load_agent_applied_state( version: AGENT_APPLIED_STATE_VERSION, client: client.id().to_string(), model: record.model, + configuration_revision: 0, claude_desktop_model_mappings: None, backup_files, updated_at_unix: SystemTime::now() @@ -1485,6 +1487,7 @@ pub(crate) fn commit_agent_configuration( version: AGENT_APPLIED_STATE_VERSION, client: client.id().to_string(), model: model.to_string(), + configuration_revision: AGENT_CONFIGURATION_REVISION, claude_desktop_model_mappings: (client == AgentClient::ClaudeDesktop) .then(|| claude_desktop_model_mappings.cloned()) .flatten(), diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index bfc43af..5336ec3 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -159,6 +159,8 @@ const MANAGED_CLAUDE_HAIKU_ALIAS_DISPLAY_NAME: &str = 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 CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY_ENV: &str = + "CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY"; const CLAUDE_AUTOCOMPACT_PCT_OVERRIDE_ENV: &str = "CLAUDE_AUTOCOMPACT_PCT_OVERRIDE"; const DISABLE_AUTO_COMPACT_ENV: &str = "DISABLE_AUTO_COMPACT"; const DEFAULT_CLAUDE_AUTO_COMPACT_PCT: u8 = 90; @@ -171,6 +173,7 @@ const MODEL_ALIAS_CONFIG_SECTIONS: &[&str] = &[ const LEGACY_AGENT_MODIFICATION_STATE_VERSION: u8 = 1; const AGENT_MODIFICATION_STATE_VERSION: u8 = 2; const AGENT_APPLIED_STATE_VERSION: u8 = 4; +const AGENT_CONFIGURATION_REVISION: u8 = 1; const AGENT_PHASE_APPLYING: &str = "applying"; const AGENT_PHASE_ACTIVE: &str = "active"; const AGENT_PHASE_RESTORING: &str = "restoring"; @@ -768,6 +771,7 @@ struct AgentConfigStatus { config_exists: bool, config_valid: bool, configured: bool, + configuration_synchronized: bool, current_model: Option, oauth_configuration: bool, modification_enabled: bool, @@ -951,6 +955,8 @@ struct AgentAppliedState { version: u8, client: String, model: String, + #[serde(default)] + configuration_revision: u8, #[serde(default, skip_serializing_if = "Option::is_none")] claude_desktop_model_mappings: Option, #[serde(default, skip_serializing_if = "Vec::is_empty")] diff --git a/src-tauri/src/tests/agent_configuration.rs b/src-tauri/src/tests/agent_configuration.rs index 038e563..85cfe58 100644 --- a/src-tauri/src/tests/agent_configuration.rs +++ b/src-tauri/src/tests/agent_configuration.rs @@ -22,6 +22,10 @@ fn claude_agent_config_preserves_existing_fields() { assert_eq!(value["env"]["ANTHROPIC_BASE_URL"], "http://127.0.0.1:8317"); assert_eq!(value["env"]["ANTHROPIC_AUTH_TOKEN"], DEFAULT_API_KEY); assert_eq!(value["env"]["ANTHROPIC_MODEL"], "claude-test"); + assert_eq!( + value["env"][CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY_ENV], + "1" + ); 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()); @@ -44,6 +48,7 @@ fn claude_code_inspection_normalizes_legacy_1m_suffix() { "env": { "ANTHROPIC_BASE_URL": "http://127.0.0.1:8317", "ANTHROPIC_AUTH_TOKEN": "test-key", + "CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY": "1", "ANTHROPIC_MODEL": "deepseek-v4-pro[1m]", "ANTHROPIC_DEFAULT_OPUS_MODEL": "deepseek-v4-pro[1m]", "ANTHROPIC_DEFAULT_SONNET_MODEL": "deepseek-v4-pro[1m]", @@ -71,6 +76,30 @@ fn claude_code_inspection_normalizes_legacy_1m_suffix() { fs::remove_dir_all(directory).unwrap(); } +#[test] +fn claude_code_requires_gateway_model_discovery_to_be_enabled() { + let directory = agent_test_home("claude-code-gateway-model-discovery"); + let path = directory.join("settings.json"); + fs::write( + &path, + r#"{ + "env": { + "ANTHROPIC_BASE_URL": "http://127.0.0.1:8317", + "ANTHROPIC_AUTH_TOKEN": "test-key", + "ANTHROPIC_MODEL": "gpt-test", + "CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY": "0" + } + }"#, + ) + .unwrap(); + + let (configured, model) = inspect_claude_agent_config(&path, 8317, "test-key").unwrap(); + + assert!(!configured); + assert_eq!(model.as_deref(), Some("gpt-test")); + fs::remove_dir_all(directory).unwrap(); +} + #[test] fn claude_mapping_legacy_json_defaults_1m_preferences_off() { let mappings: ClaudeDesktopModelMappings = @@ -762,7 +791,9 @@ fn kimi_code_config_preserves_existing_tables_and_uses_openai_provider() { let home = agent_test_home("kimi-code-agent-config"); let path = home.join(".kimi-code/config.toml"); fs::create_dir_all(path.parent().unwrap()).unwrap(); - let models = test_agent_models(&["gpt-test", "deepseek-test"]); + let mut models = test_agent_models(&["gpt-test", "deepseek-test"]); + models[0].context_window = Some(1_048_576); + models[1].context_window = Some(131_072); let rendered = build_kimi_code_agent_config( Some( r#"theme = "dark" @@ -806,6 +837,14 @@ model = "model" .as_str(), Some("tool_use") ); + assert_eq!( + value["models"]["cpa-gui/gpt-test"]["max_context_size"].as_integer(), + Some(1_048_576) + ); + assert_eq!( + value["models"]["cpa-gui/deepseek-test"]["max_context_size"].as_integer(), + Some(131_072) + ); assert_eq!( inspect_kimi_code_agent_config(&path, 8317, DEFAULT_API_KEY).unwrap(), (true, Some("gpt-test".to_string())) @@ -818,7 +857,9 @@ fn grok_build_config_preserves_existing_models_and_uses_chat_completions() { let home = agent_test_home("grok-build-agent-config"); let path = home.join(".grok/config.toml"); fs::create_dir_all(path.parent().unwrap()).unwrap(); - let models = test_agent_models(&["gpt-test", "deepseek-test"]); + let mut models = test_agent_models(&["gpt-test", "deepseek-test"]); + models[0].context_window = Some(1_048_576); + models[1].context_window = Some(131_072); let rendered = build_grok_build_agent_config( Some( r#"theme = "dark" @@ -858,6 +899,14 @@ api_key = "other-key" value["model"]["cpa-gui/gpt-test"]["api_backend"].as_str(), Some("chat_completions") ); + assert_eq!( + value["model"]["cpa-gui/gpt-test"]["context_window"].as_integer(), + Some(1_048_576) + ); + assert_eq!( + value["model"]["cpa-gui/deepseek-test"]["context_window"].as_integer(), + Some(131_072) + ); assert_eq!( inspect_grok_build_agent_config(&path, 8317, DEFAULT_API_KEY).unwrap(), (true, Some("gpt-test".to_string())) @@ -1077,6 +1126,28 @@ fn agent_model_list_parser_exposes_aliases_as_selectable_model_ids() { ); } +#[test] +fn kimi_grok_and_zcode_use_cpa_runtime_context_windows() { + for client in [ + AgentClient::KimiCode, + AgentClient::GrokBuild, + AgentClient::ZCode, + ] { + assert!(agent_uses_cpa_runtime_context_windows(client)); + } + for client in [ + AgentClient::ClaudeCode, + AgentClient::ClaudeDesktop, + AgentClient::Codex, + AgentClient::OpenCode, + AgentClient::OpenClaw, + AgentClient::Hermes, + AgentClient::DeepSeekHarness, + ] { + assert!(!agent_uses_cpa_runtime_context_windows(client)); + } +} + #[test] fn claude_model_lists_mark_yaml_aliases_when_api_returns_only_model_ids() { let mut models = test_agent_models(&[ diff --git a/src-tauri/src/tests/agent_state.rs b/src-tauri/src/tests/agent_state.rs index 98bbfe1..aec01db 100644 --- a/src-tauri/src/tests/agent_state.rs +++ b/src-tauri/src/tests/agent_state.rs @@ -453,6 +453,7 @@ fn zcode_legacy_single_file_state_upgrades_and_restores_cli_config() { version: AGENT_APPLIED_STATE_VERSION, client: AgentClient::ZCode.id().to_string(), model: "gpt-one".to_string(), + configuration_revision: 0, claude_desktop_model_mappings: None, backup_files: vec![AgentAppliedBackupFile { path: paths[0].clone(), @@ -489,9 +490,37 @@ fn zcode_legacy_single_file_state_upgrades_and_restores_cli_config() { fs::remove_dir_all(home).unwrap(); } +#[test] +fn kimi_and_grok_legacy_states_require_context_catalog_resynchronization() { + for client in [AgentClient::KimiCode, AgentClient::GrokBuild] { + let home = agent_test_home(&format!("{}-context-revision", client.id())); + let paths = agent_config_paths(client, &home); + let state_path = agent_state_path(&paths).unwrap(); + fs::create_dir_all(state_path.parent().unwrap()).unwrap(); + let mut state = AgentAppliedState { + version: AGENT_APPLIED_STATE_VERSION, + client: client.id().to_string(), + model: "gpt-test".to_string(), + configuration_revision: 0, + claude_desktop_model_mappings: None, + backup_files: Vec::new(), + updated_at_unix: 1, + }; + write_agent_applied_state(&state_path, &state).unwrap(); + + assert!(!agent_configuration_is_synchronized(client, &home, true)); + + state.configuration_revision = AGENT_CONFIGURATION_REVISION; + write_agent_applied_state(&state_path, &state).unwrap(); + assert!(agent_configuration_is_synchronized(client, &home, true)); + assert!(!agent_configuration_is_synchronized(client, &home, false)); + fs::remove_dir_all(home).unwrap(); + } +} + #[test] fn session_merge_preserves_runtime_fields_for_other_agent_formats() { - let claude_original = r#"{"env":{"KEEP_ENV":"original"},"keep":"claude"}"#; + let claude_original = r#"{"env":{"KEEP_ENV":"original","CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY":"0"},"keep":"claude"}"#; let claude_managed = build_claude_agent_config( Some(claude_original), "http://127.0.0.1:8317", @@ -516,6 +545,10 @@ fn session_merge_preserves_runtime_fields_for_other_agent_formats() { assert_eq!(claude["runtimeAdded"], true); assert_eq!(claude["env"]["RUNTIME_ENV"], "keep"); assert_eq!(claude["env"]["KEEP_ENV"], "original"); + assert_eq!( + claude["env"][CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY_ENV], + "0" + ); assert!(claude["env"].get("ANTHROPIC_BASE_URL").is_none()); assert!(claude.get("model").is_none()); @@ -735,6 +768,7 @@ fn legacy_codex_single_file_state_remains_restorable() { version: AGENT_APPLIED_STATE_VERSION, client: AgentClient::Codex.id().to_string(), model: "gpt-test".to_string(), + configuration_revision: AGENT_CONFIGURATION_REVISION, claude_desktop_model_mappings: None, backup_files: vec![AgentAppliedBackupFile { path: path.clone(), @@ -799,6 +833,7 @@ fn claude_desktop_version_three_state_can_be_closed_safely() { version: 3, client: AgentClient::ClaudeDesktop.id().to_string(), model: "gpt-test".to_string(), + configuration_revision: 0, claude_desktop_model_mappings: None, backup_files: Vec::new(), updated_at_unix: 1, @@ -930,6 +965,7 @@ fn applied_state_rejects_backup_paths_outside_the_managed_config_set() { version: AGENT_APPLIED_STATE_VERSION, client: AgentClient::OpenCode.id().to_string(), model: "gpt-test".to_string(), + configuration_revision: AGENT_CONFIGURATION_REVISION, claude_desktop_model_mappings: None, backup_files: vec![AgentAppliedBackupFile { path: path.clone(), @@ -966,6 +1002,7 @@ fn missing_session_backup_is_detected_before_any_file_is_restored() { version: AGENT_APPLIED_STATE_VERSION, client: AgentClient::Codex.id().to_string(), model: "gpt-test".to_string(), + configuration_revision: AGENT_CONFIGURATION_REVISION, claude_desktop_model_mappings: None, backup_files: vec![ AgentAppliedBackupFile { @@ -1003,6 +1040,7 @@ fn version_three_cleanup_covers_every_non_desktop_agent_format() { version: 3, client: client.id().to_string(), model: "gpt-test".to_string(), + configuration_revision: 0, claude_desktop_model_mappings: None, backup_files: Vec::new(), updated_at_unix: 1, @@ -1017,6 +1055,7 @@ fn version_three_cleanup_covers_every_non_desktop_agent_format() { "ANTHROPIC_BASE_URL": "http://127.0.0.1:8317", "ANTHROPIC_API_KEY": "secret", "ANTHROPIC_AUTH_TOKEN": "secret", + "CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY": "1", "ANTHROPIC_MODEL": "gpt-test", "ANTHROPIC_DEFAULT_HAIKU_MODEL": "gpt-test", "KEEP_ENV": "yes" @@ -1039,6 +1078,9 @@ fn version_three_cleanup_covers_every_non_desktop_agent_format() { serde_json::from_str(&fs::read_to_string(&claude_path).unwrap()).unwrap(); assert!(claude.get("model").is_none()); assert!(claude["env"].get("ANTHROPIC_BASE_URL").is_none()); + assert!(claude["env"] + .get(CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY_ENV) + .is_none()); assert_eq!(claude["env"]["KEEP_ENV"], "yes"); assert_eq!(claude["keep"], "claude"); diff --git a/src/assets/icons/zcode.png b/src/assets/icons/zcode.png new file mode 100644 index 0000000..366b1fa Binary files /dev/null and b/src/assets/icons/zcode.png differ diff --git a/src/pages/AgentsPage.tsx b/src/pages/AgentsPage.tsx index 659d047..9a10840 100644 --- a/src/pages/AgentsPage.tsx +++ b/src/pages/AgentsPage.tsx @@ -32,6 +32,7 @@ import kimiIcon from '../assets/icons/kimi-light.svg'; import openclawIcon from '../assets/icons/openclaw.svg'; import opencodeIcon from '../assets/icons/opencode.svg'; import piIcon from '../assets/icons/pi-logo-on-light.svg'; +import zcodeIcon from '../assets/icons/zcode.png'; import { agentModelAlias, filterAgentModels, @@ -79,6 +80,7 @@ type AgentConfigStatus = { pluginVersion: string | null; configValid: boolean; configured: boolean; + configurationSynchronized: boolean; currentModel: string | null; oauthConfiguration: boolean; modificationEnabled: boolean; @@ -205,12 +207,42 @@ const agentDefinitions: AgentDefinition[] = [ icon: codexIcon, descriptionKey: 'agents.description.codex', }, + { + id: 'deepseek-harness', + name: 'DeepSeek Harness', + icon: deepseekIcon, + descriptionKey: 'agents.description.deepseekHarness', + }, { id: 'opencode', name: 'OpenCode', icon: opencodeIcon, descriptionKey: 'agents.description.opencode', }, + { + id: 'pi', + name: 'Pi', + icon: piIcon, + descriptionKey: 'agents.description.pi', + }, + { + id: 'grok-build', + name: 'Grok Build', + icon: grokIcon, + descriptionKey: 'agents.description.grokBuild', + }, + { + id: 'zcode', + name: 'ZCode', + icon: zcodeIcon, + descriptionKey: 'agents.description.zcode', + }, + { + id: 'kimi-code', + name: 'Kimi Code', + icon: kimiIcon, + descriptionKey: 'agents.description.kimiCode', + }, { id: 'openclaw', name: 'OpenClaw', @@ -223,36 +255,6 @@ const agentDefinitions: AgentDefinition[] = [ icon: hermesIcon, descriptionKey: 'agents.description.hermes', }, - { - id: 'deepseek-harness', - name: 'DeepSeek Harness', - icon: deepseekIcon, - descriptionKey: 'agents.description.deepseekHarness', - }, - { - id: 'zcode', - name: 'ZCode', - Icon: Bot, - descriptionKey: 'agents.description.zcode', - }, - { - id: 'kimi-code', - name: 'Kimi Code', - icon: kimiIcon, - descriptionKey: 'agents.description.kimiCode', - }, - { - id: 'grok-build', - name: 'Grok Build', - icon: grokIcon, - descriptionKey: 'agents.description.grokBuild', - }, - { - id: 'pi', - name: 'Pi', - icon: piIcon, - descriptionKey: 'agents.description.pi', - }, ]; const agentSubpages: AgentSubpageDefinition[] = [ @@ -885,7 +887,7 @@ export function AgentsPage() { const configurationAction = resolveAgentConfigurationAction({ client: selected, modificationState: activeStatus?.modificationState ?? 'unconfigured', - configurationSynchronized: Boolean(activeStatus?.configured), + configurationSynchronized: Boolean(activeStatus?.configurationSynchronized), selectedModel, appliedModel, oauthConfiguration, diff --git a/src/services/agentConfigurationDraft.ts b/src/services/agentConfigurationDraft.ts index a8bf8cb..000330e 100644 --- a/src/services/agentConfigurationDraft.ts +++ b/src/services/agentConfigurationDraft.ts @@ -93,7 +93,10 @@ export function resolveAgentConfigurationAction({ }: ResolveAgentConfigurationActionOptions): AgentConfigurationAction { if (modificationState !== 'applied') return 'apply'; if (client === 'pi') return 'close'; - if (client === 'zcode' && !configurationSynchronized) return 'update'; + if ( + (client === 'zcode' || client === 'kimi-code' || client === 'grok-build') + && !configurationSynchronized + ) return 'update'; const modelChanged = client === 'claude-code' || client === 'claude-desktop' ? !sameAgentModelMappings(modelMappings, appliedModelMappings) diff --git a/tests/agentModelPicker.test.ts b/tests/agentModelPicker.test.ts index ab709df..269f9ad 100644 --- a/tests/agentModelPicker.test.ts +++ b/tests/agentModelPicker.test.ts @@ -175,6 +175,23 @@ describe('agent configuration update action', () => { })).toBe('update'); }); + test.each(['kimi-code', 'grok-build'])( + '%s updates an applied configuration whose CPA context catalog is stale', + (client) => { + expect(resolveAgentConfigurationAction({ + client, + modificationState: 'applied', + configurationSynchronized: false, + selectedModel: 'model-a', + appliedModel: 'model-a', + oauthConfiguration: false, + appliedOauthConfiguration: false, + modelMappings: { opus: '', sonnet: '', haiku: '' }, + appliedModelMappings: { opus: '', sonnet: '', haiku: '' }, + })).toBe('update'); + }, + ); + test('Codex also updates when only OAuth configuration changes', () => { expect(resolveAgentConfigurationAction({ client: 'codex',