edit tool: Support stringified mode parameter (#55498)
Some checks are pending
Congratsbot / check-author (push) Waiting to run
Congratsbot / congrats (push) Blocked by required conditions
deploy_nightly_docs / deploy_docs (push) Waiting to run
run_tests / check_wasm (push) Blocked by required conditions
run_tests / orchestrate (push) Waiting to run
run_tests / check_style (push) Waiting to run
run_tests / extension_tests (push) Blocked by required conditions
run_tests / clippy_windows (push) Blocked by required conditions
run_tests / clippy_linux (push) Blocked by required conditions
run_tests / clippy_mac (push) Blocked by required conditions
run_tests / clippy_mac_x86_64 (push) Blocked by required conditions
run_tests / run_tests_windows (push) Blocked by required conditions
run_tests / run_tests_linux (push) Blocked by required conditions
run_tests / run_tests_mac (push) Blocked by required conditions
run_tests / doctests (push) Blocked by required conditions
run_tests / check_workspace_binaries (push) Blocked by required conditions
run_tests / build_visual_tests_binary (push) Blocked by required conditions
run_tests / check_dependencies (push) Blocked by required conditions
run_tests / check_docs (push) Blocked by required conditions
run_tests / check_licenses (push) Blocked by required conditions
run_tests / check_scripts (push) Blocked by required conditions
run_tests / check_postgres_and_protobuf_migrations (push) Blocked by required conditions
run_tests / tests_pass (push) Blocked by required conditions

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:

- N/A
This commit is contained in:
Agus Zubiaga 2026-05-02 18:11:10 +02:00 • committed by GitHub
parent 9155bf4e17
commit db5a9be2ff
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -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<Vec<Edit>>,
}
@ -133,11 +134,11 @@ struct StreamingEditFileToolPartialInput {
display_description: Option<String>,
#[serde(default)]
path: Option<String>,
#[serde(default)]
#[serde(default, deserialize_with = "deserialize_maybe_stringified")]
mode: Option<StreamingEditFileMode>,
#[serde(default)]
content: Option<String>,
#[serde(default, deserialize_with = "deserialize_optional_vec_or_json_string")]
#[serde(default, deserialize_with = "deserialize_maybe_stringified")]
edits: Option<Vec<PartialEdit>>,
}
@ -149,30 +150,23 @@ pub struct PartialEdit {
pub new_text: Option<String>,
}
/// 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<Option<Vec<T>>, D::Error>
#[derive(Deserialize)]
#[serde(untagged)]
enum ValueOrJsonString<T> {
Value(T),
String(String),
}
fn deserialize_maybe_stringified<'de, T, D>(deserializer: D) -> Result<T, D::Error>
where
T: DeserializeOwned,
D: Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(untagged)]
enum VecOrJsonString<T> {
Vec(Vec<T>),
String(String),
}
let value = Option::<VecOrJsonString<T>>::deserialize(deserializer)?;
match value {
None => Ok(None),
Some(VecOrJsonString::Vec(items)) => Ok(Some(items)),
Some(VecOrJsonString::String(string)) => serde_json::from_str::<Vec<T>>(&string)
.map(Some)
.map_err(|error| {
D::Error::custom(format!("failed to parse stringified edits array: {error}"))
}),
match ValueOrJsonString::<T>::deserialize(deserializer)? {
ValueOrJsonString::Value(value) => Ok(value),
ValueOrJsonString::String(string) => serde_json::from_str::<T>(&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::<StreamingEditFileToolInput>(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::<StreamingEditFileToolInput>(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::<StreamingEditFileToolInput>(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::<StreamingEditFileToolPartialInput>(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::<StreamingEditFileToolPartialInput>(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::<StreamingEditFileToolPartialInput>(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<project::FakeFs>,