From ca8cae2eaa825dd6d2a3bb44d82d2a809873a29e Mon Sep 17 00:00:00 2001 From: Ben Brandt Date: Tue, 30 Jun 2026 15:03:45 +0200 Subject: [PATCH] acp_thread: Defer status updates until turn completion (#60148) We were signaling this too early, causing the syncing generator to get updated too soon. Release Notes: - N/A --- crates/acp_thread/src/acp_thread.rs | 13 ++++- crates/agent_ui/src/conversation_view.rs | 67 ++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 1 deletion(-) diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index d060ecc26a..e85a0bd48f 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -3296,10 +3296,12 @@ impl AcpThread { // state even when the send_task is cancelled before tx.send(). if is_same_turn { this.running_turn.take(); - cx.emit(AcpThreadEvent::StatusChanged); } let Ok(response) = response else { + if is_same_turn { + cx.emit(AcpThreadEvent::StatusChanged); + } // tx dropped, just return return Ok(None); }; @@ -3309,6 +3311,9 @@ impl AcpThread { Self::flush_streaming_text(&mut this.streaming_text_buffer, cx); if r.stop_reason == acp::StopReason::MaxTokens { + if is_same_turn { + cx.emit(AcpThreadEvent::StatusChanged); + } this.had_error = true; cx.emit(AcpThreadEvent::Error); log::error!("Max tokens reached. Usage: {:?}", this.token_usage); @@ -3387,10 +3392,16 @@ impl AcpThread { cx.emit(AcpThreadEvent::TokenUsageUpdated); } + if is_same_turn { + cx.emit(AcpThreadEvent::StatusChanged); + } cx.emit(AcpThreadEvent::Stopped(r.stop_reason)); Ok(Some(r)) } Err(e) => { + if is_same_turn { + cx.emit(AcpThreadEvent::StatusChanged); + } Self::flush_streaming_text(&mut this.streaming_text_buffer, cx); if is_same_turn { this.mark_pending_entries_as_canceled(cx); diff --git a/crates/agent_ui/src/conversation_view.rs b/crates/agent_ui/src/conversation_view.rs index 43b10a4bd0..5267e7adfa 100644 --- a/crates/agent_ui/src/conversation_view.rs +++ b/crates/agent_ui/src/conversation_view.rs @@ -4906,6 +4906,66 @@ pub(crate) mod tests { setup_conversation_view_with_initial_content_opt(agent, None, cx).await } + #[gpui::test] + async fn test_completed_plan_snapshot_keeps_list_state_in_sync(cx: &mut TestAppContext) { + init_test(cx); + + let connection = StubAgentConnection::new(); + let (conversation_view, cx) = + setup_conversation_view(StubAgentServer::new(connection.clone()), cx).await; + + message_editor(&conversation_view, cx).update_in(cx, |editor, window, cx| { + editor.set_text("Hello", window, cx); + }); + active_thread(&conversation_view, cx).update_in(cx, |view, window, cx| { + view.send(window, cx); + }); + cx.run_until_parked(); + + let session_id = active_thread(&conversation_view, cx).read_with(cx, |view, cx| { + assert_thread_list_item_count_matches_entries(view, cx); + view.thread.read(cx).session_id().clone() + }); + + cx.update(|_, cx| { + connection.send_update( + session_id.clone(), + acp::SessionUpdate::Plan(acp::Plan::new(vec![acp::PlanEntry::new( + "Do the thing", + acp::PlanEntryPriority::Medium, + acp::PlanEntryStatus::InProgress, + )])), + cx, + ); + }); + cx.run_until_parked(); + active_thread(&conversation_view, cx).read_with(cx, |view, cx| { + assert_thread_list_item_count_matches_entries(view, cx); + }); + + cx.update(|_, cx| { + connection.send_update( + session_id.clone(), + acp::SessionUpdate::Plan(acp::Plan::new(vec![acp::PlanEntry::new( + "Do the thing", + acp::PlanEntryPriority::Medium, + acp::PlanEntryStatus::Completed, + )])), + cx, + ); + }); + cx.run_until_parked(); + active_thread(&conversation_view, cx).read_with(cx, |view, cx| { + assert_thread_list_item_count_matches_entries(view, cx); + }); + + connection.end_turn(session_id, acp::StopReason::EndTurn); + cx.run_until_parked(); + active_thread(&conversation_view, cx).read_with(cx, |view, cx| { + assert_thread_list_item_count_matches_entries(view, cx); + }); + } + async fn setup_conversation_view_with_initial_content( agent: impl AgentServer + 'static, initial_content: AgentInitialContent, @@ -5557,6 +5617,13 @@ pub(crate) mod tests { }) } + fn assert_thread_list_item_count_matches_entries(view: &ThreadView, cx: &App) { + assert_eq!( + view.list_state.item_count(), + view.thread.read(cx).entries().len() + usize::from(view.generating_indicator_in_list) + ); + } + fn message_editor( conversation_view: &Entity, cx: &TestAppContext,