From 8c0f084a57982a71fc857975b396aa11a38447fb Mon Sep 17 00:00:00 2001 From: "zed-zippy[bot]" <234243425+zed-zippy[bot]@users.noreply.github.com> Date: Sat, 2 May 2026 16:24:26 +0000 Subject: [PATCH 1/2] edit tool: Support stringified `mode` parameter (#55498) (cherry-pick to stable) (#55500) Cherry-pick of #55498 to stable ---- Self-Review Checklist: - [x] I've reviewed my own diff for quality, security, and reliability - [x] Unsafe blocks (if any) have justifying comments - [x] The content is consistent with the [UI/UX checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) - [x] Tests cover the new/changed behavior - [x] Performance impact has been considered and is acceptable Release Notes: - Fixed an issue where agent failed to apply edits in some cases Co-authored-by: Agus Zubiaga --- .../src/tools/streaming_edit_file_tool.rs | 106 ++++++++++++++---- 1 file changed, 83 insertions(+), 23 deletions(-) diff --git a/crates/agent/src/tools/streaming_edit_file_tool.rs b/crates/agent/src/tools/streaming_edit_file_tool.rs index 0a28f1d395fc75..a51de568b5f7cf 100644 --- a/crates/agent/src/tools/streaming_edit_file_tool.rs +++ b/crates/agent/src/tools/streaming_edit_file_tool.rs @@ -84,6 +84,7 @@ pub struct StreamingEditFileToolInput { /// - 'edit': Make granular edits to an existing file. Requires 'edits' field. /// /// When a file already exists or you just created it, prefer editing it as opposed to recreating it from scratch. + #[serde(deserialize_with = "deserialize_maybe_stringified")] pub mode: StreamingEditFileMode, /// The complete content for the new file (required for 'write' mode). @@ -96,7 +97,7 @@ pub struct StreamingEditFileToolInput { #[serde( default, skip_serializing_if = "Option::is_none", - deserialize_with = "deserialize_optional_vec_or_json_string" + deserialize_with = "deserialize_maybe_stringified" )] pub edits: Option>, } @@ -133,11 +134,11 @@ struct StreamingEditFileToolPartialInput { display_description: Option, #[serde(default)] path: Option, - #[serde(default)] + #[serde(default, deserialize_with = "deserialize_maybe_stringified")] mode: Option, #[serde(default)] content: Option, - #[serde(default, deserialize_with = "deserialize_optional_vec_or_json_string")] + #[serde(default, deserialize_with = "deserialize_maybe_stringified")] edits: Option>, } @@ -149,30 +150,23 @@ pub struct PartialEdit { pub new_text: Option, } -/// Sometimes the model responds with a stringified JSON array of edits (`"[...]"`) instead of a regular array (`[...]`) -fn deserialize_optional_vec_or_json_string<'de, T, D>( - deserializer: D, -) -> Result>, D::Error> +#[derive(Deserialize)] +#[serde(untagged)] +enum ValueOrJsonString { + Value(T), + String(String), +} + +fn deserialize_maybe_stringified<'de, T, D>(deserializer: D) -> Result where T: DeserializeOwned, D: Deserializer<'de>, { - #[derive(Deserialize)] - #[serde(untagged)] - enum VecOrJsonString { - Vec(Vec), - String(String), - } - - let value = Option::>::deserialize(deserializer)?; - match value { - None => Ok(None), - Some(VecOrJsonString::Vec(items)) => Ok(Some(items)), - Some(VecOrJsonString::String(string)) => serde_json::from_str::>(&string) - .map(Some) - .map_err(|error| { - D::Error::custom(format!("failed to parse stringified edits array: {error}")) - }), + match ValueOrJsonString::::deserialize(deserializer)? { + ValueOrJsonString::Value(value) => Ok(value), + ValueOrJsonString::String(string) => serde_json::from_str::(&string).map_err(|error| { + D::Error::custom(format!("failed to parse stringified value: {error}")) + }), } } @@ -4180,6 +4174,72 @@ mod tests { ); } + #[test] + fn test_input_deserializes_double_encoded_fields() { + let input = serde_json::from_value::(json!({ + "display_description": "Edit", + "path": "root/file.txt", + "mode": "\"edit\"", + "edits": "[{\"old_text\": \"hello\\nworld\", \"new_text\": \"HELLO\\nWORLD\"}]" + })) + .expect("input should deserialize"); + + assert!(matches!(input.mode, StreamingEditFileMode::Edit)); + let edits = input.edits.expect("edits should deserialize"); + assert_eq!(edits.len(), 1); + assert_eq!(edits[0].old_text, "hello\nworld"); + assert_eq!(edits[0].new_text, "HELLO\nWORLD"); + + let input = serde_json::from_value::(json!({ + "display_description": "Edit", + "path": "root/file.txt", + "mode": "\"edit\"" + })) + .expect("input should deserialize"); + assert!(input.edits.is_none()); + + let input = serde_json::from_value::(json!({ + "display_description": "Edit", + "path": "root/file.txt", + "mode": "\"edit\"", + "edits": null + })) + .expect("input should deserialize"); + assert!(input.edits.is_none()); + + let input = serde_json::from_value::(json!({ + "display_description": "Edit", + "path": "root/file.txt", + "mode": "\"edit\"", + "edits": "[{\"old_text\": \"hello\\nworld\", \"new_text\": \"HELLO\\nWORLD\"}]" + })) + .expect("input should deserialize"); + + assert!(matches!(input.mode, Some(StreamingEditFileMode::Edit))); + let edits = input.edits.expect("edits should deserialize"); + assert_eq!(edits.len(), 1); + assert_eq!(edits[0].old_text.as_deref(), Some("hello\nworld")); + assert_eq!(edits[0].new_text.as_deref(), Some("HELLO\nWORLD")); + + let input = serde_json::from_value::(json!({ + "display_description": "Edit", + "path": "root/file.txt" + })) + .expect("input should deserialize"); + assert!(input.mode.is_none()); + assert!(input.edits.is_none()); + + let input = serde_json::from_value::(json!({ + "display_description": "Edit", + "path": "root/file.txt", + "mode": null, + "edits": null + })) + .expect("input should deserialize"); + assert!(input.mode.is_none()); + assert!(input.edits.is_none()); + } + async fn setup_test_with_fs( cx: &mut TestAppContext, fs: Arc, From ace0777af38630f1761e9c311f7372dec747739e Mon Sep 17 00:00:00 2001 From: "zed-zippy[bot]" <234243425+zed-zippy[bot]@users.noreply.github.com> Date: Mon, 4 May 2026 08:25:05 +0000 Subject: [PATCH 2/2] Bump to 1.0.1 for @bennetbo --- Cargo.lock | 2 +- crates/zed/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b758fdbd971ce7..52721653051a97 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -22202,7 +22202,7 @@ dependencies = [ [[package]] name = "zed" -version = "1.0.0" +version = "1.0.1" dependencies = [ "acp_thread", "acp_tools", diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 6726071f33ab8b..ff9df51d182811 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -2,7 +2,7 @@ description = "The fast, collaborative code editor." edition.workspace = true name = "zed" -version = "1.0.0" +version = "1.0.1" publish.workspace = true license = "GPL-3.0-or-later" authors = ["Zed Team "]