diff --git a/assets/icons/compact.svg b/assets/icons/compact.svg new file mode 100644 index 00000000000..68f6beb1bec --- /dev/null +++ b/assets/icons/compact.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index cff95f0b62a..27923a90d37 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -317,6 +317,12 @@ pub struct ContextCompaction { pub summary: Option>, } +impl ContextCompaction { + pub fn is_in_progress(&self) -> bool { + self.status == ContextCompactionStatus::InProgress + } +} + #[derive(Debug)] pub struct ContextCompactionUpdate { pub id: ContextCompactionId, @@ -1565,6 +1571,15 @@ impl AcpThread { &self.entries } + pub fn is_compacting(&self) -> bool { + self.entries.last().is_some_and(|entry| { + matches!( + entry, + AgentThreadEntry::ContextCompaction(compaction) if compaction.is_in_progress() + ) + }) + } + pub fn invalidate_mermaid_caches(&self, cx: &mut App) { for entry in &self.entries { let chunks = match entry { diff --git a/crates/agent/src/agent.rs b/crates/agent/src/agent.rs index 4ee023d3fa7..701d61746a9 100644 --- a/crates/agent/src/agent.rs +++ b/crates/agent/src/agent.rs @@ -164,7 +164,7 @@ impl From<&Skill> for NativeAvailableSkill { } } -const COMPACT_COMMAND_NAME: &str = "compact"; +pub const COMPACT_COMMAND_NAME: &str = "compact"; /// Returns the set of MCP prompt names that must be server-qualified /// (`/.`) to stay unambiguous in the slash-command popup: names diff --git a/crates/agent_ui/src/completion_provider.rs b/crates/agent_ui/src/completion_provider.rs index 01c58ab38a2..58b4bb9bbe9 100644 --- a/crates/agent_ui/src/completion_provider.rs +++ b/crates/agent_ui/src/completion_provider.rs @@ -1500,6 +1500,11 @@ impl CompletionProvider for PromptCompletio command.requires_argument && argument.is_none(); let group = show_section_headers.then(|| command.group()); + let icon_path = (command.category + == Some(acp_thread::CommandCategory::Native) + && command.name.as_ref() == agent::COMPACT_COMMAND_NAME) + .then(|| IconName::Compact.path().into()); + Completion { replace_range: source_range.clone(), new_text, @@ -1510,7 +1515,7 @@ impl CompletionProvider for PromptCompletio ), ), source: project::CompletionSource::Custom, - icon_path: None, + icon_path, icon_color: None, match_start: None, snippet_deduplication_key: None, @@ -2458,9 +2463,7 @@ fn build_slash_item_label( }; let mut builder = CodeLabelBuilder::default(); builder.push_str(name, None); - // Two spaces gives a touch of breathing room between the name and - // the muted source label. - builder.push_str(" ", None); + builder.push_str(" ", None); builder.push_str(source, source_highlight_id); // The filter range defaults to the entire label after `build()`, // which would let the source text participate in fuzzy filtering. diff --git a/crates/agent_ui/src/conversation_view.rs b/crates/agent_ui/src/conversation_view.rs index 7fc0a59f060..e1bf3ffe280 100644 --- a/crates/agent_ui/src/conversation_view.rs +++ b/crates/agent_ui/src/conversation_view.rs @@ -1540,6 +1540,7 @@ impl ConversationView { }); active.update(cx, |active, cx| { active.sync_editor_mode_for_empty_state(cx); + active.sync_generating_indicator(cx); }); } } @@ -1553,6 +1554,7 @@ impl ConversationView { list_state.remeasure_items(*index..*index + 1); active.update(cx, |active, cx| { active.auto_expand_streaming_thought(cx); + active.sync_generating_indicator(cx); }); } } diff --git a/crates/agent_ui/src/conversation_view/thread_view.rs b/crates/agent_ui/src/conversation_view/thread_view.rs index 000cb5d6c2f..8603b3552c7 100644 --- a/crates/agent_ui/src/conversation_view/thread_view.rs +++ b/crates/agent_ui/src/conversation_view/thread_view.rs @@ -3598,98 +3598,99 @@ impl ThreadView { window: &Window, cx: &Context, ) -> AnyElement { - let is_compacting = compaction.status == acp_thread::ContextCompactionStatus::InProgress; + let is_compacting = compaction.is_in_progress(); let summary = compaction.summary.clone(); let is_expanded = self.expanded_compactions.contains(&entry_ix); + let id = format!("context-compaction-{entry_ix}"); + let header_label = match compaction.status { + acp_thread::ContextCompactionStatus::InProgress => "Compacting Context…", + acp_thread::ContextCompactionStatus::Completed => "Context Compacted", + acp_thread::ContextCompactionStatus::Canceled => "Compaction Canceled", + }; + let chevron_end = if is_expanded { + IconName::ChevronUp + } else { + IconName::ChevronDown + }; let header = h_flex() - .id(("context-compaction", entry_ix)) - .px_5() - .py_1() - .gap_2() + .gap_1() .w_full() + .child(Divider::horizontal()) .child( - h_flex() - .flex_none() - .gap_1p5() - .child( - Icon::new(IconName::Scissors) + Button::new(id, header_label) + .label_size(LabelSize::Small) + .loading(is_compacting) + .disabled(is_compacting) + .start_icon( + Icon::new(IconName::Compact) .size(IconSize::XSmall) .color(Color::Muted), ) - .child( - 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), - ), + .when(!is_compacting, |this| { + this.end_icon( + Icon::new(chevron_end) + .size(IconSize::XSmall) + .color(Color::Muted), + ) + .on_click(cx.listener( + move |this, _event: &ClickEvent, _window, cx| { + this.toggle_compaction_expansion(entry_ix, cx); + }, + )) + }), ) - .child(if is_compacting { - div().flex_1().into_any_element() - } else { - Divider::horizontal().into_any_element() - }) - .child( - Disclosure::new(("compaction-disclosure", entry_ix), is_expanded) - .opened_icon(IconName::ChevronUp) - .closed_icon(IconName::ChevronDown), - ) - .on_click(cx.listener(move |this, _event: &ClickEvent, _window, cx| { - this.toggle_compaction_expansion(entry_ix, cx); - })); + .child(Divider::horizontal()); - if let Some(summary) = summary - && is_expanded - { - v_flex() - .w_full() - .child(header) - .child( - div() - .id(("compaction-summary", entry_ix)) - .mx_5() - .pl_3p5() - .border_l_1() - .border_color(self.tool_card_border_color(cx)) - .child(self.render_markdown( - summary, - MarkdownStyle::themed(MarkdownFont::Agent, window, cx), - cx, - )), - ) - .child( - div() - .mx_5() - .mb_1() - .pl_3p5() - .pt_2() - .border_l_1() - .border_color(self.tool_card_border_color(cx)) - .child( - IconButton::new( - ("compaction-summary-collapse", entry_ix), - IconName::ChevronUp, + div() + .px_5() + .w_full() + .child( + v_flex() + .pt_1p5() + .mb_1p5() + .gap_1p5() + .border_1() + .border_color(gpui::transparent_black()) + .rounded_sm() + .child(header) + .when_some(summary.filter(|_| is_expanded), |this, summary| { + this.border_color(self.tool_card_border_color(cx)) + .bg(cx.theme().colors().editor_background.opacity(0.2)) + .child( + div() + .id(("compaction-summary", entry_ix)) + .p_2() + .text_ui(cx) + .child(self.render_markdown( + summary, + MarkdownStyle::themed(MarkdownFont::Agent, window, cx), + cx, + )), ) - .full_width() - .style(ButtonStyle::Outlined) - .icon_color(Color::Muted) - .on_click(cx.listener( - move |this, _event: &ClickEvent, _window, cx| { - this.expanded_compactions.remove(&entry_ix); - cx.notify(); - }, - )), - ), - ) - .into_any() - } else { - header.into_any() - } + .child( + h_flex() + .border_t_1() + .border_color(self.tool_card_border_color(cx)) + .child( + IconButton::new( + ("compaction-summary-collapse", entry_ix), + IconName::ChevronUp, + ) + .full_width() + .on_click( + cx.listener( + move |this, _event: &ClickEvent, _window, cx| { + this.expanded_compactions.remove(&entry_ix); + cx.notify(); + }, + ), + ), + ), + ) + }), + ) + .into_any() } fn toggle_compaction_expansion(&mut self, entry_ix: usize, cx: &mut Context) { @@ -6193,7 +6194,10 @@ impl ThreadView { /// Ensures the list item count includes (or excludes) an extra item for the generating indicator pub(crate) fn sync_generating_indicator(&mut self, cx: &App) { - let is_generating = matches!(self.thread.read(cx).status(), ThreadStatus::Generating); + let thread = self.thread.read(cx); + + let is_generating = + matches!(thread.status(), ThreadStatus::Generating) && !thread.is_compacting(); if is_generating && !self.generating_indicator_in_list { let entries_count = self.thread.read(cx).entries().len(); diff --git a/crates/icons/src/icons.rs b/crates/icons/src/icons.rs index b5012bb8816..698818ced9f 100644 --- a/crates/icons/src/icons.rs +++ b/crates/icons/src/icons.rs @@ -73,6 +73,7 @@ pub enum IconName { Code, Codeberg, Command, + Compact, Control, Copilot, CopilotDisabled,