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,