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}