diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index 36c9fb40c4a573e09da05618a29c1898cced60ad..7fb48c132f971fd3449d116b22bd4437c1ebf611 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -1393,7 +1393,17 @@ impl AcpThread { ) -> Result<(), acp::Error> { match update { acp::SessionUpdate::UserMessageChunk(acp::ContentChunk { content, .. }) => { - self.push_user_content_block(None, content, cx); + // We optimistically add the full user prompt before calling `prompt`. + // Some ACP servers echo user chunks back over updates. Skip the chunk if + // it's already present in the current user message to avoid duplicating content. + let already_in_user_message = self + .entries + .last() + .and_then(|entry| entry.user_message()) + .is_some_and(|message| message.chunks.contains(&content)); + if !already_in_user_message { + self.push_user_content_block(None, content, cx); + } } acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk { content, .. }) => { self.push_assistant_content_block(content, false, cx); @@ -3440,6 +3450,52 @@ mod tests { ); } + #[gpui::test] + async fn test_ignore_echoed_user_message_chunks_during_active_turn( + cx: &mut gpui::TestAppContext, + ) { + init_test(cx); + + let fs = FakeFs::new(cx.executor()); + let project = Project::test(fs, [], cx).await; + let connection = Rc::new(FakeAgentConnection::new().on_user_message( + |request, thread, mut cx| { + async move { + let prompt = request.prompt.first().cloned().unwrap_or_else(|| "".into()); + + thread.update(&mut cx, |thread, cx| { + thread + .handle_session_update( + acp::SessionUpdate::UserMessageChunk(acp::ContentChunk::new( + prompt, + )), + cx, + ) + .unwrap(); + })?; + + Ok(acp::PromptResponse::new(acp::StopReason::EndTurn)) + } + .boxed_local() + }, + )); + + let thread = cx + .update(|cx| { + connection.new_session(project, PathList::new(&[Path::new(path!("/test"))]), cx) + }) + .await + .unwrap(); + + thread + .update(cx, |thread, cx| thread.send_raw("Hello from Zed!", cx)) + .await + .unwrap(); + + let output = thread.read_with(cx, |thread, cx| thread.to_markdown(cx)); + assert_eq!(output.matches("Hello from Zed!").count(), 1); + } + #[gpui::test] async fn test_edits_concurrently_to_user(cx: &mut TestAppContext) { init_test(cx);