From f2f3db7e2b3db6a7da430c30241002afc666b332 Mon Sep 17 00:00:00 2001 From: Taylor Ho Date: Tue, 23 Jun 2026 22:51:04 -0700 Subject: [PATCH] refactor(agents): supersede #1202 avatar-clear with derive-once Restack derive-once onto the #1202 avatar-plumbing branch and subtract #1202's avatar-clear mechanism. The avatar is a command-derived default icon, so it is derived once at create (resolve_created_avatar_url) and stored as a plain Option; there is no reconcile-time backfill to guard against. Removed: - avatar_url_cleared: bool field and all initializer/test sites - profile_sync_avatar_url .or_else(managed_agent_avatar_url) re-derive - dead Fizz-retirement / legacy-recovery / command-fallback helpers - now-unused app: &AppHandle param on reconcile_agent_profile Leaves #1202's non-avatar plumbing intact (persona/team model fields, app-avatar: prefix parsing, imported app-avatar handling). All three icon-resurrection vectors are now gone. Co-authored-by: Taylor Ho Signed-off-by: Taylor Ho --- desktop/scripts/check-file-sizes.mjs | 7 +- .../src-tauri/src/commands/agent_models.rs | 40 +--- desktop/src-tauri/src/commands/agents.rs | 212 ++---------------- .../src-tauri/src/commands/agents_tests.rs | 124 ---------- desktop/src-tauri/src/managed_agents/nest.rs | 1 - .../src/managed_agents/relay_mesh.rs | 1 - .../src-tauri/src/managed_agents/restore.rs | 9 +- .../src/managed_agents/runtime/tests.rs | 1 - .../src/managed_agents/team_repair.rs | 1 - desktop/src-tauri/src/managed_agents/types.rs | 9 - 10 files changed, 22 insertions(+), 383 deletions(-) diff --git a/desktop/scripts/check-file-sizes.mjs b/desktop/scripts/check-file-sizes.mjs index 4a6ddb436..261ee3db9 100644 --- a/desktop/scripts/check-file-sizes.mjs +++ b/desktop/scripts/check-file-sizes.mjs @@ -30,7 +30,7 @@ const rules = [ // Do not add to this list; split the file instead. Remove each entry as its // file is broken up. Tracked as a follow-up. const overrides = new Map([ - ["src-tauri/src/commands/agents.rs", 1294], + ["src-tauri/src/commands/agents.rs", 1110], // Residual repos_dir integration in ensure_nest_at: REPOS is provisioned // outside NEST_DIRS (it may be a symlink), so it needs its own create + // chmod-only-when-real-dir handling plus integration test coverage. The @@ -41,11 +41,6 @@ const overrides = new Map([ ["src-tauri/src/managed_agents/runtime.rs", 1953], ["src-tauri/src/managed_agents/personas.rs", 1080], ["src-tauri/src/managed_agents/persona_card.rs", 1050], - // avatar_url_cleared flag + app-avatar ref plumbing pushed this over the - // 1000-line cap once the branch caught up with main — a small overage from - // load-bearing avatar persistence plumbing, not generic debt growth. - // Approved override; still queued to split with the rest of this list. - ["src-tauri/src/managed_agents/types.rs", 1002], // applyWorkspace reposDir parameter threaded through the Tauri invoke for // configurable repos_dir — a 3-line overage from load-bearing parameter // plumbing, not generic debt growth. Approved override; still queued to split. diff --git a/desktop/src-tauri/src/commands/agent_models.rs b/desktop/src-tauri/src/commands/agent_models.rs index 261f501a6..cf17d241c 100644 --- a/desktop/src-tauri/src/commands/agent_models.rs +++ b/desktop/src-tauri/src/commands/agent_models.rs @@ -7,11 +7,10 @@ use crate::{ app_state::AppState, managed_agents::{ build_managed_agent_summary, default_agent_workdir, find_managed_agent_mut, - known_acp_runtime, load_managed_agents, load_personas, managed_agent_avatar_url, - missing_command_message, normalize_agent_args, resolve_command, - resolve_effective_prompt_model_provider, save_managed_agents, sync_managed_agent_processes, - try_regenerate_nest, AgentModelInfo, AgentModelsResponse, ManagedAgentRecord, - UpdateManagedAgentRequest, UpdateManagedAgentResponse, + known_acp_runtime, load_managed_agents, load_personas, missing_command_message, + normalize_agent_args, resolve_command, resolve_effective_prompt_model_provider, + save_managed_agents, sync_managed_agent_processes, try_regenerate_nest, AgentModelInfo, + AgentModelsResponse, UpdateManagedAgentRequest, UpdateManagedAgentResponse, }, relay::{relay_ws_url_with_override, sync_managed_agent_profile}, util::now_iso, @@ -23,27 +22,6 @@ fn trim_optional(value: Option) -> Option { .filter(|s| !s.is_empty()) } -fn is_persona_runtime_avatar(record: &ManagedAgentRecord, avatar_url: &str) -> bool { - record.persona_id.is_some() - && managed_agent_avatar_url(&record.agent_command) - .as_deref() - .is_some_and(|runtime_avatar_url| runtime_avatar_url == avatar_url.trim()) -} - -fn profile_sync_avatar_url(record: &ManagedAgentRecord) -> Option { - record - .avatar_url - .clone() - .filter(|avatar_url| !is_persona_runtime_avatar(record, avatar_url)) - .or_else(|| { - if record.persona_id.is_none() { - managed_agent_avatar_url(&record.agent_command) - } else { - None - } - }) -} - /// Query available models from an agent via `buzz-acp models --json`. /// /// Spawns a short-lived subprocess (no relay connection needed). The subprocess @@ -200,10 +178,8 @@ pub async fn update_managed_agent( } if let Some(avatar_update) = input.avatar_url { let normalized = trim_optional(avatar_update); - let avatar_url_cleared = normalized.is_none(); - if normalized != record.avatar_url || avatar_url_cleared != record.avatar_url_cleared { + if normalized != record.avatar_url { record.avatar_url = normalized; - record.avatar_url_cleared = avatar_url_cleared; avatar_changed = true; } } @@ -288,11 +264,7 @@ pub async fn update_managed_agent( .map_err(|e| format!("failed to parse agent keys: {e}"))?; let relay_url = record.relay_url.clone(); let display_name = record.name.clone(); - let avatar_url = if avatar_changed || record.avatar_url_cleared { - record.avatar_url.clone() - } else { - profile_sync_avatar_url(record) - }; + let avatar_url = record.avatar_url.clone(); let auth_tag = record.auth_tag.clone(); Some((agent_keys, relay_url, display_name, avatar_url, auth_tag)) } else { diff --git a/desktop/src-tauri/src/commands/agents.rs b/desktop/src-tauri/src/commands/agents.rs index 8d65b3ea8..aec952bd4 100644 --- a/desktop/src-tauri/src/commands/agents.rs +++ b/desktop/src-tauri/src/commands/agents.rs @@ -78,28 +78,6 @@ fn resolve_created_avatar_url( }) } -fn is_retired_fizz_data_url(persona_id: Option<&str>, avatar_url: &str) -> bool { - persona_id == Some("builtin:fizz") && avatar_url.trim_start().starts_with("data:image/") -} - -fn is_command_avatar_for_persona( - persona_id: Option<&str>, - agent_command: &str, - avatar_url: &str, -) -> bool { - persona_id.is_some() - && managed_agent_avatar_url(agent_command) - .as_deref() - .is_some_and(|command_avatar_url| command_avatar_url == avatar_url.trim()) -} - -fn filter_retired_fizz_avatar( - persona_id: Option<&str>, - avatar_url: Option, -) -> Option { - avatar_url.filter(|url| !is_retired_fizz_data_url(persona_id, url)) -} - #[cfg(feature = "mesh-llm")] async fn ensure_relay_mesh_for_record( app: &AppHandle, @@ -551,7 +529,6 @@ pub async fn create_managed_agent( auth_tag: auth_tag.clone(), relay_url: resolved_relay_url.clone(), avatar_url: resolved_avatar_url.clone(), - avatar_url_cleared: false, acp_command: input .acp_command .as_deref() @@ -763,22 +740,10 @@ pub(crate) struct ProfileReconcileData { pub(crate) private_key_nsec: String, pub(crate) name: String, pub(crate) relay_url: String, - /// Expected avatar URL for the published profile. `None` can mean either a - /// legacy missing value or an explicit clear; `avatar_url_cleared` - /// disambiguates those cases. + /// Expected avatar URL for the published profile. Derived once at creation + /// and stored verbatim; reconciliation republishes it as-is. pub(crate) avatar_url: Option, - pub(crate) avatar_url_cleared: bool, pub(crate) auth_tag: Option, - /// The agent's pubkey (hex). Needed to update the persisted record during - /// avatar backfill migration. - pub(crate) pubkey: String, - /// The agent's command (e.g. "goose"). Used as fallback when no profile - /// exists on the relay during avatar backfill. - pub(crate) agent_command: String, - /// Persona ID if this agent was created from a persona. Used during avatar - /// backfill to recover the correct avatar from the persona record when the - /// relay profile has been corrupted. - pub(crate) persona_id: Option, } #[tauri::command] @@ -823,11 +788,7 @@ pub async fn start_managed_agent( name: record.name.clone(), relay_url: record.relay_url.clone(), avatar_url: record.avatar_url.clone(), - avatar_url_cleared: record.avatar_url_cleared, auth_tag: record.auth_tag.clone(), - pubkey: record.pubkey.clone(), - agent_command: record.agent_command.clone(), - persona_id: record.persona_id.clone(), }; let target = if record.backend == BackendKind::Local { @@ -888,8 +849,8 @@ pub async fn start_managed_agent( // ── Profile reconciliation (fire-and-forget) ──────────────────────────── // On successful start, spawn a background task to ensure the agent's kind:0 // profile is published on the relay. This self-heals cases where the initial - // profile sync at creation time failed silently. For legacy records (pre-PR-921) - // with no persisted avatar, this also backfills the avatar from the relay. + // profile sync at creation time failed silently. The avatar derived once at + // creation is published verbatim — there is no reconcile-time backfill. if result.is_ok() { let reconcile_pubkey = pubkey.clone(); let reconcile_app = app.clone(); @@ -897,8 +858,7 @@ pub async fn start_managed_agent( use tauri::Manager; let state = reconcile_app.state::(); if let Err(e) = - reconcile_agent_profile(&state, &reconcile_app, &reconcile_pubkey, &reconcile_data) - .await + reconcile_agent_profile(&state, &reconcile_pubkey, &reconcile_data).await { eprintln!( "buzz-desktop: profile reconciliation failed for agent {reconcile_pubkey}: {e}" @@ -910,59 +870,19 @@ pub async fn start_managed_agent( result } -/// Resolve the avatar to backfill for a legacy agent record (pre-PR-921, no -/// stored `avatar_url`). -/// -/// Priority: the persona's avatar wins, because the old reconciliation code -/// could have overwritten the relay's kind:0 `picture` with the command default -/// — making the relay an unreliable source for persona-backed agents. Only fall -/// back to the relay's `picture`, then the command icon, for agents with no -/// persona avatar to recover from. -fn resolve_legacy_avatar( - persona_avatar: Option, - relay_picture: Option, - agent_command: &str, - use_command_fallback: bool, -) -> String { - persona_avatar - .or(relay_picture) - .or_else(|| { - if use_command_fallback { - managed_agent_avatar_url(agent_command) - } else { - None - } - }) - .unwrap_or_default() -} - -fn should_skip_legacy_command_avatar( - stored_avatar_was_retired_fizz: bool, - relay_picture_was_retired_fizz: bool, - persona_avatar: Option<&str>, - relay_picture: Option<&str>, -) -> bool { - (stored_avatar_was_retired_fizz || relay_picture_was_retired_fizz) - && persona_avatar.is_none() - && relay_picture.is_none() -} - /// Reconcile an agent's kind:0 profile on the relay. /// /// Queries the relay for the agent's existing profile and re-publishes if missing /// or stale. This is fire-and-forget — errors are returned to the caller for /// logging but never block agent startup. /// -/// For legacy records (pre-PR-921) where `avatar_url` is `None`, this function -/// backfills via `resolve_legacy_avatar` — preferring the persona record's avatar -/// over the relay's `picture`, since the old code may have corrupted the relay -/// profile — and persists the updated record. After backfill, normal -/// Query and publish both target the agent's stored `relay_url` so that, under -/// an active workspace relay override, reconciliation reads and writes the same -/// relay the agent's profile actually lives on. +/// The avatar is derived once at creation and stored on the record; reconcile +/// publishes that stored value verbatim with no backfill. Query and publish both +/// target the agent's stored `relay_url` so that, under an active workspace relay +/// override, reconciliation reads and writes the same relay the agent's profile +/// actually lives on. pub(crate) async fn reconcile_agent_profile( state: &AppState, - app: &AppHandle, agent_pubkey: &str, data: &ProfileReconcileData, ) -> Result<(), String> { @@ -971,114 +891,10 @@ pub(crate) async fn reconcile_agent_profile( // Query the relay for the agent's existing kind:0 profile. let existing = query_agent_profile(state, &data.relay_url, agent_pubkey).await?; - // Resolve the expected avatar. A user-initiated clear is intentionally - // `None` and must not be backfilled from persona/relay/runtime defaults. - // For legacy records that have no stored avatar_url yet, `None` still means - // backfill from the best available historical source. - let stored_avatar = - filter_retired_fizz_avatar(data.persona_id.as_deref(), data.avatar_url.clone()); - let stored_avatar_was_retired_fizz = data - .avatar_url - .as_deref() - .is_some_and(|url| is_retired_fizz_data_url(data.persona_id.as_deref(), url)); - let stored_avatar_was_command_fallback = stored_avatar.as_deref().is_some_and(|url| { - is_command_avatar_for_persona(data.persona_id.as_deref(), &data.agent_command, url) - }); - let stored_avatar = stored_avatar.filter(|url| { - !is_command_avatar_for_persona(data.persona_id.as_deref(), &data.agent_command, url) - }); - let expected_avatar = if data.avatar_url_cleared && stored_avatar.is_none() { - None - } else { - match stored_avatar { - Some(url) => Some(url.to_string()), - None => { - // Legacy record: the relay profile may have been corrupted by the - // old reconciliation code (it overwrote the persona avatar with the - // command default), so the persona record is the authoritative source. - let persona_avatar = filter_retired_fizz_avatar( - data.persona_id.as_deref(), - data.persona_id.as_ref().and_then(|pid| { - load_personas(app) - .ok()? - .into_iter() - .find(|p| p.id == *pid)? - .avatar_url - }), - ); - let relay_picture_raw = existing.as_ref().and_then(|info| info.picture.clone()); - let relay_picture_was_retired_fizz = relay_picture_raw - .as_deref() - .is_some_and(|url| is_retired_fizz_data_url(data.persona_id.as_deref(), url)); - let relay_picture = - filter_retired_fizz_avatar(data.persona_id.as_deref(), relay_picture_raw); - let relay_picture_was_command_fallback = - relay_picture.as_deref().is_some_and(|url| { - is_command_avatar_for_persona( - data.persona_id.as_deref(), - &data.agent_command, - url, - ) - }); - let relay_picture = relay_picture.filter(|url| { - !is_command_avatar_for_persona( - data.persona_id.as_deref(), - &data.agent_command, - url, - ) - }); - - let skip_command_fallback = should_skip_legacy_command_avatar( - stored_avatar_was_retired_fizz, - relay_picture_was_retired_fizz, - persona_avatar.as_deref(), - relay_picture.as_deref(), - ); - let backfilled = if skip_command_fallback { - String::new() - } else { - resolve_legacy_avatar( - persona_avatar, - relay_picture, - &data.agent_command, - data.persona_id.is_none(), - ) - }; - - // Persist the backfilled avatar so this migration only runs once, - // or clear the retired built-in Fizz data URL if there is no - // current profile image to backfill. - let should_persist_avatar = stored_avatar_was_retired_fizz - || relay_picture_was_retired_fizz - || stored_avatar_was_command_fallback - || relay_picture_was_command_fallback - || (!backfilled.is_empty() - && data.avatar_url.as_deref() != Some(backfilled.as_str())); - if should_persist_avatar { - let _store_guard = state - .managed_agents_store_lock - .lock() - .map_err(|e| e.to_string())?; - let mut records = load_managed_agents(app)?; - if let Some(record) = records.iter_mut().find(|r| r.pubkey == data.pubkey) { - record.avatar_url = if backfilled.is_empty() { - None - } else { - Some(backfilled.clone()) - }; - record.avatar_url_cleared = backfilled.is_empty(); - save_managed_agents(app, &records)?; - } - } - - if backfilled.is_empty() { - None - } else { - Some(backfilled) - } - } - } - }; + // Republish the avatar exactly as derived once at creation. There is no + // reconcile-time backfill: a stored `None` (including an explicit clear) + // is published verbatim. + let expected_avatar = data.avatar_url.clone(); if !profile_needs_sync(existing.as_ref(), &data.name, expected_avatar.as_deref()) { return Ok(()); diff --git a/desktop/src-tauri/src/commands/agents_tests.rs b/desktop/src-tauri/src/commands/agents_tests.rs index a13be10ff..22bed61fc 100644 --- a/desktop/src-tauri/src/commands/agents_tests.rs +++ b/desktop/src-tauri/src/commands/agents_tests.rs @@ -82,33 +82,6 @@ fn created_persona_avatar_does_not_use_command_fallback() { assert_eq!(resolved, None); } -#[test] -fn retired_fizz_data_url_is_treated_as_absent() { - assert_eq!( - filter_retired_fizz_avatar( - Some("builtin:fizz"), - Some("data:image/png;base64,old-demo".to_string()), - ), - None, - ); - assert_eq!( - filter_retired_fizz_avatar( - Some("custom:fizz"), - Some("data:image/png;base64,user-avatar".to_string()), - ) - .as_deref(), - Some("data:image/png;base64,user-avatar"), - ); - assert_eq!( - filter_retired_fizz_avatar( - Some("builtin:fizz"), - Some("https://relay.example/avatar.png".to_string()), - ) - .as_deref(), - Some("https://relay.example/avatar.png"), - ); -} - fn profile(name: Option<&str>, picture: Option<&str>) -> crate::relay::AgentProfileInfo { crate::relay::AgentProfileInfo { display_name: name.map(str::to_string), @@ -172,100 +145,3 @@ fn profile_needs_sync_when_expected_avatar_absent_but_published() { let existing = profile(Some("Duncan"), Some("https://x/a.png")); assert!(profile_needs_sync(Some(&existing), "Duncan", None)); } - -#[test] -fn legacy_avatar_prefers_persona_over_corrupted_relay_picture() { - // The regression: the relay picture was overwritten with the command - // default. The persona avatar must win so the correct avatar is restored. - let resolved = resolve_legacy_avatar( - Some("https://x/persona.png".to_string()), - Some("https://x/default-icon.png".to_string()), - "goose", - false, - ); - - assert_eq!(resolved, "https://x/persona.png"); -} - -#[test] -fn legacy_avatar_falls_back_to_relay_picture_without_persona() { - let resolved = resolve_legacy_avatar( - None, - Some("https://x/relay.png".to_string()), - "goose", - false, - ); - - assert_eq!(resolved, "https://x/relay.png"); -} - -#[test] -fn legacy_avatar_falls_back_to_command_icon_when_no_persona_or_relay() { - use crate::managed_agents::managed_agent_avatar_url; - - let resolved = resolve_legacy_avatar(None, None, "goose", true); - - assert_eq!(resolved, managed_agent_avatar_url("goose").unwrap()); -} - -#[test] -fn legacy_avatar_empty_when_nothing_resolves() { - let resolved = resolve_legacy_avatar(None, None, "totally-unknown-command", true); - - assert!(resolved.is_empty()); -} - -#[test] -fn legacy_persona_avatar_does_not_use_command_fallback() { - let resolved = resolve_legacy_avatar(None, None, "goose", false); - - assert!(resolved.is_empty()); -} - -#[test] -fn detects_command_avatar_for_persona_agents() { - let command_avatar = crate::managed_agents::managed_agent_avatar_url("goose") - .expect("goose avatar should resolve"); - - assert!(is_command_avatar_for_persona( - Some("builtin:fizz"), - "goose", - &command_avatar, - )); - assert!(!is_command_avatar_for_persona( - None, - "goose", - &command_avatar, - )); - assert!(!is_command_avatar_for_persona( - Some("builtin:fizz"), - "goose", - "https://x/fizz.png", - )); -} - -#[test] -fn legacy_avatar_skips_command_icon_for_retired_stored_fizz_avatar() { - assert!(should_skip_legacy_command_avatar(true, false, None, None)); -} - -#[test] -fn legacy_avatar_skips_command_icon_for_retired_relay_fizz_avatar() { - assert!(should_skip_legacy_command_avatar(false, true, None, None)); -} - -#[test] -fn legacy_avatar_keeps_command_icon_when_retired_fizz_has_current_avatar_source() { - assert!(!should_skip_legacy_command_avatar( - false, - true, - Some("https://x/persona.png"), - None, - )); - assert!(!should_skip_legacy_command_avatar( - false, - true, - None, - Some("https://x/relay.png"), - )); -} diff --git a/desktop/src-tauri/src/managed_agents/nest.rs b/desktop/src-tauri/src/managed_agents/nest.rs index b822e3d41..9fec60d6d 100644 --- a/desktop/src-tauri/src/managed_agents/nest.rs +++ b/desktop/src-tauri/src/managed_agents/nest.rs @@ -993,7 +993,6 @@ mod tests { auth_tag: None, relay_url: String::new(), avatar_url: None, - avatar_url_cleared: false, acp_command: String::new(), agent_command: String::new(), agent_args: vec![], diff --git a/desktop/src-tauri/src/managed_agents/relay_mesh.rs b/desktop/src-tauri/src/managed_agents/relay_mesh.rs index 6924e5187..808546aa4 100644 --- a/desktop/src-tauri/src/managed_agents/relay_mesh.rs +++ b/desktop/src-tauri/src/managed_agents/relay_mesh.rs @@ -69,7 +69,6 @@ mod tests { auth_tag: Some("tag".into()), relay_url: "ws://localhost:3000".into(), avatar_url: None, - avatar_url_cleared: false, acp_command: "buzz-acp".into(), agent_command: "goose".into(), agent_args: vec![], diff --git a/desktop/src-tauri/src/managed_agents/restore.rs b/desktop/src-tauri/src/managed_agents/restore.rs index 410f0ed62..0e4f72375 100644 --- a/desktop/src-tauri/src/managed_agents/restore.rs +++ b/desktop/src-tauri/src/managed_agents/restore.rs @@ -210,11 +210,7 @@ pub async fn restore_managed_agents_on_launch( name: record.name.clone(), relay_url: record.relay_url.clone(), avatar_url: record.avatar_url.clone(), - avatar_url_cleared: record.avatar_url_cleared, auth_tag: record.auth_tag.clone(), - pubkey: record.pubkey.clone(), - agent_command: record.agent_command.clone(), - persona_id: record.persona_id.clone(), }, )) }) @@ -229,10 +225,7 @@ pub async fn restore_managed_agents_on_launch( let reconcile_app = app.clone(); tauri::async_runtime::spawn(async move { let state = reconcile_app.state::(); - if let Err(e) = - crate::commands::reconcile_agent_profile(&state, &reconcile_app, &pubkey, &data) - .await - { + if let Err(e) = crate::commands::reconcile_agent_profile(&state, &pubkey, &data).await { eprintln!("buzz-desktop: profile reconciliation failed for agent {pubkey}: {e}"); } }); diff --git a/desktop/src-tauri/src/managed_agents/runtime/tests.rs b/desktop/src-tauri/src/managed_agents/runtime/tests.rs index 6be96057c..f8a922da5 100644 --- a/desktop/src-tauri/src/managed_agents/runtime/tests.rs +++ b/desktop/src-tauri/src/managed_agents/runtime/tests.rs @@ -130,7 +130,6 @@ fn fixture( auth_tag, relay_url: "ws://localhost:3000".into(), avatar_url: None, - avatar_url_cleared: false, acp_command: "buzz-acp".into(), agent_command: "goose".into(), agent_args: vec![], diff --git a/desktop/src-tauri/src/managed_agents/team_repair.rs b/desktop/src-tauri/src/managed_agents/team_repair.rs index 002b0dfd7..d9e272066 100644 --- a/desktop/src-tauri/src/managed_agents/team_repair.rs +++ b/desktop/src-tauri/src/managed_agents/team_repair.rs @@ -279,7 +279,6 @@ mod tests { auth_tag: None, relay_url: String::new(), avatar_url: None, - avatar_url_cleared: false, acp_command: String::new(), agent_command: String::new(), agent_args: vec![], diff --git a/desktop/src-tauri/src/managed_agents/types.rs b/desktop/src-tauri/src/managed_agents/types.rs index 3b894d8ec..e3372cdbe 100644 --- a/desktop/src-tauri/src/managed_agents/types.rs +++ b/desktop/src-tauri/src/managed_agents/types.rs @@ -107,9 +107,6 @@ pub struct ManagedAgentRecord { /// `#[serde(default)]` so pre-existing records deserialize as `None`. #[serde(default)] pub avatar_url: Option, - /// True when `avatar_url: None` came from an explicit user clear. - #[serde(default)] - pub avatar_url_cleared: bool, pub acp_command: String, pub agent_command: String, pub agent_args: Vec, @@ -695,13 +692,7 @@ mod tests { assert_eq!(record.auth_tag, None); assert_eq!(record.avatar_url, None); - assert!(!record.avatar_url_cleared); assert_eq!(record.pubkey, "abcd1234"); - - let mut value = serde_json::to_value(&record).expect("should serialize"); - value["avatar_url_cleared"] = true.into(); - let cleared: ManagedAgentRecord = serde_json::from_value(value).unwrap(); - assert!(cleared.avatar_url_cleared); } /// Agent records WITH an auth_tag round-trip correctly through serde.