From 21da59dfd9de3be3535af0642766b62d55365ec9 Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner Date: Sun, 29 Mar 2026 13:17:32 +0200 Subject: [PATCH 1/6] agent: Fix streaming edit file tool inserting newlines when old_text ends with newline (#52661) 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 --- .../src/tools/streaming_edit_file_tool.rs | 39 ++++++ crates/agent/src/tools/tool_edit_parser.rs | 113 +++++++++++++++--- 2 files changed, 133 insertions(+), 19 deletions(-) diff --git a/crates/agent/src/tools/streaming_edit_file_tool.rs b/crates/agent/src/tools/streaming_edit_file_tool.rs index df99b4d65a62e3bb12239ef58d9ad49416554209..88ec1e67787ad6efbeaa46b83b9034a24b10d3db 100644 --- a/crates/agent/src/tools/streaming_edit_file_tool.rs +++ b/crates/agent/src/tools/streaming_edit_file_tool.rs @@ -3969,6 +3969,45 @@ mod tests { ); } + #[gpui::test] + async fn test_streaming_edit_preserves_blank_line_after_trailing_newline_replacement( + cx: &mut TestAppContext, + ) { + let file_content = "before\ntarget\n\nafter\n"; + let old_text = "target\n"; + let new_text = "one\ntwo\ntarget\n"; + let expected = "before\none\ntwo\ntarget\n\nafter\n"; + + let (tool, _project, _action_log, _fs, _thread) = + setup_test(cx, json!({"file.rs": file_content})).await; + let (sender, input) = ToolInput::::test(); + let (event_stream, _receiver) = ToolCallEventStream::test(); + let task = cx.update(|cx| tool.clone().run(input, event_stream, cx)); + + sender.send_final(json!({ + "display_description": "description", + "path": "root/file.rs", + "mode": "edit", + "edits": [{"old_text": old_text, "new_text": new_text}] + })); + + let result = task.await; + + let StreamingEditFileToolOutput::Success { + new_text: final_text, + .. + } = result.unwrap() + else { + panic!("expected success"); + }; + + pretty_assertions::assert_eq!( + final_text, + expected, + "Edit should preserve a single blank line before test_after" + ); + } + #[gpui::test] async fn test_streaming_reject_created_file_deletes_it(cx: &mut TestAppContext) { let (tool, _project, action_log, fs, _thread) = setup_test(cx, json!({"dir": {}})).await; diff --git a/crates/agent/src/tools/tool_edit_parser.rs b/crates/agent/src/tools/tool_edit_parser.rs index 86259db916f49c07bbecc63625a93a9ebb955539..86f249ff34eb13b43209331227f624d740ab33af 100644 --- a/crates/agent/src/tools/tool_edit_parser.rs +++ b/crates/agent/src/tools/tool_edit_parser.rs @@ -78,7 +78,7 @@ impl ToolEditParser { if partial.new_text.is_some() { // new_text appeared, so old_text is done — emit everything. let start = state.old_text_emitted_len.min(old_text.len()); - let chunk = old_text[start..].to_string(); + let chunk = normalize_done_chunk(old_text[start..].to_string()); state.old_text_done = true; state.old_text_emitted_len = old_text.len(); events.push(ToolEditEvent::OldTextChunk { @@ -87,7 +87,8 @@ impl ToolEditParser { done: true, }); } else { - let safe_end = safe_emit_end(old_text); + let safe_end = safe_emit_end_for_edit_text(old_text); + if safe_end > state.old_text_emitted_len { let chunk = old_text[state.old_text_emitted_len..safe_end].to_string(); state.old_text_emitted_len = safe_end; @@ -104,7 +105,8 @@ impl ToolEditParser { if let Some(new_text) = &partial.new_text && !state.new_text_done { - let safe_end = safe_emit_end(new_text); + let safe_end = safe_emit_end_for_edit_text(new_text); + if safe_end > state.new_text_emitted_len { let chunk = new_text[state.new_text_emitted_len..safe_end].to_string(); state.new_text_emitted_len = safe_end; @@ -160,7 +162,7 @@ impl ToolEditParser { if !state.old_text_done { let start = state.old_text_emitted_len.min(edit.old_text.len()); - let chunk = edit.old_text[start..].to_string(); + let chunk = normalize_done_chunk(edit.old_text[start..].to_string()); state.old_text_done = true; state.old_text_emitted_len = edit.old_text.len(); events.push(ToolEditEvent::OldTextChunk { @@ -172,7 +174,7 @@ impl ToolEditParser { if !state.new_text_done { let start = state.new_text_emitted_len.min(edit.new_text.len()); - let chunk = edit.new_text[start..].to_string(); + let chunk = normalize_done_chunk(edit.new_text[start..].to_string()); state.new_text_done = true; state.new_text_emitted_len = edit.new_text.len(); events.push(ToolEditEvent::NewTextChunk { @@ -252,6 +254,22 @@ fn safe_emit_end(text: &str) -> usize { } } +fn safe_emit_end_for_edit_text(text: &str) -> usize { + let safe_end = safe_emit_end(text); + if safe_end > 0 && text.as_bytes()[safe_end - 1] == b'\n' { + safe_end - 1 + } else { + safe_end + } +} + +fn normalize_done_chunk(mut chunk: String) -> String { + if chunk.ends_with('\n') { + chunk.pop(); + } + chunk +} + #[cfg(test)] mod tests { use super::*; @@ -337,6 +355,69 @@ mod tests { ); } + #[test] + fn test_done_chunks_strip_trailing_newline() { + let mut parser = ToolEditParser::default(); + + let events = parser.finalize_edits(&[Edit { + old_text: "before\n".into(), + new_text: "after\n".into(), + }]); + assert_eq!( + events.as_slice(), + &[ + ToolEditEvent::OldTextChunk { + edit_index: 0, + chunk: "before".into(), + done: true, + }, + ToolEditEvent::NewTextChunk { + edit_index: 0, + chunk: "after".into(), + done: true, + }, + ] + ); + } + + #[test] + fn test_partial_edit_chunks_hold_back_trailing_newline() { + let mut parser = ToolEditParser::default(); + + let events = parser.push_edits(&[PartialEdit { + old_text: Some("before\n".into()), + new_text: Some("after\n".into()), + }]); + assert_eq!( + events.as_slice(), + &[ + ToolEditEvent::OldTextChunk { + edit_index: 0, + chunk: "before".into(), + done: true, + }, + ToolEditEvent::NewTextChunk { + edit_index: 0, + chunk: "after".into(), + done: false, + }, + ] + ); + + let events = parser.finalize_edits(&[Edit { + old_text: "before\n".into(), + new_text: "after\n".into(), + }]); + assert_eq!( + events.as_slice(), + &[ToolEditEvent::NewTextChunk { + edit_index: 0, + chunk: "".into(), + done: true, + }] + ); + } + #[test] fn test_multiple_edits_sequential() { let mut parser = ToolEditParser::default(); @@ -858,22 +939,16 @@ mod tests { ); // Next partial: the fixer corrects the escape to \n. - // The held-back byte was wrong, but we never emitted it. Now the - // correct newline at that position is emitted normally. + // Because edit text also holds back a trailing newline, nothing new + // is emitted yet. let events = parser.push_edits(&[PartialEdit { old_text: Some("hello,\n".into()), new_text: None, }]); - assert_eq!( - events.as_slice(), - &[ToolEditEvent::OldTextChunk { - edit_index: 0, - chunk: "\n".into(), - done: false, - }] - ); + assert!(events.is_empty()); - // Continue normally. + // Continue normally. The held-back newline is emitted together with the + // next content once it is no longer trailing. let events = parser.push_edits(&[PartialEdit { old_text: Some("hello,\nworld".into()), new_text: None, @@ -882,7 +957,7 @@ mod tests { events.as_slice(), &[ToolEditEvent::OldTextChunk { edit_index: 0, - chunk: "world".into(), + chunk: "\nworld".into(), done: false, }] ); @@ -919,7 +994,7 @@ mod tests { }, ToolEditEvent::NewTextChunk { edit_index: 0, - chunk: "LINE1\n".into(), + chunk: "LINE1".into(), done: false, }, ] @@ -933,7 +1008,7 @@ mod tests { events.as_slice(), &[ToolEditEvent::NewTextChunk { edit_index: 0, - chunk: "LINE2\nLINE3".into(), + chunk: "\nLINE2\nLINE3".into(), done: false, }] ); From dbb8afe6764f07bf62272e38d2a7ba5b27e1946e Mon Sep 17 00:00:00 2001 From: hnakashima <69759577+nakashima-hikaru@users.noreply.github.com> Date: Sun, 29 Mar 2026 23:43:05 +0900 Subject: [PATCH 2/6] gpui: Fix BGRA conversion for SVG rendering (#52641) ### Description Fixes swapped red/blue channels when rendering SVG images. #### Describe the bug When rendering a full-color SVG into an Image object using Image::from_bytes(ImageFormat::Svg, ...) on macOS, the resulting bitmap has its Red and Blue channels swapped. For example, a color specified as #38BDF8 (Light Blue) in the SVG source appears as yellowish in the rendered GPUI view. #### Steps to reproduce 1. Create a GPUI application. 1. Generate or load an SVG string containing a specific color, for example: ```xml ``` 3. Load this SVG into an Image object: ```rust let image = Arc::new(Image::from_bytes( ImageFormat::Svg, svg_string.into_bytes(), )); ``` 4. Display this image in a view using an img() element. #### Expected behavior The rectangle should be rendered in **Light Blue (#38BDF8)**. #### Actual behavior The rectangle is rendered in **Yellowish Color (#F8BD38)**. ### 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 ### Closes #ISSUE ### Release Notes: - Fixed swapped color channels when pasting SVG images from the clipboard. --------- Co-authored-by: MrSubidubi --- crates/gpui/src/elements/img.rs | 2 +- crates/gpui/src/platform.rs | 26 +++++++++++++++++++++- crates/gpui/src/svg_renderer.rs | 7 ++---- crates/markdown/src/mermaid.rs | 3 +-- crates/svg_preview/src/svg_preview_view.rs | 2 +- 5 files changed, 30 insertions(+), 10 deletions(-) diff --git a/crates/gpui/src/elements/img.rs b/crates/gpui/src/elements/img.rs index 875f9e6dc1cc7d248f9e70488e52480dcca53fa3..ccf10d038c271ac54a0060b4c17c9de86ce9eb5c 100644 --- a/crates/gpui/src/elements/img.rs +++ b/crates/gpui/src/elements/img.rs @@ -697,7 +697,7 @@ impl Asset for ImageAssetLoader { Ok(Arc::new(RenderImage::new(data))) } else { svg_renderer - .render_single_frame(&bytes, 1.0, true) + .render_single_frame(&bytes, 1.0) .map_err(Into::into) } } diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index 9d672ce34df061b11dce3437101afc55d2b086c7..806a34040a4ec685c3d5c6ec01f47b5026e349a6 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -2108,7 +2108,7 @@ impl Image { ImageFormat::Ico => frames_for_image(&self.bytes, image::ImageFormat::Ico)?, ImageFormat::Svg => { return svg_renderer - .render_single_frame(&self.bytes, 1.0, false) + .render_single_frame(&self.bytes, 1.0) .map_err(Into::into); } }; @@ -2190,6 +2190,30 @@ impl From for ClipboardString { } } +#[cfg(test)] +mod image_tests { + use super::*; + use std::sync::Arc; + + #[test] + fn test_svg_image_to_image_data_converts_to_bgra() { + let image = Image::from_bytes( + ImageFormat::Svg, + br##" + +"## + .to_vec(), + ); + + let render_image = image.to_image_data(SvgRenderer::new(Arc::new(()))).unwrap(); + let bytes = render_image.as_bytes(0).unwrap(); + + for pixel in bytes.chunks_exact(4) { + assert_eq!(pixel, &[0xF8, 0xBD, 0x38, 0xFF]); + } + } +} + #[cfg(all(test, any(target_os = "linux", target_os = "freebsd")))] mod tests { use super::*; diff --git a/crates/gpui/src/svg_renderer.rs b/crates/gpui/src/svg_renderer.rs index 217555e3b0e295d06e375e19d013e0b520118e0b..8653ab9b162031772ab29367b60ff988e33cd823 100644 --- a/crates/gpui/src/svg_renderer.rs +++ b/crates/gpui/src/svg_renderer.rs @@ -150,7 +150,6 @@ impl SvgRenderer { &self, bytes: &[u8], scale_factor: f32, - to_brga: bool, ) -> Result, usvg::Error> { self.render_pixmap( bytes, @@ -161,10 +160,8 @@ impl SvgRenderer { image::ImageBuffer::from_raw(pixmap.width(), pixmap.height(), pixmap.take()) .unwrap(); - if to_brga { - for pixel in buffer.chunks_exact_mut(4) { - swap_rgba_pa_to_bgra(pixel); - } + for pixel in buffer.chunks_exact_mut(4) { + swap_rgba_pa_to_bgra(pixel); } let mut image = RenderImage::new(SmallVec::from_const([Frame::new(buffer)])); diff --git a/crates/markdown/src/mermaid.rs b/crates/markdown/src/mermaid.rs index 560a67787ff897c6f792c97fafdd9ed617c020e6..15f3de4d8e8c64010fe96846b05d75f012c5fc0d 100644 --- a/crates/markdown/src/mermaid.rs +++ b/crates/markdown/src/mermaid.rs @@ -106,7 +106,7 @@ impl CachedMermaidDiagram { let svg_string = mermaid_rs_renderer::render(&contents.contents)?; let scale = contents.scale as f32 / 100.0; svg_renderer - .render_single_frame(svg_string.as_bytes(), scale, true) + .render_single_frame(svg_string.as_bytes(), scale) .map_err(|error| anyhow::anyhow!("{error}")) }) .await; @@ -325,7 +325,6 @@ mod tests { .render_single_frame( br#""#, 1.0, - true, ) .unwrap() }) diff --git a/crates/svg_preview/src/svg_preview_view.rs b/crates/svg_preview/src/svg_preview_view.rs index 1a001c6e18854428636626cc499e49433710a84d..259243b8ac7cd7d4122fc2f535d490b359442440 100644 --- a/crates/svg_preview/src/svg_preview_view.rs +++ b/crates/svg_preview/src/svg_preview_view.rs @@ -110,7 +110,7 @@ impl SvgPreviewView { let renderer = cx.svg_renderer(); let content = buffer.read(cx).snapshot(); let background_task = cx.background_spawn(async move { - renderer.render_single_frame(content.text().as_bytes(), SCALE_FACTOR, true) + renderer.render_single_frame(content.text().as_bytes(), SCALE_FACTOR) }); self._refresh = cx.spawn_in(window, async move |this, cx| { From ef4af8f92415f6f3082a2d373fadb5883f8adee1 Mon Sep 17 00:00:00 2001 From: Om Chillure Date: Sun, 29 Mar 2026 22:01:42 +0530 Subject: [PATCH 3/6] Fix/gemini tool schema unsupported keys (#52670) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### Summary The Gemini API enforces strict validation on `function_declarations` and rejects requests containing unsupported JSON Schema keywords such as `additionalProperties`, `propertyNames`. This caused Write mode to fail with "failed to stream completion" when tools with complex schemas were used. This PR strips these unsupported keywords from tool schemas before sending them to the Gemini API in `adapt_to_json_schema_subset`. ### How to Review - Check `crates/language_model/src/tool_schema.rs` — the `adapt_to_json_schema_subset` function now removes `additionalProperties` and `propertyNames` from schemas. - Tests are added covering removal of these keys and nested schema handling. - To reproduce the original issue, send a tool schema containing `propertyNames` or `additionalProperties` to the Gemini API — it returns HTTP 400 `INVALID_ARGUMENT` ### How to Test Run the unit tests: ```sh cargo test -p language_model ``` OR manually reproduce this using -> ``` curl -s "https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key=YOUR_KEY" \ -H 'Content-Type: application/json' \ -d '{"contents":[{"parts":[{"text":"test"}]}],"tools":[{"functionDeclarations":[{"name":"test","parameters":{"type":"OBJECT","properties":{"field":{"type":"OBJECT","propertyNames":{"pattern":"^[a-z]+$"},"additionalProperties":{"type":"STRING"}}}}}]}]}' ``` #### Closes #52430 - [x] I've reviewed my own diff for quality, security, and reliability - [ ] Unsafe blocks (if any) have justifying comments - [ ] 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 Video [Screencast from 2026-03-29 08-32-18.webm](https://github.com/user-attachments/assets/a0069f0e-1f2b-45dc-85bf-f24aacb08599) ### Note : Reopens previous work from closed PR #52644 (fork was deleted) Release Notes: - Fixed an issue where Gemini models would not work when using specific MCP servers --- crates/language_model/src/tool_schema.rs | 29 ++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/crates/language_model/src/tool_schema.rs b/crates/language_model/src/tool_schema.rs index 6fbb3761b43ea04924aaa23373920c41a14c74e3..69afcd1f288064748e3c953a27003361ed5115b0 100644 --- a/crates/language_model/src/tool_schema.rs +++ b/crates/language_model/src/tool_schema.rs @@ -105,9 +105,12 @@ fn adapt_to_json_schema_subset(json: &mut Value) -> Result<()> { ); } - const KEYS_TO_REMOVE: [(&str, fn(&Value) -> bool); 5] = [ + const KEYS_TO_REMOVE: [(&str, fn(&Value) -> bool); 6] = [ ("format", |value| value.is_string()), - ("additionalProperties", |value| value.is_boolean()), + // Gemini doesn't support `additionalProperties` in any form (boolean or schema object) + ("additionalProperties", |_| true), + // Gemini doesn't support `propertyNames` + ("propertyNames", |_| true), ("exclusiveMinimum", |value| value.is_number()), ("exclusiveMaximum", |value| value.is_number()), ("optional", |value| value.is_boolean()), @@ -234,6 +237,28 @@ mod tests { "format": {}, }) ); + + // additionalProperties as an object schema is also unsupported by Gemini + let mut json = json!({ + "type": "object", + "properties": { + "name": { "type": "string" } + }, + "additionalProperties": { "type": "string" }, + "propertyNames": { "pattern": "^[A-Za-z]+$" } + }); + + adapt_to_json_schema_subset(&mut json).unwrap(); + + assert_eq!( + json, + json!({ + "type": "object", + "properties": { + "name": { "type": "string" } + } + }) + ); } #[test] From 46a0262dc5b0daf287e177a9debcc14f7ffba427 Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner Date: Sun, 29 Mar 2026 20:59:18 +0200 Subject: [PATCH 4/6] agent: Remove duplicated description from tool schema (#52678) Turns out we were including the description of a tool inside the schema again, which I don't think is needed. Before: ``` LanguageModelRequestTool { name: "web_search", description: "Search the web for information using your query.\nUse this when you need real-time information, facts, or data that might not be in your training.\nResults will include snippets and links from relevant web pages.", input_schema: Object { "required": Array [ String("query"), ], "description": String("Search the web for information using your query.\nUse this when you need real-time information, facts, or data that might not be in your training.\nResults will include snippets and links from relevant web pages."), "type": String("object"), "properties": Object { "query": Object { "description": String("The search term or question to query on the web."), "type": String("string"), }, }, "additionalProperties": Bool(false), }, use_input_streaming: false, }, ``` After: ``` LanguageModelRequestTool { name: "web_search", description: "Search the web for information using your query.\nUse this when you need real-time information, facts, or data that might not be in your training.\nResults will include snippets and links from relevant web pages.", input_schema: Object { "required": Array [ String("query"), ], "type": String("object"), "properties": Object { "query": Object { "description": String("The search term or question to query on the web."), "type": String("string"), }, }, "additionalProperties": Bool(false), }, use_input_streaming: false, }, ``` 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 Closes #45315 Release Notes: - agent: Reduced amount of tokens consumed by tool descriptions --- crates/language_model/src/tool_schema.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/language_model/src/tool_schema.rs b/crates/language_model/src/tool_schema.rs index 69afcd1f288064748e3c953a27003361ed5115b0..878870482a7527bf815797d16e03ad8edc79642e 100644 --- a/crates/language_model/src/tool_schema.rs +++ b/crates/language_model/src/tool_schema.rs @@ -67,6 +67,7 @@ pub fn adapt_schema_to_format( if let Value::Object(obj) = json { obj.remove("$schema"); obj.remove("title"); + obj.remove("description"); } match format { From 24ea5e98da14b666bdda6b0b52259be8c864a325 Mon Sep 17 00:00:00 2001 From: Ben Brandt Date: Sun, 29 Mar 2026 22:58:50 +0200 Subject: [PATCH 5/6] eval_cli: Simplify build setup for more datasets (#52686) Cleans up build setup and simplifies it a bunch so that we can use the binary in more eval docker containers Release Notes: - N/A --- crates/agent_ui/Cargo.toml | 3 +- crates/agent_ui/src/conversation_view.rs | 2 + crates/eval_cli/Dockerfile | 55 +++--- crates/eval_cli/script/build-linux | 8 +- crates/eval_cli/zed_eval/agent.py | 217 +++++++++++++++++++---- crates/eval_cli/zed_eval/install.sh.j2 | 55 ------ crates/sidebar/Cargo.toml | 2 +- crates/zed/Cargo.toml | 2 +- 8 files changed, 218 insertions(+), 126 deletions(-) delete mode 100644 crates/eval_cli/zed_eval/install.sh.j2 diff --git a/crates/agent_ui/Cargo.toml b/crates/agent_ui/Cargo.toml index 0d95aa9bc8d69e1197b35ebb7268ba0020aea3af..6c045d4dd2114834605da278aad111fab174d4c6 100644 --- a/crates/agent_ui/Cargo.toml +++ b/crates/agent_ui/Cargo.toml @@ -23,6 +23,7 @@ test-support = [ "workspace/test-support", "agent/test-support", ] +audio = ["dep:audio"] unit-eval = [] [dependencies] @@ -38,7 +39,7 @@ heapless.workspace = true assistant_text_thread.workspace = true assistant_slash_command.workspace = true assistant_slash_commands.workspace = true -audio.workspace = true +audio = { workspace = true, optional = true } base64.workspace = true buffer_diff.workspace = true chrono.workspace = true diff --git a/crates/agent_ui/src/conversation_view.rs b/crates/agent_ui/src/conversation_view.rs index 3fabb528315f8f32c03d358c13d123e5bb299fd7..14ab1c16322594f42e9035bc070a39f4151a273e 100644 --- a/crates/agent_ui/src/conversation_view.rs +++ b/crates/agent_ui/src/conversation_view.rs @@ -13,6 +13,7 @@ use agent_servers::AgentServerDelegate; use agent_servers::{AgentServer, GEMINI_TERMINAL_AUTH_METHOD_ID}; use agent_settings::{AgentProfileId, AgentSettings}; use anyhow::{Result, anyhow}; +#[cfg(feature = "audio")] use audio::{Audio, Sound}; use buffer_diff::BufferDiff; use client::zed_urls; @@ -2278,6 +2279,7 @@ impl ConversationView { window: &mut Window, cx: &mut Context, ) { + #[cfg(feature = "audio")] self.play_notification_sound(window, cx); self.show_notification(caption, icon, window, cx); } diff --git a/crates/eval_cli/Dockerfile b/crates/eval_cli/Dockerfile index 7b91a7adf991428670fac43ad745a6e9998c9c38..f733ca42cbe5b37f25f25f510eee3fc57e102495 100644 --- a/crates/eval_cli/Dockerfile +++ b/crates/eval_cli/Dockerfile @@ -7,55 +7,44 @@ # Or use the helper script: # crates/eval_cli/script/build-linux -FROM rust:1.93.1-bookworm AS builder +FROM rust:1.93 AS builder WORKDIR /app -# Install build dependencies (subset of script/linux needed for headless GPUI). + # Pre-install the toolchain specified in rust-toolchain.toml so it is cached. +RUN rustup toolchain install 1.93 --profile minimal \ + --component rustfmt --component clippy --component rust-analyzer --component rust-src \ + --target wasm32-wasip2 --target wasm32-unknown-unknown --target x86_64-unknown-linux-musl --target x86_64-unknown-linux-gnu + +# Install build tools. cmake + build-essential are needed for vendored C +# libraries (libgit2-sys, zstd-sys, libsqlite3-sys). No audio/GUI -dev +# packages required — eval-cli runs headless with those features disabled. +# +# cargo-zigbuild cross-compiles against a specific glibc version (2.31 = +# Debian Bullseye / Ubuntu Focal) so the resulting binary is portable to +# any Linux distro with glibc >= 2.31. RUN apt-get update && apt-get install -y --no-install-recommends \ cmake \ - clang \ - g++ \ - libasound2-dev \ - libfontconfig-dev \ - libgit2-dev \ - libglib2.0-dev \ - libssl-dev \ - libwayland-dev \ - libx11-xcb-dev \ - libxkbcommon-x11-dev \ - libzstd-dev \ - libsqlite3-dev \ build-essential \ curl \ + xz-utils \ && rm -rf /var/lib/apt/lists/* -# Install wild linker for faster linking (built from source to match bookworm's glibc). -RUN cargo install --locked wild-linker --version 0.8.0 --root /usr/local +RUN mkdir -p /opt/zig \ + && curl -fsSL https://ziglang.org/download/0.15.2/zig-x86_64-linux-0.15.2.tar.xz \ + | tar -xJ -C /opt/zig --strip-components=1 \ + && ln -s /opt/zig/zig /usr/local/bin/zig -# Download WASI SDK (needed by some dependencies). -ARG TARGETARCH -RUN mkdir -p /app/target && \ - WASI_ARCH=$([ "$TARGETARCH" = "arm64" ] && echo "arm64" || echo "x86_64") && \ - curl -L "https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-25/wasi-sdk-25.0-${WASI_ARCH}-linux.tar.gz" \ - | tar -xz -C /app/target && \ - mv /app/target/wasi-sdk-25.0-${WASI_ARCH}-linux /app/target/wasi-sdk - -# Pre-install the toolchain specified in rust-toolchain.toml so it is cached. -RUN rustup toolchain install 1.93 --profile minimal \ - --component rustfmt --component clippy --component rust-analyzer --component rust-src \ - --target wasm32-wasip2 --target wasm32-unknown-unknown --target x86_64-unknown-linux-musl +RUN cargo install --locked cargo-zigbuild COPY . . -ENV CC=clang CXX=clang++ -ENV RUSTFLAGS="-C linker=clang -C link-arg=--ld-path=wild" - RUN --mount=type=cache,target=/usr/local/cargo/registry \ --mount=type=cache,target=/usr/local/cargo/git \ --mount=type=cache,target=/app/target \ - cargo build --release --package eval_cli && \ - cp /app/target/release/eval-cli /eval-cli && \ + cargo zigbuild --release --package eval_cli \ + --target x86_64-unknown-linux-gnu.2.31 && \ + cp /app/target/x86_64-unknown-linux-gnu/release/eval-cli /eval-cli && \ strip /eval-cli FROM scratch diff --git a/crates/eval_cli/script/build-linux b/crates/eval_cli/script/build-linux index 9c710668de2aa5e956efff727e6ef8eb2c5ed627..788f1c1ef4a86b1d17e39a01b176f2ae6545e44c 100755 --- a/crates/eval_cli/script/build-linux +++ b/crates/eval_cli/script/build-linux @@ -1,8 +1,10 @@ #!/usr/bin/env bash # # Build eval-cli for x86_64 Linux from any host (macOS, Linux, etc.) -# using Docker. The resulting binary is placed at the path printed on -# completion (default: target/eval-cli). +# using Docker + cargo-zigbuild. Targets glibc 2.31 (Debian Bullseye / +# Ubuntu Focal) so the binary is portable to any modern Linux distro. +# The resulting binary is placed at the path printed on completion +# (default: target/eval-cli). # # Usage: # crates/eval_cli/script/build-linux [--output PATH] @@ -36,7 +38,7 @@ cd "$REPO_ROOT" IMAGE_TAG="eval-cli-builder" -echo "Building eval-cli for x86_64-unknown-linux-gnu..." +echo "Building eval-cli for x86_64-unknown-linux-gnu (glibc >= 2.31)..." echo " Repo root: $REPO_ROOT" echo " Output: $OUTPUT" echo "" diff --git a/crates/eval_cli/zed_eval/agent.py b/crates/eval_cli/zed_eval/agent.py index 5e70713e0dec8512c6303dc5ed7314c245fb6728..2184d7a12f2aa9f56d3ecf77ff5e61417f733cf2 100644 --- a/crates/eval_cli/zed_eval/agent.py +++ b/crates/eval_cli/zed_eval/agent.py @@ -22,7 +22,7 @@ import os import shlex from pathlib import Path -from harbor.agents.installed.base import BaseInstalledAgent, ExecInput +from harbor.agents.installed.base import BaseInstalledAgent, with_prompt_template from harbor.environments.base import BaseEnvironment from harbor.models.agent.context import AgentContext @@ -51,12 +51,143 @@ class ZedAgent(BaseInstalledAgent): def name() -> str: return "zed" - @property - def _install_agent_template_path(self) -> Path: - return Path(__file__).parent / "install.sh.j2" + async def _detect_workdir(self, environment: BaseEnvironment) -> str: + """Detect the repo working directory inside the container. + + Checks, in order: + 1. Explicit ``EVAL_CLI_WORKDIR`` extra-env override + 2. ``/app`` (SWE-bench Pro) + 3. ``/testbed`` (SWE-bench Verified) + 4. ``/repo`` + 5. First git repo found under ``/`` (max depth 3) + """ + override = self._extra_env.get("EVAL_CLI_WORKDIR") + if override: + return override + + result = await self.exec_as_agent( + environment, + command=( + "for d in /app /testbed /repo; do " + ' if [ -d "$d/.git" ]; then echo "$d"; exit 0; fi; ' + "done; " + "find / -maxdepth 3 -name .git -type d 2>/dev/null " + '| head -1 | sed "s|/.git$||"' + ), + ) + workdir = result.stdout.strip() + if not workdir: + raise RuntimeError( + "Could not find a git repository in the container. " + "Set EVAL_CLI_WORKDIR explicitly via --ae EVAL_CLI_WORKDIR=/path/to/repo" + ) + return workdir + + async def install(self, environment: BaseEnvironment) -> None: + await self.exec_as_root( + environment, + command=( + "apt-get update && " + "apt-get install -y --no-install-recommends " + "ca-certificates " + "curl " + "git" + ), + env={"DEBIAN_FRONTEND": "noninteractive"}, + ) + + await self.exec_as_root( + environment, + command=( + "curl -fsSL https://deb.nodesource.com/setup_22.x | bash - && " + "apt-get install -y --no-install-recommends nodejs" + ), + env={"DEBIAN_FRONTEND": "noninteractive"}, + ) + + # Pre-install default LSPs so Zed doesn't have to download them at + # runtime. Each gets its own subdirectory under $ZED_DATA_DIR/languages. + await self.exec_as_agent( + environment, + command=( + "set -euo pipefail; " + 'ZED_DATA_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/zed"; ' + # basedpyright (Python - default type checker) + 'BASEDPYRIGHT_DIR="$ZED_DATA_DIR/languages/basedpyright"; ' + 'mkdir -p "$BASEDPYRIGHT_DIR"; ' + 'npm install --prefix "$BASEDPYRIGHT_DIR" --save-exact basedpyright; ' + # typescript-language-server (TypeScript/JS - default LSP) + 'TSSERVER_DIR="$ZED_DATA_DIR/languages/typescript-language-server"; ' + 'mkdir -p "$TSSERVER_DIR"; ' + 'npm install --prefix "$TSSERVER_DIR" --save-exact typescript typescript-language-server; ' + # vtsls (VS Code TypeScript language features) + 'VTSLS_DIR="$ZED_DATA_DIR/languages/vtsls"; ' + 'mkdir -p "$VTSLS_DIR"; ' + 'npm install --prefix "$VTSLS_DIR" --save-exact @vtsls/language-server typescript; ' + # tailwindcss-language-server + 'TAILWIND_DIR="$ZED_DATA_DIR/languages/tailwindcss-language-server"; ' + 'mkdir -p "$TAILWIND_DIR"; ' + 'npm install --prefix "$TAILWIND_DIR" --save-exact @tailwindcss/language-server' + ), + ) - async def setup(self, environment: BaseEnvironment) -> None: - await environment.exec(command="mkdir -p /installed-agent") + # eslint LSP (downloaded from zed-industries/vscode-eslint GitHub release, + # then compiled — this mirrors what Zed does at runtime). + await self.exec_as_agent( + environment, + command=( + "set -euo pipefail; " + 'ZED_DATA_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/zed"; ' + 'ESLINT_DIR="$ZED_DATA_DIR/languages/eslint/vscode-eslint-2.4.4"; ' + 'mkdir -p "$ESLINT_DIR"; ' + 'curl -fsSL "https://github.com/zed-industries/vscode-eslint/archive/refs/tags/release/2.4.4.tar.gz" ' + '| tar -xz -C "$ESLINT_DIR"; ' + 'mv "$ESLINT_DIR"/vscode-eslint-release-2.4.4 "$ESLINT_DIR/vscode-eslint"; ' + 'cd "$ESLINT_DIR/vscode-eslint" && npm install && npm run compile' + ), + ) + + # gopls (Go - default LSP). Only install when Go is present in the + # container (i.e. Go-related SWE-bench tasks). + await self.exec_as_agent( + environment, + command=( + "if command -v go >/dev/null 2>&1; then " + "go install golang.org/x/tools/gopls@latest; " + "fi" + ), + ) + + await self.exec_as_agent( + environment, + command=( + "curl -LsSf https://astral.sh/uv/install.sh | sh && " + '. "$HOME/.local/bin/env"' + ), + ) + + agent_home_result = await self.exec_as_agent( + environment, + command='printf %s "$HOME"', + ) + agent_home = agent_home_result.stdout.strip() + if not agent_home: + raise RuntimeError("Could not determine agent home directory") + + await self.exec_as_root( + environment, + command=( + f"ln -sf {shlex.quote(agent_home + '/.local/bin/uv')} /usr/local/bin/uv && " + f"ln -sf {shlex.quote(agent_home + '/.local/bin/uvx')} /usr/local/bin/uvx" + ), + ) + + # Install a modern ruff so `ruff server` works without --preview. + # This also makes it available as a CLI tool for the agent. + await self.exec_as_agent( + environment, + command=('export PATH="$HOME/.local/bin:$PATH" && uv tool install ruff'), + ) if self._binary_path: binary = Path(self._binary_path) @@ -69,18 +200,29 @@ class ZedAgent(BaseInstalledAgent): source_path=binary, target_path="/usr/local/bin/eval-cli", ) - await environment.exec(command="chmod +x /usr/local/bin/eval-cli") - - await super().setup(environment) + await self.exec_as_root( + environment, + command="chmod +x /usr/local/bin/eval-cli && eval-cli --help", + ) + return - @property - def _template_variables(self) -> dict[str, str]: - variables = super()._template_variables - if self._binary_path: - variables["binary_uploaded"] = "true" if self._download_url: - variables["download_url"] = self._download_url - return variables + await self.exec_as_root( + environment, + command=( + f"curl -fsSL {shlex.quote(self._download_url)} " + "-o /usr/local/bin/eval-cli && " + "chmod +x /usr/local/bin/eval-cli && " + "eval-cli --help" + ), + ) + return + + raise ValueError( + "No eval-cli binary provided. " + "Either pass binary_path=/path/to/target/release/eval-cli " + "or set download_url=/EVAL_CLI_DOWNLOAD_URL." + ) def populate_context_post_run(self, context: AgentContext) -> None: result_data = None @@ -131,18 +273,27 @@ class ZedAgent(BaseInstalledAgent): return env - def create_run_agent_commands(self, instruction: str) -> list[ExecInput]: + @with_prompt_template + async def run( + self, instruction: str, environment: BaseEnvironment, context: AgentContext + ) -> None: escaped_instruction = shlex.quote(instruction) env = self._get_api_env() - parts = ["eval-cli", "--workdir /testbed", "--output-dir /logs/agent"] + workdir = await self._detect_workdir(environment) + + parts = [ + "eval-cli", + f"--workdir {shlex.quote(workdir)}", + "--output-dir /logs/agent", + ] if self.model_name: - parts.append(f"--model {self.model_name}") + parts.append(f"--model {shlex.quote(self.model_name)}") timeout = self._extra_env.get("EVAL_CLI_TIMEOUT") if timeout: - parts.append(f"--timeout {timeout}") + parts.append(f"--timeout {shlex.quote(timeout)}") staff = self._extra_env.get("EVAL_CLI_STAFF") if staff and staff.lower() == "false": @@ -161,18 +312,20 @@ class ZedAgent(BaseInstalledAgent): parts.append(f"--instruction {escaped_instruction}") - eval_cli_command = ( - " ".join(parts) + " 2>&1 | stdbuf -oL tee /logs/agent/eval-cli.txt" + await self.exec_as_agent( + environment, + command=( + " ".join(parts) + " 2>&1 | stdbuf -oL tee /logs/agent/eval-cli.txt" + ), + env=env, ) - patch_command = ( - "cd /testbed && " - "git add -A && " - "git diff --cached HEAD > /logs/agent/patch.diff && " - 'echo "Patch size: $(wc -c < /logs/agent/patch.diff) bytes"' + await self.exec_as_agent( + environment, + command=( + "git add -A && " + "git diff --cached HEAD > /logs/agent/patch.diff && " + 'echo "Patch size: $(wc -c < /logs/agent/patch.diff) bytes"' + ), + cwd=workdir, ) - - return [ - ExecInput(command=eval_cli_command, env=env), - ExecInput(command=patch_command), - ] diff --git a/crates/eval_cli/zed_eval/install.sh.j2 b/crates/eval_cli/zed_eval/install.sh.j2 deleted file mode 100644 index 80b1fae991cf37ebf07df1784e1cefcfd8fc7209..0000000000000000000000000000000000000000 --- a/crates/eval_cli/zed_eval/install.sh.j2 +++ /dev/null @@ -1,55 +0,0 @@ -#!/bin/bash -set -euo pipefail - -# Install runtime dependencies needed by the eval-cli binary (dynamically linked -# against glibc + these shared libraries from its GPUI/terminal/language stacks). -apt-get update -apt-get install -y --no-install-recommends \ - ca-certificates \ - curl \ - git \ - libasound2 \ - libfontconfig1 \ - libglib2.0-0 \ - libsqlite3-0 \ - libssl3 \ - libwayland-client0 \ - libx11-xcb1 \ - libxkbcommon-x11-0 \ - libzstd1 - -# Install Node.js 22 LTS (needed by language servers like basedpyright). -curl -fsSL https://deb.nodesource.com/setup_22.x | bash - -apt-get install -y --no-install-recommends nodejs - -# Preinstall basedpyright in Zed's language server cache to avoid first-run npm install latency. -ZED_DATA_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/zed" -BASEDPYRIGHT_DIR="$ZED_DATA_DIR/languages/basedpyright" -mkdir -p "$BASEDPYRIGHT_DIR" -npm install --prefix "$BASEDPYRIGHT_DIR" --save-exact basedpyright - -# Install uv (needed for running Python tests in SWE-bench tasks). -curl -LsSf https://astral.sh/uv/install.sh | sh -. "$HOME/.local/bin/env" -ln -sf "$HOME/.local/bin/uv" /usr/local/bin/uv -ln -sf "$HOME/.local/bin/uvx" /usr/local/bin/uvx - -{% if binary_uploaded is defined %} -# Binary was uploaded directly via setup() — just verify it works. -eval-cli --help -{% elif download_url is defined %} -curl -fsSL "{{ download_url }}" -o /usr/local/bin/eval-cli -chmod +x /usr/local/bin/eval-cli -eval-cli --help -{% else %} -echo "ERROR: No eval-cli binary provided." -echo "" -echo "Either pass binary_path= to upload a local build:" -echo " --ae binary_path=/path/to/target/release/eval-cli" -echo "" -echo "Or set download_url= / EVAL_CLI_DOWNLOAD_URL:" -echo " --ae download_url=https://example.com/eval-cli" -exit 1 -{% endif %} - -echo "INSTALL_SUCCESS" diff --git a/crates/sidebar/Cargo.toml b/crates/sidebar/Cargo.toml index a75a6b1af7a26723c1691b27676072c1869b5847..b788fd0c00aebc5d6c4acda0df2719e9adf9af11 100644 --- a/crates/sidebar/Cargo.toml +++ b/crates/sidebar/Cargo.toml @@ -20,7 +20,7 @@ action_log.workspace = true agent.workspace = true agent-client-protocol.workspace = true agent_settings.workspace = true -agent_ui.workspace = true +agent_ui = { workspace = true, features = ["audio"] } anyhow.workspace = true chrono.workspace = true collections.workspace = true diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index a602220f2c02a0e510b46e86f4cec5fed2488ac9..c24371667e7d3f984f0960f6b3f18d5d0f1e5f4c 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -68,7 +68,7 @@ activity_indicator.workspace = true agent.workspace = true agent-client-protocol.workspace = true agent_settings.workspace = true -agent_ui.workspace = true +agent_ui = { workspace = true, features = ["audio"] } anyhow.workspace = true askpass.workspace = true assets.workspace = true From 3060e4170ea5ef0e6886b9ac1853aaead9ddd59f Mon Sep 17 00:00:00 2001 From: Gustaf Johansson Date: Mon, 30 Mar 2026 00:11:57 +0200 Subject: [PATCH 6/6] gpui: Add middle and right click mouse events for macOS (#49637) - [x] Added a solid test coverage and/or screenshots from doing manual testing Manual testing performed successfully on Tahoe 26.2. - [x] Done a self-review taking into account security and performance aspects - [x] Aligned any UI changes with the [UI checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) Release Notes: - Fixed missing right- and middle-mouse button drag on MacOS Co-authored-by: MrSubidubi --- crates/gpui_macos/src/window.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/crates/gpui_macos/src/window.rs b/crates/gpui_macos/src/window.rs index 51758ae095ef29901dd3fbb550e9c863ceaf3762..398cf46eab09dc8412ffdda8eb550b8ad4e09b40 100644 --- a/crates/gpui_macos/src/window.rs +++ b/crates/gpui_macos/src/window.rs @@ -188,6 +188,14 @@ unsafe fn build_classes() { sel!(mouseDragged:), handle_view_event as extern "C" fn(&Object, Sel, id), ); + decl.add_method( + sel!(rightMouseDragged:), + handle_view_event as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(otherMouseDragged:), + handle_view_event as extern "C" fn(&Object, Sel, id), + ); decl.add_method( sel!(scrollWheel:), handle_view_event as extern "C" fn(&Object, Sel, id),