diff --git a/desktop/src-tauri/src/managed_agents/discovery.rs b/desktop/src-tauri/src/managed_agents/discovery.rs index eecbf4de3e..bc6605e86c 100644 --- a/desktop/src-tauri/src/managed_agents/discovery.rs +++ b/desktop/src-tauri/src/managed_agents/discovery.rs @@ -122,7 +122,7 @@ const KNOWN_ACP_RUNTIMES: &[KnownAcpRuntime] = &[ adapter_install_hint: "Buzz talks to the Claude Code CLI through an ACP adapter. Install it with: npm install -g @agentclientprotocol/claude-agent-acp.", skill_dir: Some(".claude/skills"), supports_acp_model_switching: false, - model_env_var: None, + model_env_var: Some(crate::managed_agents::ANTHROPIC_MODEL_ENV_KEY), // #2692 provider_env_var: None, provider_locked: true, default_env: &[], diff --git a/desktop/src-tauri/src/managed_agents/discovery/tests.rs b/desktop/src-tauri/src/managed_agents/discovery/tests.rs index 1b587dca0e..c3335424d5 100644 --- a/desktop/src-tauri/src/managed_agents/discovery/tests.rs +++ b/desktop/src-tauri/src/managed_agents/discovery/tests.rs @@ -780,7 +780,7 @@ fn probe_codex_acp_version_parses_full_semver_output() { "adapter output must parse to its full semantic version" ); } - +mod claude_model_env; mod codex_version; #[cfg(unix)] diff --git a/desktop/src-tauri/src/managed_agents/discovery/tests/claude_model_env.rs b/desktop/src-tauri/src/managed_agents/discovery/tests/claude_model_env.rs new file mode 100644 index 0000000000..5cf03994c1 --- /dev/null +++ b/desktop/src-tauri/src/managed_agents/discovery/tests/claude_model_env.rs @@ -0,0 +1,9 @@ +/// The claude runtime must declare a model env channel — without one, the +/// model picked in the UI is persisted but never applied at spawn (#2692). +#[test] +fn claude_runtime_declares_anthropic_model_env_var() { + let claude = super::super::known_acp_runtime_exact("claude").expect("claude runtime registered"); + assert_eq!(claude.model_env_var, Some("ANTHROPIC_MODEL")); + assert!(claude.provider_locked); + assert_eq!(claude.provider_env_var, None); +} diff --git a/desktop/src-tauri/src/managed_agents/env_vars.rs b/desktop/src-tauri/src/managed_agents/env_vars.rs index 592a5cbbd9..69be4100ba 100644 --- a/desktop/src-tauri/src/managed_agents/env_vars.rs +++ b/desktop/src-tauri/src/managed_agents/env_vars.rs @@ -24,7 +24,14 @@ use std::collections::BTreeMap; /// /// Non-structured knobs (`GOOSE_TEMPERATURE`, `GOOSE_CONTEXT_LIMIT`) are NOT /// in this list — they have no structured counterpart and must be preserved. +/// Env var the Claude CLI honors as a session model override. Single source +/// of truth for the key — referenced by the claude runtime entry +/// (`discovery.rs`), the derived-key list below, and the readiness +/// provider-model mapping, so the three cannot drift apart. +pub(crate) const ANTHROPIC_MODEL_ENV_KEY: &str = "ANTHROPIC_MODEL"; + pub(crate) const DERIVED_PROVIDER_MODEL_ENV_KEYS: &[&str] = &[ + ANTHROPIC_MODEL_ENV_KEY, "GOOSE_MODEL", "GOOSE_PROVIDER", "BUZZ_AGENT_MODEL", diff --git a/desktop/src-tauri/src/managed_agents/env_vars/tests.rs b/desktop/src-tauri/src/managed_agents/env_vars/tests.rs index cf57b12546..fa2c7a42e5 100644 --- a/desktop/src-tauri/src/managed_agents/env_vars/tests.rs +++ b/desktop/src-tauri/src/managed_agents/env_vars/tests.rs @@ -418,9 +418,9 @@ fn merged_env_drops_oversize_value() { // ── derived provider/model key filter ────────────────────────────── // -// Pack import must strip derived env keys (GOOSE_MODEL, GOOSE_PROVIDER, -// BUZZ_AGENT_MODEL, BUZZ_AGENT_PROVIDER) so they don't shadow the -// structured AgentDefinition.model / AgentDefinition.provider fields after +// Pack import must strip derived env keys (ANTHROPIC_MODEL, GOOSE_MODEL, +// GOOSE_PROVIDER, BUZZ_AGENT_MODEL, BUZZ_AGENT_PROVIDER) so they don't shadow +// the structured AgentDefinition.model / AgentDefinition.provider fields after // the user edits them in the UI. #[test] @@ -439,6 +439,7 @@ fn is_derived_key_is_case_insensitive() { assert!(is_derived_provider_model_key("Goose_Provider")); assert!(is_derived_provider_model_key("buzz_agent_model")); assert!(is_derived_provider_model_key("BUZZ_AGENT_PROVIDER")); + assert!(is_derived_provider_model_key("anthropic_model")); } #[test] diff --git a/desktop/src-tauri/src/managed_agents/readiness.rs b/desktop/src-tauri/src/managed_agents/readiness.rs index c053d933c5..05e2244806 100644 --- a/desktop/src-tauri/src/managed_agents/readiness.rs +++ b/desktop/src-tauri/src/managed_agents/readiness.rs @@ -479,7 +479,7 @@ fn buzz_agent_requirements(effective: &EffectiveAgentEnv) -> Vec { Some("databricks") | Some("databricks_v2") | Some("databricks-v2") => { Some("DATABRICKS_MODEL") } - Some("anthropic") => Some("ANTHROPIC_MODEL"), + Some("anthropic") => Some(super::ANTHROPIC_MODEL_ENV_KEY), Some("openai") | Some("openai-compat") => Some("OPENAI_COMPAT_MODEL"), _ => None, }; diff --git a/desktop/src-tauri/src/managed_agents/runtime/tests.rs b/desktop/src-tauri/src/managed_agents/runtime/tests.rs index 3f6ee996f6..270c21e222 100644 --- a/desktop/src-tauri/src/managed_agents/runtime/tests.rs +++ b/desktop/src-tauri/src/managed_agents/runtime/tests.rs @@ -535,13 +535,13 @@ fn runtime_metadata_env_vars_injects_model_and_provider() { #[test] fn runtime_metadata_env_vars_skips_provider_when_locked() { let vars = runtime_metadata_env_vars( - None, // claude has no model_env_var - None, // claude has no provider_env_var + Some("ANTHROPIC_MODEL"), + None, true, // provider_locked = true Some("claude-opus-4-7"), Some("anthropic"), ); - assert!(vars.is_empty()); + assert_eq!(vars, vec![("ANTHROPIC_MODEL", "claude-opus-4-7")]); } #[test] diff --git a/desktop/src/features/agents/ui/UnifiedAgentsSection.tsx b/desktop/src/features/agents/ui/UnifiedAgentsSection.tsx index 34f9f9819f..6f0846feea 100644 --- a/desktop/src/features/agents/ui/UnifiedAgentsSection.tsx +++ b/desktop/src/features/agents/ui/UnifiedAgentsSection.tsx @@ -272,8 +272,14 @@ function AgentPersonaCard({ }); const isActive = agent ? isManagedAgentActive(agent) : false; const profileQuery = useUserProfileQuery(agent?.pubkey); + // Prefer the managed agent record — it survives restart even when the + // profile query cache is still empty (#2576). const avatarUrl = agent - ? firstAvatarUrl(persona.avatarUrl, profileQuery.data?.avatarUrl) + ? firstAvatarUrl( + agent.avatarUrl, + persona.avatarUrl, + profileQuery.data?.avatarUrl, + ) : persona.avatarUrl; const friendlyError = agent ? friendlyAgentLastError(agent.lastError, agent.lastErrorCode)?.copy @@ -361,6 +367,12 @@ function StandaloneAgentCard({ }) { const title = agent.name; const profileQuery = useUserProfileQuery(agent.pubkey); + // Prefer the managed agent record — it survives restart even when the + // profile query cache is still empty (#2576). + const avatarUrl = firstAvatarUrl( + agent.avatarUrl, + profileQuery.data?.avatarUrl, + ); const friendlyError = friendlyAgentLastError( agent.lastError, agent.lastErrorCode, @@ -374,7 +386,7 @@ function StandaloneAgentCard({ avatar={ onStartAgent(agent.pubkey)} /> } - avatarUrl={profileQuery.data?.avatarUrl} + avatarUrl={avatarUrl} dataTestId={`managed-agent-${agent.pubkey}`} label={title} modelLabel={resolveAgentCardModelLabel({