tool_call_card_header.rs

  1use gpui::{Animation, AnimationExt, AnyElement, App, IntoElement, pulsating_between};
  2use std::time::Duration;
  3use ui::{Tooltip, prelude::*};
  4
  5/// A reusable header component for tool call cards.
  6#[derive(IntoElement)]
  7pub struct ToolCallCardHeader {
  8    icon: IconName,
  9    primary_text: SharedString,
 10    secondary_text: Option<SharedString>,
 11    code_path: Option<SharedString>,
 12    disclosure_slot: Option<AnyElement>,
 13    is_loading: bool,
 14    error: Option<String>,
 15}
 16
 17impl ToolCallCardHeader {
 18    pub fn new(icon: IconName, primary_text: impl Into<SharedString>) -> Self {
 19        Self {
 20            icon,
 21            primary_text: primary_text.into(),
 22            secondary_text: None,
 23            code_path: None,
 24            disclosure_slot: None,
 25            is_loading: false,
 26            error: None,
 27        }
 28    }
 29
 30    pub fn with_secondary_text(mut self, text: impl Into<SharedString>) -> Self {
 31        self.secondary_text = Some(text.into());
 32        self
 33    }
 34
 35    pub fn with_code_path(mut self, text: impl Into<SharedString>) -> Self {
 36        self.code_path = Some(text.into());
 37        self
 38    }
 39
 40    pub fn disclosure_slot(mut self, element: impl IntoElement) -> Self {
 41        self.disclosure_slot = Some(element.into_any_element());
 42        self
 43    }
 44
 45    pub fn loading(mut self) -> Self {
 46        self.is_loading = true;
 47        self
 48    }
 49
 50    pub fn with_error(mut self, error: impl Into<String>) -> Self {
 51        self.error = Some(error.into());
 52        self
 53    }
 54}
 55
 56impl RenderOnce for ToolCallCardHeader {
 57    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
 58        let font_size = rems(0.8125);
 59        let line_height = window.line_height();
 60
 61        let secondary_text = self.secondary_text;
 62        let code_path = self.code_path;
 63
 64        let bullet_divider = || {
 65            div()
 66                .size(px(3.))
 67                .rounded_full()
 68                .bg(cx.theme().colors().text)
 69        };
 70
 71        h_flex()
 72            .id("tool-label-container")
 73            .gap_2()
 74            .max_w_full()
 75            .overflow_x_scroll()
 76            .opacity(0.8)
 77            .child(
 78                h_flex()
 79                    .h(line_height)
 80                    .gap_1p5()
 81                    .text_size(font_size)
 82                    .child(
 83                        h_flex().h(line_height).justify_center().child(
 84                            Icon::new(self.icon)
 85                                .size(IconSize::Small)
 86                                .color(Color::Muted),
 87                        ),
 88                    )
 89                    .map(|this| {
 90                        if let Some(error) = &self.error {
 91                            this.child(format!("{} failed", self.primary_text)).child(
 92                                IconButton::new("error_info", IconName::Warning)
 93                                    .shape(ui::IconButtonShape::Square)
 94                                    .icon_size(IconSize::XSmall)
 95                                    .icon_color(Color::Warning)
 96                                    .tooltip(Tooltip::text(error.clone())),
 97                            )
 98                        } else {
 99                            this.child(self.primary_text.clone())
100                        }
101                    })
102                    .when_some(secondary_text, |this, secondary_text| {
103                        this.child(bullet_divider())
104                            .child(div().text_size(font_size).child(secondary_text))
105                    })
106                    .when_some(code_path, |this, code_path| {
107                        this.child(bullet_divider())
108                            .child(Label::new(code_path).size(LabelSize::Small).inline_code(cx))
109                    })
110                    .with_animation(
111                        "loading-label",
112                        Animation::new(Duration::from_secs(2))
113                            .repeat()
114                            .with_easing(pulsating_between(0.6, 1.)),
115                        move |this, delta| {
116                            if self.is_loading {
117                                this.opacity(delta)
118                            } else {
119                                this
120                            }
121                        },
122                    ),
123            )
124            .when_some(self.disclosure_slot, |container, disclosure_slot| {
125                container
126                    .group("disclosure")
127                    .justify_between()
128                    .child(div().visible_on_hover("disclosure").child(disclosure_slot))
129            })
130    }
131}