agent_ui: Handle legacy agent enum variants during deserialization (#51125)

Add custom `Deserialize` implementations for `AgentType` and
`ExternalAgent` to map old built-in variant names to current custom
agent names, while still accepting current serialized formats.

Release Notes:

- N/A
This commit is contained in:
Ben Brandt authored and GitHub committed 2026-03-09 19:10:29 +01:00
1 parent c429048333
commit 5359ccf2f2
2 files changed
+227 -2

No files matched your search

+131 -1
View File
@@ -400,7 +400,7 @@ enum WhichFontSize {
}
// TODO unify this with ExternalAgent
#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
#[derive(Debug, Default, Clone, PartialEq, Serialize)]
pub enum AgentType {
#[default]
NativeAgent,
@@ -410,6 +410,63 @@ pub enum AgentType {
},
}
// Custom impl handles legacy variant names from before the built-in agents were moved to
// the registry: "ClaudeAgent" -> Custom { name: "claude-acp" }, "Codex" -> Custom { name:
// "codex-acp" }, "Gemini" -> Custom { name: "gemini" }.
// Can be removed at some point in the future and go back to #[derive(Deserialize)].
impl<'de> Deserialize<'de> for AgentType {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let value = serde_json::Value::deserialize(deserializer)?;
if let Some(s) = value.as_str() {
return match s {
"NativeAgent" => Ok(Self::NativeAgent),
"TextThread" => Ok(Self::TextThread),
"ClaudeAgent" | "ClaudeCode" => Ok(Self::Custom {
name: CLAUDE_AGENT_NAME.into(),
}),
"Codex" => Ok(Self::Custom {
name: CODEX_NAME.into(),
}),
"Gemini" => Ok(Self::Custom {
name: GEMINI_NAME.into(),
}),
other => Err(serde::de::Error::unknown_variant(
other,
&[
"NativeAgent",
"TextThread",
"Custom",
"ClaudeAgent",
"ClaudeCode",
"Codex",
"Gemini",
],
)),
};
}
if let Some(obj) = value.as_object() {
if let Some(inner) = obj.get("Custom") {
#[derive(Deserialize)]
struct CustomFields {
name: SharedString,
}
let fields: CustomFields =
serde_json::from_value(inner.clone()).map_err(serde::de::Error::custom)?;
return Ok(Self::Custom { name: fields.name });
}
}
Err(serde::de::Error::custom(
"expected a string variant or {\"Custom\": {\"name\": ...}}",
))
}
}
impl AgentType {
pub fn is_native(&self) -> bool {
matches!(self, Self::NativeAgent)
@@ -5310,4 +5367,77 @@ mod tests {
);
});
}
#[test]
fn test_deserialize_legacy_agent_type_variants() {
assert_eq!(
serde_json::from_str::<AgentType>(r#""ClaudeAgent""#).unwrap(),
AgentType::Custom {
name: CLAUDE_AGENT_NAME.into(),
},
);
assert_eq!(
serde_json::from_str::<AgentType>(r#""ClaudeCode""#).unwrap(),
AgentType::Custom {
name: CLAUDE_AGENT_NAME.into(),
},
);
assert_eq!(
serde_json::from_str::<AgentType>(r#""Codex""#).unwrap(),
AgentType::Custom {
name: CODEX_NAME.into(),
},
);
assert_eq!(
serde_json::from_str::<AgentType>(r#""Gemini""#).unwrap(),
AgentType::Custom {
name: GEMINI_NAME.into(),
},
);
}
#[test]
fn test_deserialize_current_agent_type_variants() {
assert_eq!(
serde_json::from_str::<AgentType>(r#""NativeAgent""#).unwrap(),
AgentType::NativeAgent,
);
assert_eq!(
serde_json::from_str::<AgentType>(r#""TextThread""#).unwrap(),
AgentType::TextThread,
);
assert_eq!(
serde_json::from_str::<AgentType>(r#"{"Custom":{"name":"my-agent"}}"#).unwrap(),
AgentType::Custom {
name: "my-agent".into(),
},
);
}
#[test]
fn test_deserialize_legacy_serialized_panel() {
let json = serde_json::json!({
"width": 300.0,
"selected_agent": "ClaudeAgent",
"last_active_thread": {
"session_id": "test-session",
"agent_type": "Codex",
},
});
let panel: SerializedAgentPanel = serde_json::from_value(json).unwrap();
assert_eq!(
panel.selected_agent,
Some(AgentType::Custom {
name: CLAUDE_AGENT_NAME.into(),
}),
);
let thread = panel.last_active_thread.unwrap();
assert_eq!(
thread.agent_type,
AgentType::Custom {
name: CODEX_NAME.into(),
},
);
}
}
+96 -1
View File
@@ -212,13 +212,70 @@ pub struct NewNativeAgentThreadFromSummary {
}
// TODO unify this with AgentType
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[derive(Debug, Clone, PartialEq, Serialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum ExternalAgent {
NativeAgent,
Custom { name: SharedString },
}
// Custom impl handles legacy variant names from before the built-in agents were moved to
// the registry: "claude_code" -> Custom { name: "claude-acp" }, "codex" -> Custom { name:
// "codex-acp" }, "gemini" -> Custom { name: "gemini" }.
// Can be removed at some point in the future and go back to #[derive(Deserialize)].
impl<'de> serde::Deserialize<'de> for ExternalAgent {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
use project::agent_server_store::{CLAUDE_AGENT_NAME, CODEX_NAME, GEMINI_NAME};
let value = serde_json::Value::deserialize(deserializer)?;
if let Some(s) = value.as_str() {
return match s {
"native_agent" => Ok(Self::NativeAgent),
"claude_code" | "claude_agent" => Ok(Self::Custom {
name: CLAUDE_AGENT_NAME.into(),
}),
"codex" => Ok(Self::Custom {
name: CODEX_NAME.into(),
}),
"gemini" => Ok(Self::Custom {
name: GEMINI_NAME.into(),
}),
other => Err(serde::de::Error::unknown_variant(
other,
&[
"native_agent",
"custom",
"claude_agent",
"claude_code",
"codex",
"gemini",
],
)),
};
}
if let Some(obj) = value.as_object() {
if let Some(inner) = obj.get("custom") {
#[derive(serde::Deserialize)]
struct CustomFields {
name: SharedString,
}
let fields: CustomFields =
serde_json::from_value(inner.clone()).map_err(serde::de::Error::custom)?;
return Ok(Self::Custom { name: fields.name });
}
}
Err(serde::de::Error::custom(
"expected a string variant or {\"custom\": {\"name\": ...}}",
))
}
}
impl ExternalAgent {
pub fn server(
&self,
@@ -685,4 +742,42 @@ mod tests {
);
});
}
#[test]
fn test_deserialize_legacy_external_agent_variants() {
use project::agent_server_store::{CLAUDE_AGENT_NAME, CODEX_NAME, GEMINI_NAME};
assert_eq!(
serde_json::from_str::<ExternalAgent>(r#""claude_code""#).unwrap(),
ExternalAgent::Custom {
name: CLAUDE_AGENT_NAME.into(),
},
);
assert_eq!(
serde_json::from_str::<ExternalAgent>(r#""codex""#).unwrap(),
ExternalAgent::Custom {
name: CODEX_NAME.into(),
},
);
assert_eq!(
serde_json::from_str::<ExternalAgent>(r#""gemini""#).unwrap(),
ExternalAgent::Custom {
name: GEMINI_NAME.into(),
},
);
}
#[test]
fn test_deserialize_current_external_agent_variants() {
assert_eq!(
serde_json::from_str::<ExternalAgent>(r#""native_agent""#).unwrap(),
ExternalAgent::NativeAgent,
);
assert_eq!(
serde_json::from_str::<ExternalAgent>(r#"{"custom":{"name":"my-agent"}}"#).unwrap(),
ExternalAgent::Custom {
name: "my-agent".into(),
},
);
}
}