From 9e88f3f33cf4c2fd7d2594299aa2d8e5c9476ff9 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Mon, 29 Dec 2025 08:27:43 -0300 Subject: [PATCH] agent_ui: Fix issues with mention crease (#45683) This PR introduces the `MentionCrease` component, aimed at solving two issues with mention creases in the agent panel: - Previously, the mention crease was using a button with a regular size, which is bigger than the default buffer font line height. That made the crease look clipped and also overlapping with one another when in a multiple line scenario where the creases would be on top of each other. `MentionCrease` uses the window line height value to set the button height, with a small one pixel vertical padding just for a bit of spacing. - Previously, given the crease used a `Label`, its font size wouldn't scale if you changed the `agent_buffer_font_size` setting. Now, `MentionCrease` uses that font size value, which makes regular text and its text grow together as you'd expect. Release Notes: - agent: Fix a bug where mention creases didn't scale with `agent_buffer_font_size` and got clipped/jumbled when rendered one above the other. --- crates/agent_ui/src/mention_set.rs | 73 ++++------------- crates/agent_ui/src/ui.rs | 2 + crates/agent_ui/src/ui/mention_crease.rs | 100 +++++++++++++++++++++++ 3 files changed, 117 insertions(+), 58 deletions(-) create mode 100644 crates/agent_ui/src/ui/mention_crease.rs diff --git a/crates/agent_ui/src/mention_set.rs b/crates/agent_ui/src/mention_set.rs index eee28bbfb2d36ce8f41e64cafd2e8f24b504f97f..ea4b82e3037393ef1c73ea5608d61b0f0e5de054 100644 --- a/crates/agent_ui/src/mention_set.rs +++ b/crates/agent_ui/src/mention_set.rs @@ -12,8 +12,8 @@ use editor::{ }; use futures::{AsyncReadExt as _, FutureExt as _, future::Shared}; use gpui::{ - Animation, AnimationExt as _, AppContext, ClipboardEntry, Context, Empty, Entity, EntityId, - Image, ImageFormat, Img, SharedString, Task, WeakEntity, pulsating_between, + AppContext, ClipboardEntry, Context, Empty, Entity, EntityId, Image, ImageFormat, Img, + SharedString, Task, WeakEntity, }; use http_client::{AsyncBody, HttpClientWithUrl}; use itertools::Either; @@ -32,13 +32,14 @@ use std::{ path::{Path, PathBuf}, rc::Rc, sync::Arc, - time::Duration, }; use text::OffsetRangeExt; -use ui::{ButtonLike, Disclosure, TintColor, Toggleable, prelude::*}; +use ui::{Disclosure, Toggleable, prelude::*}; use util::{ResultExt, debug_panic, rel_path::RelPath}; use workspace::{Workspace, notifications::NotifyResultExt as _}; +use crate::ui::MentionCrease; + pub type MentionTask = Shared>>; #[derive(Debug, Clone, Eq, PartialEq)] @@ -754,25 +755,8 @@ fn render_fold_icon_button( .update(cx, |editor, cx| editor.is_range_selected(&fold_range, cx)) .unwrap_or_default(); - ButtonLike::new(fold_id) - .style(ButtonStyle::Filled) - .selected_style(ButtonStyle::Tinted(TintColor::Accent)) - .toggle_state(is_in_text_selection) - .child( - h_flex() - .gap_1() - .child( - Icon::from_path(icon_path.clone()) - .size(IconSize::XSmall) - .color(Color::Muted), - ) - .child( - Label::new(label.clone()) - .size(LabelSize::Small) - .buffer_font(cx) - .single_line(), - ), - ) + MentionCrease::new(fold_id, icon_path.clone(), label.clone()) + .is_toggled(is_in_text_selection) .into_any_element() } }) @@ -947,12 +931,14 @@ impl Render for LoadingContext { .editor .update(cx, |editor, cx| editor.is_range_selected(&self.range, cx)) .unwrap_or_default(); - ButtonLike::new(("loading-context", self.id)) - .style(ButtonStyle::Filled) - .selected_style(ButtonStyle::Tinted(TintColor::Accent)) - .toggle_state(is_in_text_selection) - .when_some(self.image.clone(), |el, image_task| { - el.hoverable_tooltip(move |_, cx| { + + let id = ElementId::from(("loading_context", self.id)); + + MentionCrease::new(id, self.icon.clone(), self.label.clone()) + .is_toggled(is_in_text_selection) + .is_loading(self.loading.is_some()) + .when_some(self.image.clone(), |this, image_task| { + this.image_preview(move |_, cx| { let image = image_task.peek().cloned().transpose().ok().flatten(); let image_task = image_task.clone(); cx.new::(|cx| ImageHover { @@ -971,35 +957,6 @@ impl Render for LoadingContext { .into() }) }) - .child( - h_flex() - .gap_1() - .child( - Icon::from_path(self.icon.clone()) - .size(IconSize::XSmall) - .color(Color::Muted), - ) - .child( - Label::new(self.label.clone()) - .size(LabelSize::Small) - .buffer_font(cx) - .single_line(), - ) - .map(|el| { - if self.loading.is_some() { - el.with_animation( - "loading-context-crease", - Animation::new(Duration::from_secs(2)) - .repeat() - .with_easing(pulsating_between(0.4, 0.8)), - |label, delta| label.opacity(delta), - ) - .into_any() - } else { - el.into_any() - } - }), - ) } } diff --git a/crates/agent_ui/src/ui.rs b/crates/agent_ui/src/ui.rs index b484fdb6c6c480f1cffe78eea7a51f635d3906a1..376c50ef483d3cea37ac7d841093d6e6b7a19d07 100644 --- a/crates/agent_ui/src/ui.rs +++ b/crates/agent_ui/src/ui.rs @@ -4,6 +4,7 @@ mod burn_mode_tooltip; mod claude_code_onboarding_modal; mod end_trial_upsell; mod hold_for_default; +mod mention_crease; mod model_selector_components; mod onboarding_modal; mod usage_callout; @@ -14,6 +15,7 @@ pub use burn_mode_tooltip::*; pub use claude_code_onboarding_modal::*; pub use end_trial_upsell::*; pub use hold_for_default::*; +pub use mention_crease::*; pub use model_selector_components::*; pub use onboarding_modal::*; pub use usage_callout::*; diff --git a/crates/agent_ui/src/ui/mention_crease.rs b/crates/agent_ui/src/ui/mention_crease.rs new file mode 100644 index 0000000000000000000000000000000000000000..013d6659493bd0930d132a662d374f60ca47961f --- /dev/null +++ b/crates/agent_ui/src/ui/mention_crease.rs @@ -0,0 +1,100 @@ +use std::time::Duration; + +use gpui::{Animation, AnimationExt, AnyView, IntoElement, Window, pulsating_between}; +use settings::Settings; +use theme::ThemeSettings; +use ui::{ButtonLike, TintColor, prelude::*}; + +#[derive(IntoElement)] +pub struct MentionCrease { + id: ElementId, + icon: SharedString, + label: SharedString, + is_toggled: bool, + is_loading: bool, + image_preview: Option AnyView + 'static>>, +} + +impl MentionCrease { + pub fn new( + id: impl Into, + icon: impl Into, + label: impl Into, + ) -> Self { + Self { + id: id.into(), + icon: icon.into(), + label: label.into(), + is_toggled: false, + is_loading: false, + image_preview: None, + } + } + + pub fn is_toggled(mut self, is_toggled: bool) -> Self { + self.is_toggled = is_toggled; + self + } + + pub fn is_loading(mut self, is_loading: bool) -> Self { + self.is_loading = is_loading; + self + } + + pub fn image_preview( + mut self, + builder: impl Fn(&mut Window, &mut App) -> AnyView + 'static, + ) -> Self { + self.image_preview = Some(Box::new(builder)); + self + } +} + +impl RenderOnce for MentionCrease { + fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { + let settings = ThemeSettings::get_global(cx); + let font_size = settings.agent_buffer_font_size(cx); + let buffer_font = settings.buffer_font.clone(); + + let button_height = DefiniteLength::Absolute(AbsoluteLength::Pixels( + px(window.line_height().into()) - px(1.), + )); + + ButtonLike::new(self.id) + .style(ButtonStyle::Outlined) + .size(ButtonSize::Compact) + .height(button_height) + .selected_style(ButtonStyle::Tinted(TintColor::Accent)) + .toggle_state(self.is_toggled) + .when_some(self.image_preview, |this, image_preview| { + this.hoverable_tooltip(image_preview) + }) + .child( + h_flex() + .pb_px() + .gap_1() + .font(buffer_font) + .text_size(font_size) + .child( + Icon::from_path(self.icon.clone()) + .size(IconSize::XSmall) + .color(Color::Muted), + ) + .child(self.label.clone()) + .map(|this| { + if self.is_loading { + this.with_animation( + "loading-context-crease", + Animation::new(Duration::from_secs(2)) + .repeat() + .with_easing(pulsating_between(0.4, 0.8)), + |label, delta| label.opacity(delta), + ) + .into_any() + } else { + this.into_any() + } + }), + ) + } +}