diff --git a/desktop/src-tauri/src/managed_agents/discovery/catalog.rs b/desktop/src-tauri/src/managed_agents/discovery/catalog.rs index fecf792f214..42b84a59f40 100644 --- a/desktop/src-tauri/src/managed_agents/discovery/catalog.rs +++ b/desktop/src-tauri/src/managed_agents/discovery/catalog.rs @@ -65,7 +65,10 @@ pub(crate) 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, + // Claude Code reads ANTHROPIC_MODEL at process start (same key readiness + // already maps for anthropic). Without this, Edit Agent model picks never + // reach the spawned process (#2692). + model_env_var: Some(crate::managed_agents::ANTHROPIC_MODEL_ENV_KEY), 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 7121151cd47..e6ebc4ef90c 100644 --- a/desktop/src-tauri/src/managed_agents/discovery/tests.rs +++ b/desktop/src-tauri/src/managed_agents/discovery/tests.rs @@ -637,7 +637,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 00000000000..5cf03994c1b --- /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 6b12fbcd2be..fd9b09caed1 100644 --- a/desktop/src-tauri/src/managed_agents/env_vars.rs +++ b/desktop/src-tauri/src/managed_agents/env_vars.rs @@ -26,7 +26,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 dc38c3d126f..6e54fda02a7 100644 --- a/desktop/src-tauri/src/managed_agents/env_vars/tests.rs +++ b/desktop/src-tauri/src/managed_agents/env_vars/tests.rs @@ -442,9 +442,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] @@ -463,6 +463,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 7178b20a085..1e9a75529ce 100644 --- a/desktop/src-tauri/src/managed_agents/readiness.rs +++ b/desktop/src-tauri/src/managed_agents/readiness.rs @@ -493,7 +493,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"), Some("openrouter") => Some("OPENROUTER_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 68d8ad70472..0017f6752ca 100644 --- a/desktop/src-tauri/src/managed_agents/runtime/tests.rs +++ b/desktop/src-tauri/src/managed_agents/runtime/tests.rs @@ -524,13 +524,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/lib/agentCardAvatar.test.mjs b/desktop/src/features/agents/lib/agentCardAvatar.test.mjs index 5acd9ae109b..dcfcabbb3b4 100644 --- a/desktop/src/features/agents/lib/agentCardAvatar.test.mjs +++ b/desktop/src/features/agents/lib/agentCardAvatar.test.mjs @@ -23,6 +23,17 @@ test("running agent card falls back to the definition avatar", () => { ); }); +test("running agent card falls back to the managed agent avatar", () => { + assert.equal( + resolveAgentCardAvatarUrl( + null, + null, + " https://relay.example/managed.png ", + ), + "https://relay.example/managed.png", + ); +}); + test("running agent card ignores blank avatar values", () => { assert.equal(resolveAgentCardAvatarUrl(" ", ""), null); }); diff --git a/desktop/src/features/agents/lib/agentCardAvatar.ts b/desktop/src/features/agents/lib/agentCardAvatar.ts index 057c413daac..19216775755 100644 --- a/desktop/src/features/agents/lib/agentCardAvatar.ts +++ b/desktop/src/features/agents/lib/agentCardAvatar.ts @@ -8,8 +8,13 @@ export function resolveAgentCardAvatarUrl( profileAvatarUrl: string | null | undefined, personaAvatarUrl: string | null | undefined, + managedAgentAvatarUrl?: string | null | undefined, ): string | null { - for (const candidate of [profileAvatarUrl, personaAvatarUrl]) { + for (const candidate of [ + profileAvatarUrl, + managedAgentAvatarUrl, + personaAvatarUrl, + ]) { const trimmed = candidate?.trim(); if (trimmed) return trimmed; } diff --git a/desktop/src/features/agents/ui/UnifiedAgentsSection.tsx b/desktop/src/features/agents/ui/UnifiedAgentsSection.tsx index 0aac9be9a1d..7197ea5b04b 100644 --- a/desktop/src/features/agents/ui/UnifiedAgentsSection.tsx +++ b/desktop/src/features/agents/ui/UnifiedAgentsSection.tsx @@ -285,8 +285,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 - ? resolveAgentCardAvatarUrl(profileQuery.data?.avatarUrl, persona.avatarUrl) + ? resolveAgentCardAvatarUrl( + profileQuery.data?.avatarUrl, + persona.avatarUrl, + agent.avatarUrl, + ) : persona.avatarUrl; const friendlyError = agent ? friendlyAgentLastError(agent.lastError, agent.lastErrorCode)?.copy @@ -394,6 +400,13 @@ function StandaloneAgentCard({ const availability = getAvailability(agent.pubkey); 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 = resolveAgentCardAvatarUrl( + profileQuery.data?.avatarUrl, + undefined, + agent.avatarUrl, + ); const friendlyError = friendlyAgentLastError( agent.lastError, agent.lastErrorCode, @@ -407,7 +420,7 @@ function StandaloneAgentCard({ avatar={ } - avatarUrl={profileQuery.data?.avatarUrl} + avatarUrl={avatarUrl} dataTestId={`managed-agent-${agent.pubkey}`} footerAccessory={