mirror of
https://github.com/zed-industries/zed.git
synced 2026-07-10 00:13:29 +00:00
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:
parent
f164afda46
commit
0e9e8d0e68
7 changed files with 117 additions and 86 deletions
6
assets/icons/compact.svg
Normal file
6
assets/icons/compact.svg
Normal 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 |
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -73,6 +73,7 @@ pub enum IconName {
|
|||
Code,
|
||||
Codeberg,
|
||||
Command,
|
||||
Compact,
|
||||
Control,
|
||||
Copilot,
|
||||
CopilotDisabled,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue