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
This commit is contained in:
Ben Brandt authored and GitHub committed 2026-06-30 13:03:45 +00:00
1 parent 647b78d5ef
commit ca8cae2eaa
2 files changed
+79 -1

No files matched your search

+12 -1
View File
@@ -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);
+67
View File
@@ -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<ConversationView>,
cx: &TestAppContext,