Handle removed `IconName` variants in text thread deserialization (#47624)

lex00 , Claude Opus 4.5 , and MrSubidubi created

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 <noreply@anthropic.com>
Co-authored-by: MrSubidubi <finn@zed.dev>

Change summary

crates/assistant_slash_command/src/assistant_slash_command.rs | 46 ++++
1 file changed, 45 insertions(+), 1 deletion(-)

Detailed changes

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<IconName, D::Error>
+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<T> {
     pub range: Range<T>,
+    #[serde(deserialize_with = "deserialize_icon_with_fallback")]
     pub icon: IconName,
     pub label: SharedString,
     pub metadata: Option<serde_json::Value>,
@@ -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<usize> = 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<usize> = serde_json::from_value(json).unwrap();
+        assert_eq!(section.icon, IconName::Code);
+    }
 }