mirror of
https://github.com/zed-industries/zed.git
synced 2026-08-31 02:21:14 +00:00
agent: Improve compaction UX (#58913)
Makes it so that we show "Compaction cancelled" when the user interrupts generation. Also fixes another small issue where the chevron of the expanded state would not reflect if it is expanded/collapsed while there is no summary yet. Release Notes: - N/A
This commit is contained in:
parent
9baefe701e
commit
e17e272d24
3 changed files with 106 additions and 43 deletions
|
|
@ -298,12 +298,20 @@ pub enum AgentThreadEntry {
|
|||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ContextCompactionId(pub Arc<str>);
|
||||
|
||||
#[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<Entity<Markdown>>,
|
||||
|
|
@ -313,6 +321,7 @@ pub struct ContextCompaction {
|
|||
pub struct ContextCompactionUpdate {
|
||||
pub id: ContextCompactionId,
|
||||
pub summary_delta: String,
|
||||
pub status: Option<ContextCompactionStatus>,
|
||||
}
|
||||
|
||||
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<Self>) {
|
||||
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));
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1407,11 +1407,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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2685,7 +2691,10 @@ impl Thread {
|
|||
) -> Result<ControlFlow<()>> {
|
||||
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() => {
|
||||
|
|
@ -2749,6 +2758,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())));
|
||||
|
|
@ -4632,10 +4645,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();
|
||||
}
|
||||
|
|
@ -4650,6 +4671,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();
|
||||
|
|
|
|||
|
|
@ -3594,16 +3594,13 @@ impl ThreadView {
|
|||
fn render_context_compaction(
|
||||
&self,
|
||||
entry_ix: usize,
|
||||
total_entries: usize,
|
||||
compaction: &acp_thread::ContextCompaction,
|
||||
window: &Window,
|
||||
cx: &Context<Self>,
|
||||
) -> 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 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))
|
||||
|
|
@ -3621,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),
|
||||
|
|
@ -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)
|
||||
|
|
@ -5653,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)
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue