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.p_1()
 60                            .bg(cx.theme().colors().element_background.opacity(0.2))
 61                    })
 62                    .child(
 63                        h_flex()
 64                            .w_full()
 65                            .when(self.use_card_layout, |this| this.px_1())
 66                            .hover(|s| s.bg(cx.theme().colors().element_hover))
 67                            .gap_1p5()
 68                            .rounded_xs()
 69                            .child(
 70                                Icon::new(self.icon)
 71                                    .size(IconSize::Small)
 72                                    .color(Color::Muted),
 73                            )
 74                            .child(
 75                                Label::new(self.title)
 76                                    .size(LabelSize::Small)
 77                                    .color(Color::Muted),
 78                            ),
 79                    )
 80                    .when_some(self.actions_slot, |this, action| this.child(action)),
 81            )
 82            .when_some(self.content, |this, content| {
 83                this.child(
 84                    div()
 85                        .map(|this| {
 86                            if self.use_card_layout {
 87                                this.p_2()
 88                                    .border_t_1()
 89                                    .border_color(cx.theme().colors().border)
 90                                    .bg(cx.theme().colors().editor_background)
 91                            } else {
 92                                this.pl_4()
 93                                    .ml_1p5()
 94                                    .border_l_1()
 95                                    .border_color(cx.theme().colors().border)
 96                            }
 97                        })
 98                        .child(content),
 99                )
100            })
101    }
102}
103
104impl Component for ToolCall {
105    fn scope() -> ComponentScope {
106        ComponentScope::Agent
107    }
108
109    fn preview(_window: &mut Window, cx: &mut App) -> Option<AnyElement> {
110        let container = || {
111            v_flex()
112                .p_2()
113                .w_128()
114                .border_1()
115                .border_color(cx.theme().colors().border_variant)
116                .bg(cx.theme().colors().panel_background)
117        };
118
119        let muted_icon_button = |id: &'static str, icon: IconName| {
120            IconButton::new(id, icon)
121                .icon_size(IconSize::Small)
122                .icon_color(Color::Muted)
123        };
124
125        let examples = vec![
126            single_example(
127                "Non-card (header only)",
128                container()
129                    .child(
130                        ToolCall::new("Search repository")
131                            .icon(IconName::ToolSearch)
132                            .actions_slot(muted_icon_button(
133                                "toolcall-noncard-expand",
134                                IconName::ChevronDown,
135                            )),
136                    )
137                    .into_any_element(),
138            ),
139            single_example(
140                "Non-card + content",
141                container()
142                    .child(
143                        ToolCall::new("Edit file: src/main.rs")
144                            .icon(IconName::File)
145                            .content(
146                                Label::new("Tool output here — markdown, list, etc.")
147                                    .size(LabelSize::Small)
148                                    .color(Color::Muted),
149                            ),
150                    )
151                    .into_any_element(),
152            ),
153            single_example(
154                "Card layout + actions",
155                container()
156                    .child(
157                        ToolCall::new("Run Command")
158                            .icon(IconName::ToolTerminal)
159                            .use_card_layout(true)
160                            .actions_slot(muted_icon_button(
161                                "toolcall-card-expand",
162                                IconName::ChevronDown,
163                            ))
164                            .content(
165                                Label::new("git status")
166                                    .size(LabelSize::Small)
167                                    .buffer_font(cx),
168                            ),
169                    )
170                    .into_any_element(),
171            ),
172        ];
173
174        Some(example_group(examples).vertical().into_any_element())
175    }
176}