From a0d7698867a6e05934a12fe0f217933de99bbff3 Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Wed, 25 Feb 2026 08:10:15 +0100 Subject: [PATCH] agent: Delay edit tool buffer clearing until the first chunk is sent (#49633) Release Notes: - The agent edit tool no longer clears files until the first edit comes in, preventing a buffer being empty for prolonged time if the agent is slow in reporting the first text chunk --- crates/agent/src/edit_agent.rs | 106 ++++++++++++++++++++++----------- 1 file changed, 71 insertions(+), 35 deletions(-) diff --git a/crates/agent/src/edit_agent.rs b/crates/agent/src/edit_agent.rs index 3e67cba1b63f4136a03b88c3007aee99489a6e80..9f2f2f1877e20620373b1a7aacbf2f7b3a407bfd 100644 --- a/crates/agent/src/edit_agent.rs +++ b/crates/agent/src/edit_agent.rs @@ -166,56 +166,69 @@ impl EditAgent { output_events_tx: mpsc::UnboundedSender, cx: &mut AsyncApp, ) -> Result<()> { - cx.update(|cx| { - buffer.update(cx, |buffer, cx| buffer.set_text("", cx)); - self.action_log.update(cx, |log, cx| { - log.buffer_edited(buffer.clone(), cx); - }); + let buffer_id = cx.update(|cx| { + let buffer_id = buffer.read(cx).remote_id(); self.project.update(cx, |project, cx| { project.set_agent_location( Some(AgentLocation { buffer: buffer.downgrade(), - position: language::Anchor::max_for_buffer(buffer.read(cx).remote_id()), + position: language::Anchor::min_for_buffer(buffer_id), }), cx, ) }); + buffer_id + }); + + let send_edit_event = || { output_events_tx .unbounded_send(EditAgentOutputEvent::Edited( - Anchor::min_max_range_for_buffer(buffer.read(cx).remote_id()), + Anchor::min_max_range_for_buffer(buffer_id), )) - .ok(); - }); - + .ok() + }; + let set_agent_location = |cx: &mut _| { + self.project.update(cx, |project, cx| { + project.set_agent_location( + Some(AgentLocation { + buffer: buffer.downgrade(), + position: language::Anchor::max_for_buffer(buffer_id), + }), + cx, + ) + }) + }; + let mut first_chunk = true; while let Some(event) = parse_rx.next().await { match event? { CreateFileParserEvent::NewTextChunk { chunk } => { - let buffer_id = cx.update(|cx| { - buffer.update(cx, |buffer, cx| buffer.append(chunk, cx)); + cx.update(|cx| { + buffer.update(cx, |buffer, cx| { + if mem::take(&mut first_chunk) { + buffer.set_text(chunk, cx) + } else { + buffer.append(chunk, cx) + } + }); self.action_log .update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx)); - self.project.update(cx, |project, cx| { - project.set_agent_location( - Some(AgentLocation { - buffer: buffer.downgrade(), - position: language::Anchor::max_for_buffer( - buffer.read(cx).remote_id(), - ), - }), - cx, - ) - }); - buffer.read(cx).remote_id() + set_agent_location(cx); }); - output_events_tx - .unbounded_send(EditAgentOutputEvent::Edited( - Anchor::min_max_range_for_buffer(buffer_id), - )) - .ok(); + send_edit_event(); } } } + if first_chunk { + cx.update(|cx| { + buffer.update(cx, |buffer, cx| buffer.set_text("", cx)); + self.action_log + .update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx)); + set_agent_location(cx); + }); + send_edit_event(); + } + Ok(()) } @@ -1194,19 +1207,16 @@ mod tests { ); cx.run_until_parked(); - assert_matches!( - drain_events(&mut events).as_slice(), - [EditAgentOutputEvent::Edited(_)] - ); + assert_eq!(drain_events(&mut events).as_slice(), []); assert_eq!( buffer.read_with(cx, |buffer, _| buffer.snapshot().text()), - "" + "abc\ndef\nghi" ); assert_eq!( project.read_with(cx, |project, _| project.agent_location()), Some(AgentLocation { buffer: buffer.downgrade(), - position: language::Anchor::max_for_buffer( + position: language::Anchor::min_for_buffer( cx.update(|cx| buffer.read(cx).remote_id()) ), }) @@ -1290,6 +1300,32 @@ mod tests { ); } + #[gpui::test] + async fn test_overwrite_no_content(cx: &mut TestAppContext) { + let agent = init_test(cx).await; + let buffer = cx.new(|cx| Buffer::local("abc\ndef\nghi", cx)); + let (chunks_tx, chunks_rx) = mpsc::unbounded::<&str>(); + let (apply, mut events) = agent.overwrite_with_chunks( + buffer.clone(), + chunks_rx.map(|chunk| Ok(chunk.to_string())), + &mut cx.to_async(), + ); + + drop(chunks_tx); + cx.run_until_parked(); + + let result = apply.await; + assert!(result.is_ok(),); + assert_matches!( + drain_events(&mut events).as_slice(), + [EditAgentOutputEvent::Edited { .. }] + ); + assert_eq!( + buffer.read_with(cx, |buffer, _| buffer.snapshot().text()), + "" + ); + } + #[gpui::test(iterations = 100)] async fn test_indent_new_text_chunks(mut rng: StdRng) { let chunks = to_random_chunks(&mut rng, " abc\n def\n ghi");