tool_call.rs

  1use crate::prelude::*;
  2use gpui::{AnyElement, IntoElement, ParentElement, SharedString};
  3
  4#[derive(IntoElement, RegisterComponent)]
  5pub struct ToolCall {
  6    icon: IconName,
  7    title: SharedString,
  8    actions_slot: Option<AnyElement>,
  9    content: Option<AnyElement>,
 10    use_card_layout: bool,
 11}
 12
 13impl ToolCall {
 14    pub fn new(title: impl Into<SharedString>) -> Self {
 15        Self {
 16            icon: IconName::ToolSearch,
 17            title: title.into(),
 18            actions_slot: None,
 19            use_card_layout: false,
 20            content: None,
 21        }
 22    }
 23
 24    pub fn icon(mut self, icon: IconName) -> Self {
 25        self.icon = icon;
 26        self
 27    }
 28
 29    pub fn actions_slot(mut self, action: impl IntoElement) -> Self {
 30        self.actions_slot = Some(action.into_any_element());
 31        self
 32    }
 33
 34    pub fn content(mut self, content: impl IntoElement) -> Self {
 35        self.content = Some(content.into_any_element());
 36        self
 37    }
 38
 39    pub fn use_card_layout(mut self, use_card_layout: bool) -> Self {
 40        self.use_card_layout = use_card_layout;
 41        self
 42    }
 43}
 44
 45impl RenderOnce for ToolCall {
 46    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
 47        v_flex()
 48            .when(self.use_card_layout, |this| {
 49                this.border_1()
 50                    .border_color(cx.theme().colors().border)
 51                    .rounded_md()
 52                    .overflow_hidden()
 53            })
 54            .child(
 55                h_flex()
 56                    .gap_1()
 57                    .justify_between()
 58                    .when(self.use_card_layout, |this| {
 59                        this.px_2()
 60                            .py_1()
 61                            .bg(cx.theme().colors().element_background.opacity(0.2))
 62                    })
 63                    .child(
 64                        h_flex()
 65                            .hover(|s| s.bg(cx.theme().colors().element_hover.opacity(0.5)))
 66                            .gap_1p5()
 67                            .rounded_xs()
 68                            .child(
 69                                Icon::new(self.icon)
 70                                    .size(IconSize::Small)
 71                                    .color(Color::Muted),
 72                            )
 73                            .child(
 74                                Label::new(self.title)
 75                                    .size(LabelSize::Small)
 76                                    .color(Color::Muted),
 77                            ),
 78                    )
 79                    .when_some(self.actions_slot, |this, action| this.child(action)),
 80            )
 81            .when_some(self.content, |this, content| {
 82                this.child(
 83                    div()
 84                        .when(self.use_card_layout, |this| {
 85                            this.p_2()
 86                                .border_t_1()
 87                                .border_color(cx.theme().colors().border)
 88                                .bg(cx.theme().colors().editor_background)
 89                        })
 90                        .child(content),
 91                )
 92            })
 93    }
 94}
 95
 96impl Component for ToolCall {
 97    fn scope() -> ComponentScope {
 98        ComponentScope::Agent
 99    }
100
101    fn preview(_window: &mut Window, cx: &mut App) -> Option<AnyElement> {
102        let container = || {
103            v_flex()
104                .p_2()
105                .w_128()
106                .border_1()
107                .border_color(cx.theme().colors().border_variant)
108                .bg(cx.theme().colors().panel_background)
109        };
110
111        let muted_icon_button = |id: &'static str, icon: IconName| {
112            IconButton::new(id, icon)
113                .icon_size(IconSize::Small)
114                .icon_color(Color::Muted)
115        };
116
117        let examples = vec![
118            single_example(
119                "Non-card (header only)",
120                container()
121                    .child(
122                        ToolCall::new("Search repository")
123                            .icon(IconName::ToolSearch)
124                            .actions_slot(muted_icon_button(
125                                "toolcall-noncard-expand",
126                                IconName::ChevronDown,
127                            )),
128                    )
129                    .into_any_element(),
130            ),
131            single_example(
132                "Non-card + content",
133                container()
134                    .child(
135                        ToolCall::new("Edit file: src/main.rs")
136                            .icon(IconName::File)
137                            .content(
138                                Label::new("Tool output here — markdown, list, etc.")
139                                    .size(LabelSize::Small)
140                                    .color(Color::Muted),
141                            ),
142                    )
143                    .into_any_element(),
144            ),
145            single_example(
146                "Card layout + actions",
147                container()
148                    .child(
149                        ToolCall::new("Run Command")
150                            .icon(IconName::ToolTerminal)
151                            .use_card_layout(true)
152                            .actions_slot(muted_icon_button(
153                                "toolcall-card-expand",
154                                IconName::ChevronDown,
155                            ))
156                            .content(
157                                Label::new("git status")
158                                    .size(LabelSize::Small)
159                                    .buffer_font(cx),
160                            ),
161                    )
162                    .into_any_element(),
163            ),
164        ];
165
166        Some(example_group(examples).vertical().into_any_element())
167    }
168}