Fix repeated prompts in opencode acp (#53216)

Om Chillure created

### Summary
Fixes duplicated Prompts/context in ACP threads after sending a message,
as reported in #53201.

### Root Cause
The thread already inserts the user prompt optimistically at send time.
If an ACP server also echoes UserMessageChunk updates for the same
prompt, the same content is appended again, which can duplicate rendered
context sections.

### Fix
Ignore echoed UserMessageChunk updates while a turn is actively running,
so user prompt content is not appended twice.

### Validation
- Reproduced with OpenCode ACP flow from the issue.
- Confirmed duplication appears after send (not in the input box).
- Confirmed duplicate Prompts/context no longer appears with the fix.

### Video 

[Screencast from 2026-04-06
08-21-32.webm](https://github.com/user-attachments/assets/33075312-9af7-4dd5-a2a3-5e1169b80243)


### 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 #53201

## Release Notes
- Fixed duplicated Prompts/context in ACP conversations when servers
echo user message chunks after send.

Change summary

crates/acp_thread/src/acp_thread.rs | 58 ++++++++++++++++++++++++++++++
1 file changed, 57 insertions(+), 1 deletion(-)

Detailed changes

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);