thread_view: Add refinements to compaction UI design (#58926)

Here are the things I tried to tackle on this PR:

- Make the compaction call stand out a bit more from other regular tool
calls
- Make text size of the compaction's markdown output be consistent with
the thread body text
- Don't show regular thread loading spinner while compaction is in
progress, given that has its own spinner
- Add a different icon than scissors for compaction and add it to the
autocomplete menu so its consistent w/ skills items

<img width="600" alt="Screenshot 2026-06-09 at 10  57@2x"
src="https://github.com/user-attachments/assets/5c3d25af-aa49-42e1-b6b6-c3f95c664456"
/>

Release Notes:

- N/A
This commit is contained in:
Danilo Leal 2026-06-09 11:38:34 -03:00 committed by GitHub
parent f164afda46
commit 0e9e8d0e68
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 117 additions and 86 deletions

6
assets/icons/compact.svg Normal file
View file

@ -0,0 +1,6 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10 10L14 14M13.2 10H10V13.2" stroke="#DCE0E5" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M2.8 10H6V13.2M6 10L2 14" stroke="#DCE0E5" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M13.2 6H10V2.8M10 6L14 2" stroke="#DCE0E5" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M2.8 6H6V2.8M6 6L2 2" stroke="#DCE0E5" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 583 B

View file

@ -317,6 +317,12 @@ pub struct ContextCompaction {
pub summary: Option<Entity<Markdown>>,
}
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 {

View file

@ -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
/// (`/<server>.<name>`) to stay unambiguous in the slash-command popup: names

View file

@ -1500,6 +1500,11 @@ impl<T: PromptCompletionProviderDelegate> 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<T: PromptCompletionProviderDelegate> 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.

View file

@ -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);
});
}
}

View file

@ -3598,98 +3598,99 @@ impl ThreadView {
window: &Window,
cx: &Context<Self>,
) -> 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<Self>) {
@ -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();

View file

@ -73,6 +73,7 @@ pub enum IconName {
Code,
Codeberg,
Command,
Compact,
Control,
Copilot,
CopilotDisabled,