@@ -41,7 +41,7 @@ use text::Anchor;
use theme::ThemeSettings;
use ui::{
Callout, Disclosure, Divider, DividerColor, ElevationIndex, KeyBinding, PopoverMenuHandle,
- Scrollbar, ScrollbarState, Tooltip, prelude::*,
+ Scrollbar, ScrollbarState, SpinnerLabel, Tooltip, prelude::*,
};
use util::{ResultExt, size::format_file_size, time::duration_alt_display};
use workspace::{CollaboratorId, Workspace};
@@ -1205,7 +1205,7 @@ impl AcpThreadView {
div()
.py_3()
.px_2()
- .rounded_lg()
+ .rounded_md()
.shadow_md()
.bg(cx.theme().colors().editor_background)
.border_1()
@@ -1263,7 +1263,7 @@ impl AcpThreadView {
.into_any()
}
AgentThreadEntry::AssistantMessage(AssistantMessage { chunks }) => {
- let style = default_markdown_style(false, window, cx);
+ let style = default_markdown_style(false, false, window, cx);
let message_body = v_flex()
.w_full()
.gap_2p5()
@@ -1398,8 +1398,6 @@ impl AcpThreadView {
.relative()
.w_full()
.gap_1p5()
- .opacity(0.8)
- .hover(|style| style.opacity(1.))
.child(
h_flex()
.size_4()
@@ -1440,6 +1438,7 @@ impl AcpThreadView {
.child(
div()
.text_size(self.tool_name_font_size())
+ .text_color(cx.theme().colors().text_muted)
.child("Thinking"),
)
.on_click(cx.listener({
@@ -1463,9 +1462,10 @@ impl AcpThreadView {
.border_l_1()
.border_color(self.tool_card_border_color(cx))
.text_ui_sm(cx)
- .child(
- self.render_markdown(chunk, default_markdown_style(false, window, cx)),
- ),
+ .child(self.render_markdown(
+ chunk,
+ default_markdown_style(false, false, window, cx),
+ )),
)
})
.into_any_element()
@@ -1555,11 +1555,11 @@ impl AcpThreadView {
| ToolCallStatus::Completed => None,
ToolCallStatus::InProgress => Some(
Icon::new(IconName::ArrowCircle)
- .color(Color::Accent)
+ .color(Color::Muted)
.size(IconSize::Small)
.with_animation(
"running",
- Animation::new(Duration::from_secs(2)).repeat(),
+ Animation::new(Duration::from_secs(3)).repeat(),
|icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
)
.into_any(),
@@ -1572,6 +1572,10 @@ impl AcpThreadView {
),
};
+ let failed_tool_call = matches!(
+ tool_call.status,
+ ToolCallStatus::Rejected | ToolCallStatus::Canceled | ToolCallStatus::Failed
+ );
let needs_confirmation = matches!(
tool_call.status,
ToolCallStatus::WaitingForConfirmation { .. }
@@ -1652,7 +1656,7 @@ impl AcpThreadView {
v_flex()
.when(use_card_layout, |this| {
- this.rounded_lg()
+ this.rounded_md()
.border_1()
.border_color(self.tool_card_border_color(cx))
.bg(cx.theme().colors().editor_background)
@@ -1664,20 +1668,16 @@ impl AcpThreadView {
.w_full()
.gap_1()
.justify_between()
- .map(|this| {
- if use_card_layout {
- this.pl_2()
- .pr_1p5()
- .py_1()
- .rounded_t_md()
- .when(is_open, |this| {
- this.border_b_1()
- .border_color(self.tool_card_border_color(cx))
- })
- .bg(self.tool_card_header_bg(cx))
- } else {
- this.opacity(0.8).hover(|style| style.opacity(1.))
- }
+ .when(use_card_layout, |this| {
+ this.pl_2()
+ .pr_1p5()
+ .py_1()
+ .rounded_t_md()
+ .when(is_open && !failed_tool_call, |this| {
+ this.border_b_1()
+ .border_color(self.tool_card_border_color(cx))
+ })
+ .bg(self.tool_card_header_bg(cx))
})
.child(
h_flex()
@@ -1709,13 +1709,15 @@ impl AcpThreadView {
.px_1p5()
.rounded_sm()
.overflow_x_scroll()
- .opacity(0.8)
.hover(|label| {
- label.opacity(1.).bg(cx
- .theme()
- .colors()
- .element_hover
- .opacity(0.5))
+ label.bg(cx.theme().colors().element_hover.opacity(0.5))
+ })
+ .map(|this| {
+ if use_card_layout {
+ this.text_color(cx.theme().colors().text)
+ } else {
+ this.text_color(cx.theme().colors().text_muted)
+ }
})
.child(name)
.tooltip(Tooltip::text("Jump to File"))
@@ -1738,7 +1740,7 @@ impl AcpThreadView {
.overflow_x_scroll()
.child(self.render_markdown(
tool_call.label.clone(),
- default_markdown_style(false, window, cx),
+ default_markdown_style(false, true, window, cx),
)),
)
.child(gradient_overlay(gradient_color))
@@ -1804,9 +1806,9 @@ impl AcpThreadView {
.border_color(self.tool_card_border_color(cx))
.text_sm()
.text_color(cx.theme().colors().text_muted)
- .child(self.render_markdown(markdown, default_markdown_style(false, window, cx)))
+ .child(self.render_markdown(markdown, default_markdown_style(false, false, window, cx)))
.child(
- Button::new(button_id, "Collapse Output")
+ Button::new(button_id, "Collapse")
.full_width()
.style(ButtonStyle::Outlined)
.label_size(LabelSize::Small)
@@ -2131,7 +2133,7 @@ impl AcpThreadView {
.to_string()
} else {
format!(
- "Output is {} long—to avoid unexpected token usage, \
+ "Output is {} long, and to avoid unexpected token usage, \
only 16 KB was sent back to the model.",
format_file_size(output.original_content_len as u64, true),
)
@@ -2199,7 +2201,7 @@ impl AcpThreadView {
.border_1()
.when(tool_failed || command_failed, |card| card.border_dashed())
.border_color(border_color)
- .rounded_lg()
+ .rounded_md()
.overflow_hidden()
.child(
v_flex()
@@ -2553,9 +2555,10 @@ impl AcpThreadView {
.into_any(),
)
.children(description.map(|desc| {
- div().text_ui(cx).text_center().child(
- self.render_markdown(desc.clone(), default_markdown_style(false, window, cx)),
- )
+ div().text_ui(cx).text_center().child(self.render_markdown(
+ desc.clone(),
+ default_markdown_style(false, false, window, cx),
+ ))
}))
.children(
configuration_view
@@ -3379,7 +3382,7 @@ impl AcpThreadView {
"used-tokens-label",
Animation::new(Duration::from_secs(2))
.repeat()
- .with_easing(pulsating_between(0.6, 1.)),
+ .with_easing(pulsating_between(0.3, 0.8)),
|label, delta| label.alpha(delta),
)
.into_any()
@@ -4636,9 +4639,9 @@ impl Render for AcpThreadView {
ThreadStatus::Idle
| ThreadStatus::WaitingForToolConfirmation => None,
ThreadStatus::Generating => div()
- .px_5()
.py_2()
- .child(LoadingLabel::new("").size(LabelSize::Small))
+ .px(rems_from_px(22.))
+ .child(SpinnerLabel::new().size(LabelSize::Small))
.into(),
},
)
@@ -4671,7 +4674,12 @@ impl Render for AcpThreadView {
}
}
-fn default_markdown_style(buffer_font: bool, window: &Window, cx: &App) -> MarkdownStyle {
+fn default_markdown_style(
+ buffer_font: bool,
+ muted_text: bool,
+ window: &Window,
+ cx: &App,
+) -> MarkdownStyle {
let theme_settings = ThemeSettings::get_global(cx);
let colors = cx.theme().colors();
@@ -4692,20 +4700,26 @@ fn default_markdown_style(buffer_font: bool, window: &Window, cx: &App) -> Markd
TextSize::Default.rems(cx)
};
+ let text_color = if muted_text {
+ colors.text_muted
+ } else {
+ colors.text
+ };
+
text_style.refine(&TextStyleRefinement {
font_family: Some(font_family),
font_fallbacks: theme_settings.ui_font.fallbacks.clone(),
font_features: Some(theme_settings.ui_font.features.clone()),
font_size: Some(font_size.into()),
line_height: Some(line_height.into()),
- color: Some(cx.theme().colors().text),
+ color: Some(text_color),
..Default::default()
});
MarkdownStyle {
base_text_style: text_style.clone(),
syntax: cx.theme().syntax().clone(),
- selection_background_color: cx.theme().colors().element_selection_background,
+ selection_background_color: colors.element_selection_background,
code_block_overflow_x_scroll: true,
table_overflow_x_scroll: true,
heading_level_styles: Some(HeadingLevelStyles {
@@ -4791,7 +4805,7 @@ fn plan_label_markdown_style(
window: &Window,
cx: &App,
) -> MarkdownStyle {
- let default_md_style = default_markdown_style(false, window, cx);
+ let default_md_style = default_markdown_style(false, false, window, cx);
MarkdownStyle {
base_text_style: TextStyle {
@@ -4811,7 +4825,7 @@ fn plan_label_markdown_style(
}
fn terminal_command_markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
- let default_md_style = default_markdown_style(true, window, cx);
+ let default_md_style = default_markdown_style(true, false, window, cx);
MarkdownStyle {
base_text_style: TextStyle {
@@ -1,6 +1,6 @@
use std::sync::Arc;
-use gpui::{ClickEvent, CursorStyle};
+use gpui::{ClickEvent, CursorStyle, SharedString};
use crate::{Color, IconButton, IconButtonShape, IconName, IconSize, prelude::*};
@@ -14,6 +14,7 @@ pub struct Disclosure {
cursor_style: CursorStyle,
opened_icon: IconName,
closed_icon: IconName,
+ visible_on_hover: Option<SharedString>,
}
impl Disclosure {
@@ -27,6 +28,7 @@ impl Disclosure {
cursor_style: CursorStyle::PointingHand,
opened_icon: IconName::ChevronDown,
closed_icon: IconName::ChevronRight,
+ visible_on_hover: None,
}
}
@@ -73,6 +75,13 @@ impl Clickable for Disclosure {
}
}
+impl VisibleOnHover for Disclosure {
+ fn visible_on_hover(mut self, group_name: impl Into<SharedString>) -> Self {
+ self.visible_on_hover = Some(group_name.into());
+ self
+ }
+}
+
impl RenderOnce for Disclosure {
fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
IconButton::new(
@@ -87,6 +96,9 @@ impl RenderOnce for Disclosure {
.icon_size(IconSize::Small)
.disabled(self.disabled)
.toggle_state(self.selected)
+ .when_some(self.visible_on_hover.clone(), |this, group_name| {
+ this.visible_on_hover(group_name)
+ })
.when_some(self.on_toggle, move |this, on_toggle| {
this.on_click(move |event, window, cx| on_toggle(event, window, cx))
})
@@ -0,0 +1,192 @@
+use crate::prelude::*;
+use gpui::{Animation, AnimationExt, FontWeight};
+use std::time::Duration;
+
+/// Different types of spinner animations
+#[derive(Debug, Default, Clone, Copy, PartialEq)]
+pub enum SpinnerVariant {
+ #[default]
+ Dots,
+ DotsVariant,
+}
+
+/// A spinner indication, based on the label component, that loops through
+/// frames of the specified animation. It implements `LabelCommon` as well.
+///
+/// # Default Example
+///
+/// ```
+/// use ui::{SpinnerLabel};
+///
+/// SpinnerLabel::new();
+/// ```
+///
+/// # Variant Example
+///
+/// ```
+/// use ui::{SpinnerLabel};
+///
+/// SpinnerLabel::dots_variant();
+/// ```
+#[derive(IntoElement, RegisterComponent)]
+pub struct SpinnerLabel {
+ base: Label,
+ variant: SpinnerVariant,
+ frames: Vec<&'static str>,
+ duration: Duration,
+}
+
+impl SpinnerVariant {
+ fn frames(&self) -> Vec<&'static str> {
+ match self {
+ SpinnerVariant::Dots => vec!["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"],
+ SpinnerVariant::DotsVariant => vec!["⣼", "⣹", "⢻", "⠿", "⡟", "⣏", "⣧", "⣶"],
+ }
+ }
+
+ fn duration(&self) -> Duration {
+ match self {
+ SpinnerVariant::Dots => Duration::from_millis(1000),
+ SpinnerVariant::DotsVariant => Duration::from_millis(1000),
+ }
+ }
+
+ fn animation_id(&self) -> &'static str {
+ match self {
+ SpinnerVariant::Dots => "spinner_label_dots",
+ SpinnerVariant::DotsVariant => "spinner_label_dots_variant",
+ }
+ }
+}
+
+impl SpinnerLabel {
+ pub fn new() -> Self {
+ Self::with_variant(SpinnerVariant::default())
+ }
+
+ pub fn with_variant(variant: SpinnerVariant) -> Self {
+ let frames = variant.frames();
+ let duration = variant.duration();
+
+ SpinnerLabel {
+ base: Label::new(frames[0]),
+ variant,
+ frames,
+ duration,
+ }
+ }
+
+ pub fn dots() -> Self {
+ Self::with_variant(SpinnerVariant::Dots)
+ }
+
+ pub fn dots_variant() -> Self {
+ Self::with_variant(SpinnerVariant::DotsVariant)
+ }
+}
+
+impl LabelCommon for SpinnerLabel {
+ fn size(mut self, size: LabelSize) -> Self {
+ self.base = self.base.size(size);
+ self
+ }
+
+ fn weight(mut self, weight: FontWeight) -> Self {
+ self.base = self.base.weight(weight);
+ self
+ }
+
+ fn line_height_style(mut self, line_height_style: LineHeightStyle) -> Self {
+ self.base = self.base.line_height_style(line_height_style);
+ self
+ }
+
+ fn color(mut self, color: Color) -> Self {
+ self.base = self.base.color(color);
+ self
+ }
+
+ fn strikethrough(mut self) -> Self {
+ self.base = self.base.strikethrough();
+ self
+ }
+
+ fn italic(mut self) -> Self {
+ self.base = self.base.italic();
+ self
+ }
+
+ fn alpha(mut self, alpha: f32) -> Self {
+ self.base = self.base.alpha(alpha);
+ self
+ }
+
+ fn underline(mut self) -> Self {
+ self.base = self.base.underline();
+ self
+ }
+
+ fn truncate(mut self) -> Self {
+ self.base = self.base.truncate();
+ self
+ }
+
+ fn single_line(mut self) -> Self {
+ self.base = self.base.single_line();
+ self
+ }
+
+ fn buffer_font(mut self, cx: &App) -> Self {
+ self.base = self.base.buffer_font(cx);
+ self
+ }
+
+ fn inline_code(mut self, cx: &App) -> Self {
+ self.base = self.base.inline_code(cx);
+ self
+ }
+}
+
+impl RenderOnce for SpinnerLabel {
+ fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
+ let frames = self.frames.clone();
+ let duration = self.duration;
+
+ self.base.color(Color::Muted).with_animation(
+ self.variant.animation_id(),
+ Animation::new(duration).repeat(),
+ move |mut label, delta| {
+ let frame_index = (delta * frames.len() as f32) as usize % frames.len();
+
+ label.set_text(frames[frame_index]);
+ label
+ },
+ )
+ }
+}
+
+impl Component for SpinnerLabel {
+ fn scope() -> ComponentScope {
+ ComponentScope::Loading
+ }
+
+ fn name() -> &'static str {
+ "Spinner Label"
+ }
+
+ fn sort_name() -> &'static str {
+ "Spinner Label"
+ }
+
+ fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
+ let examples = vec![
+ single_example("Default", SpinnerLabel::new().into_any_element()),
+ single_example(
+ "Dots Variant",
+ SpinnerLabel::dots_variant().into_any_element(),
+ ),
+ ];
+
+ Some(example_group(examples).vertical().into_any_element())
+ }
+}