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}