From 079aeccf67fc6f9e7cd0522fbc78ef79e762a966 Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner Date: Tue, 9 Jun 2026 13:03:02 +0200 Subject: [PATCH 1/2] Show as expanded even with no summary --- crates/agent_ui/src/conversation_view/thread_view.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/crates/agent_ui/src/conversation_view/thread_view.rs b/crates/agent_ui/src/conversation_view/thread_view.rs index 58f2e804c95e09..203c9104ba91c6 100644 --- a/crates/agent_ui/src/conversation_view/thread_view.rs +++ b/crates/agent_ui/src/conversation_view/thread_view.rs @@ -3602,8 +3602,7 @@ impl ThreadView { let is_compacting = entry_ix + 1 == total_entries && self.thread.read(cx).status() == acp_thread::ThreadStatus::Generating; let summary = compaction.summary.clone(); - let summary_available = summary.is_some(); - let is_expanded = summary_available && self.expanded_compactions.contains(&entry_ix); + let is_expanded = self.expanded_compactions.contains(&entry_ix); let header = h_flex() .id(("context-compaction", entry_ix)) @@ -3644,7 +3643,9 @@ impl ThreadView { this.toggle_compaction_expansion(entry_ix, cx); })); - if let Some(summary) = summary.filter(|_| is_expanded) { + if let Some(summary) = summary + && is_expanded + { v_flex() .w_full() .child(header) From c2dbe88f4142f34bc4653a8f2ac47947d4eaae52 Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner Date: Tue, 9 Jun 2026 13:23:47 +0200 Subject: [PATCH 2/2] agent: Improve compaction UX --- crates/acp_thread/src/acp_thread.rs | 78 ++++++++++++------- crates/agent/src/thread.rs | 48 ++++++++++-- .../src/conversation_view/thread_view.rs | 16 ++-- 3 files changed, 102 insertions(+), 40 deletions(-) diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index 421a3afb7f0a01..cff95f0b62a3cd 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -298,12 +298,20 @@ pub enum AgentThreadEntry { #[derive(Debug, Clone, PartialEq, Eq)] pub struct ContextCompactionId(pub Arc); +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ContextCompactionStatus { + InProgress, + Completed, + Canceled, +} + /// A point in the thread where the conversation history was compacted to free /// up room in the model's context window. The summary can be expanded to inspect /// what the model retained. #[derive(Debug)] pub struct ContextCompaction { pub id: ContextCompactionId, + pub status: ContextCompactionStatus, /// The compaction summary, streamed in as the model produces it. This is /// `None` for provider-native compaction, which produces no summary to show. pub summary: Option>, @@ -313,6 +321,7 @@ pub struct ContextCompaction { pub struct ContextCompactionUpdate { pub id: ContextCompactionId, pub summary_delta: String, + pub status: Option, } impl AgentThreadEntry { @@ -2063,19 +2072,25 @@ impl AcpThread { return; }; - if compaction.summary.is_none() { - compaction.summary = Some(cx.new(|cx| { - Markdown::new( - update.summary_delta.into(), - Some(language_registry), - None, - cx, - ) - })); - } else if let Some(summary) = compaction.summary.clone() { - summary.update(cx, |markdown, cx| { - markdown.append(&update.summary_delta, cx) - }); + if !update.summary_delta.is_empty() { + if compaction.summary.is_none() { + compaction.summary = Some(cx.new(|cx| { + Markdown::new( + update.summary_delta.into(), + Some(language_registry), + None, + cx, + ) + })); + } else if let Some(summary) = compaction.summary.clone() { + summary.update(cx, |markdown, cx| { + markdown.append(&update.summary_delta, cx) + }); + } + } + + if let Some(status) = update.status { + compaction.status = status; } cx.emit(AcpThreadEvent::EntryUpdated(ix)); @@ -2669,7 +2684,7 @@ impl AcpThread { let canceled = matches!(r.stop_reason, acp::StopReason::Cancelled); if canceled { - this.mark_pending_tools_as_canceled(); + this.mark_pending_entries_as_canceled(cx); } if !canceled { @@ -2745,25 +2760,34 @@ impl AcpThread { self.connection.cancel(&self.session_id, cx); Self::flush_streaming_text(&mut self.streaming_text_buffer, cx); - self.mark_pending_tools_as_canceled(); + self.mark_pending_entries_as_canceled(cx); // Wait for the send task to complete cx.background_spawn(turn.send_task) } - fn mark_pending_tools_as_canceled(&mut self) { - for entry in self.entries.iter_mut() { - if let AgentThreadEntry::ToolCall(call) = entry { - let cancel = matches!( - call.status, - ToolCallStatus::Pending - | ToolCallStatus::WaitingForConfirmation { .. } - | ToolCallStatus::InProgress - ); - - if cancel { - call.status = ToolCallStatus::Canceled; + fn mark_pending_entries_as_canceled(&mut self, cx: &mut Context) { + for (ix, entry) in self.entries.iter_mut().enumerate() { + match entry { + AgentThreadEntry::ToolCall(call) => { + let cancel = matches!( + call.status, + ToolCallStatus::Pending + | ToolCallStatus::WaitingForConfirmation { .. } + | ToolCallStatus::InProgress + ); + if cancel { + call.status = ToolCallStatus::Canceled; + cx.emit(AcpThreadEvent::EntryUpdated(ix)); + } + } + AgentThreadEntry::ContextCompaction(compaction) => { + if compaction.status == ContextCompactionStatus::InProgress { + compaction.status = ContextCompactionStatus::Canceled; + cx.emit(AcpThreadEvent::EntryUpdated(ix)); + } } + _ => {} } } } diff --git a/crates/agent/src/thread.rs b/crates/agent/src/thread.rs index 3fd029e8d74464..414253ef572049 100644 --- a/crates/agent/src/thread.rs +++ b/crates/agent/src/thread.rs @@ -1413,11 +1413,17 @@ impl Thread { ); match info { CompactionInfo::Summary(summary) => { - stream.send_context_compaction(compaction_id.clone()); + stream.send_context_compaction( + compaction_id.clone(), + acp_thread::ContextCompactionStatus::Completed, + ); stream.send_context_compaction_update(compaction_id.clone(), summary); } CompactionInfo::ProviderNative { .. } => { - stream.send_context_compaction(compaction_id); + stream.send_context_compaction( + compaction_id, + acp_thread::ContextCompactionStatus::Completed, + ); } } } @@ -2691,7 +2697,10 @@ impl Thread { ) -> Result> { log::debug!("Running compaction"); let compaction_id = acp_thread::ContextCompactionId(Uuid::new_v4().to_string().into()); - event_stream.send_context_compaction(compaction_id.clone()); + event_stream.send_context_compaction( + compaction_id.clone(), + acp_thread::ContextCompactionStatus::InProgress, + ); let stream = futures::select! { result = model.stream_completion(request, cx).fuse() => result, _ = cancellation_rx.changed().fuse() => { @@ -2755,6 +2764,10 @@ impl Thread { } log::debug!("Compaction succeeded:\n{summary}"); + event_stream.update_context_compaction_status( + compaction_id, + acp_thread::ContextCompactionStatus::Completed, + ); this.update(cx, |this, cx| { let compaction = Arc::new(Message::Compaction(CompactionInfo::Summary(summary.into()))); @@ -4626,10 +4639,18 @@ impl ThreadEventStream { self.0.unbounded_send(Ok(ThreadEvent::Retry(status))).ok(); } - fn send_context_compaction(&self, id: acp_thread::ContextCompactionId) { + fn send_context_compaction( + &self, + id: acp_thread::ContextCompactionId, + status: acp_thread::ContextCompactionStatus, + ) { self.0 .unbounded_send(Ok(ThreadEvent::ContextCompaction( - acp_thread::ContextCompaction { id, summary: None }, + acp_thread::ContextCompaction { + id, + status, + summary: None, + }, ))) .ok(); } @@ -4644,6 +4665,23 @@ impl ThreadEventStream { acp_thread::ContextCompactionUpdate { id, summary_delta: summary_delta.to_string(), + status: None, + }, + ))) + .ok(); + } + + fn update_context_compaction_status( + &self, + id: acp_thread::ContextCompactionId, + status: acp_thread::ContextCompactionStatus, + ) { + self.0 + .unbounded_send(Ok(ThreadEvent::ContextCompactionUpdate( + acp_thread::ContextCompactionUpdate { + id, + summary_delta: String::new(), + status: Some(status), }, ))) .ok(); diff --git a/crates/agent_ui/src/conversation_view/thread_view.rs b/crates/agent_ui/src/conversation_view/thread_view.rs index 203c9104ba91c6..000cb5d6c2ffe2 100644 --- a/crates/agent_ui/src/conversation_view/thread_view.rs +++ b/crates/agent_ui/src/conversation_view/thread_view.rs @@ -3594,13 +3594,11 @@ impl ThreadView { fn render_context_compaction( &self, entry_ix: usize, - total_entries: usize, compaction: &acp_thread::ContextCompaction, window: &Window, cx: &Context, ) -> AnyElement { - let is_compacting = entry_ix + 1 == total_entries - && self.thread.read(cx).status() == acp_thread::ThreadStatus::Generating; + let is_compacting = compaction.status == acp_thread::ContextCompactionStatus::InProgress; let summary = compaction.summary.clone(); let is_expanded = self.expanded_compactions.contains(&entry_ix); @@ -3620,10 +3618,12 @@ impl ThreadView { .color(Color::Muted), ) .child( - Label::new(if is_compacting { - "Compacting context…" - } else { - "Context compacted" + Label::new(match compaction.status { + acp_thread::ContextCompactionStatus::InProgress => { + "Compacting context…" + } + acp_thread::ContextCompactionStatus::Completed => "Context compacted", + acp_thread::ContextCompactionStatus::Canceled => "Compaction cancelled", }) .size(LabelSize::Custom(self.tool_name_font_size())) .color(Color::Muted), @@ -5654,7 +5654,7 @@ impl ThreadView { self.render_completed_plan(entries, window, cx) } AgentThreadEntry::ContextCompaction(compaction) => { - self.render_context_compaction(entry_ix, total_entries, compaction, window, cx) + self.render_context_compaction(entry_ix, compaction, window, cx) } };