From 0db0cc65d67326baccb890a57fefca135a81fbff Mon Sep 17 00:00:00 2001 From: lex00 <121451605+lex00@users.noreply.github.com> Date: Mon, 26 Jan 2026 16:56:55 -0700 Subject: [PATCH] Handle removed `IconName` variants in text thread deserialization (#47624) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #41776 ## Problem Old AI Text Thread sessions fail to open from History because the deserializer fails on unknown icon variants. When icons are removed or renamed in refactors, old saved threads become unloadable with errors like: ``` unknown variant `AtSign`, expected one of `Ai`, `AiAnthropic`... ``` ## Solution Added a lenient deserializer for the `icon` field in `SlashCommandOutputSection` that falls back to `IconName::Code` for unknown variants. This ensures old saved threads remain loadable even as icons are added/removed from the codebase. ## Test Plan - Added unit test for valid icon deserialization - Added unit test for unknown icon fallback to `Code` - Added unit test for various unknown icon variants - Added unit test confirming serialization unchanged - All tests pass: `cargo test -p assistant_slash_command` Release Notes: - Fixed old AI text thread sessions failing to open from History when they contain icons that were removed in previous updates. 🤖 Generated with [Claude Code](https://claude.ai/code) --------- Co-authored-by: Claude Opus 4.5 Co-authored-by: MrSubidubi --- .../src/assistant_slash_command.rs | 46 ++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/crates/assistant_slash_command/src/assistant_slash_command.rs b/crates/assistant_slash_command/src/assistant_slash_command.rs index 2e6bb7325e14ac109d77854e1d848c541a685458..883f2f98c2693be5ae915cdab53ea94786222341 100644 --- a/crates/assistant_slash_command/src/assistant_slash_command.rs +++ b/crates/assistant_slash_command/src/assistant_slash_command.rs @@ -13,7 +13,7 @@ use language::CodeLabelBuilder; use language::HighlightId; use language::{BufferSnapshot, CodeLabel, LspAdapterDelegate, OffsetRangeExt}; pub use language_model::Role; -use serde::{Deserialize, Serialize}; +use serde::{Deserialize, Deserializer, Serialize}; use std::{ ops::Range, sync::{Arc, atomic::AtomicBool}, @@ -21,6 +21,18 @@ use std::{ use ui::ActiveTheme; use workspace::{Workspace, ui::IconName}; +/// Deserializes IconName, falling back to Code for unknown variants. +/// This handles old saved data that may contain removed or renamed icon variants. +fn deserialize_icon_with_fallback<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + Ok(String::deserialize(deserializer) + .ok() + .and_then(|string| serde_json::from_value(serde_json::Value::String(string)).ok()) + .unwrap_or(IconName::Code)) +} + pub fn init(cx: &mut App) { SlashCommandRegistry::default_global(cx); extension_slash_command::init(cx); @@ -256,6 +268,7 @@ impl SlashCommandOutput { #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct SlashCommandOutputSection { pub range: Range, + #[serde(deserialize_with = "deserialize_icon_with_fallback")] pub icon: IconName, pub label: SharedString, pub metadata: Option, @@ -570,4 +583,35 @@ mod tests { assert_eq!(new_output, output); } } + + #[test] + fn test_deserialize_with_valid_icon_pascal_case() { + // Test that PascalCase icons (serde default) deserialize correctly + let json = json!({ + "range": { + "start": 0, + "end": 5 + }, + "icon": "AcpRegistry", + "label": "Test", + "metadata": null + }); + let section: SlashCommandOutputSection = serde_json::from_value(json).unwrap(); + assert_eq!(section.icon, IconName::AcpRegistry); + } + #[test] + fn test_deserialize_with_unknown_icon() { + // Test that unknown icon variants fall back to Code + let json = json!({ + "range": { + "start": 0, + "end": 5 + }, + "icon": "removed_icon", + "label": "Old Icon", + "metadata": null + }); + let section: SlashCommandOutputSection = serde_json::from_value(json).unwrap(); + assert_eq!(section.icon, IconName::Code); + } }