Skip to main content

ui/components/ai/
agent_thread_toolbar.rs

1use gpui::SharedString;
2
3use crate::prelude::*;
4
5/// Toolbar shown atop an agent chat thread: a title, an optional right-hand
6/// slot (typically an [`AgentModelSelector`](super::AgentModelSelector)), and
7/// optional usage/cost figures.
8///
9/// `usage` is a list of `(label, value)` pairs (e.g. `("Tokens", "12.4k")`,
10/// `("Cost", "$0.08")`) supplied by the caller. This is intentionally decoupled
11/// from any `boltz-acpx` usage/cost type — `ui` must not depend on `acpx`.
12#[derive(IntoElement, RegisterComponent)]
13pub struct AgentThreadToolbar {
14    id: ElementId,
15    title: SharedString,
16    right_slot: Option<AnyElement>,
17    usage: Option<Vec<(String, String)>>,
18}
19
20impl AgentThreadToolbar {
21    pub fn new(id: impl Into<ElementId>, title: impl Into<SharedString>) -> Self {
22        Self {
23            id: id.into(),
24            title: title.into(),
25            right_slot: None,
26            usage: None,
27        }
28    }
29
30    /// Right-hand slot, typically a model selector.
31    pub fn right_slot(mut self, element: impl IntoElement) -> Self {
32        self.right_slot = Some(element.into_any_element());
33        self
34    }
35
36    /// Usage/cost figures as `(label, value)` pairs, e.g.
37    /// `[("Tokens", "12.4k"), ("Cost", "$0.08")]`.
38    pub fn usage(mut self, usage: Vec<(String, String)>) -> Self {
39        self.usage = Some(usage);
40        self
41    }
42}
43
44impl RenderOnce for AgentThreadToolbar {
45    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
46        h_flex()
47            .id(self.id)
48            .w_full()
49            .justify_between()
50            .items_center()
51            .gap_2()
52            .px_2()
53            .py_1p5()
54            .border_b_1()
55            .border_color(cx.theme().colors().border_variant)
56            .child(
57                h_flex()
58                    .gap_3()
59                    .min_w_0()
60                    .child(Label::new(self.title).truncate())
61                    .when_some(self.usage, |this, usage| {
62                        this.children(usage.into_iter().map(|(label, value)| {
63                            h_flex()
64                                .gap_1()
65                                .flex_shrink_0()
66                                .child(Label::new(label).size(LabelSize::Small).color(Color::Muted))
67                                .child(Label::new(value).size(LabelSize::Small))
68                        }))
69                    }),
70            )
71            .when_some(self.right_slot, |this, slot| this.child(slot))
72    }
73}
74
75impl Component for AgentThreadToolbar {
76    fn scope() -> ComponentScope {
77        ComponentScope::Agent
78    }
79
80    fn description() -> Option<&'static str> {
81        Some(
82            "The toolbar shown atop an agent chat thread: title, a right-hand \
83             slot for a model selector, and optional usage/cost figures.",
84        )
85    }
86
87    fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
88        Some(
89            v_flex()
90                .w_96()
91                .gap_4()
92                .child(single_example(
93                    "Title only",
94                    AgentThreadToolbar::new("toolbar-1", "New Thread").into_any_element(),
95                ))
96                .child(single_example(
97                    "With usage",
98                    AgentThreadToolbar::new("toolbar-2", "Refactor auth module")
99                        .usage(vec![
100                            ("Tokens".to_string(), "12.4k".to_string()),
101                            ("Cost".to_string(), "$0.08".to_string()),
102                        ])
103                        .into_any_element(),
104                ))
105                .child(single_example(
106                    "With right slot",
107                    AgentThreadToolbar::new("toolbar-3", "Fix flaky test")
108                        .right_slot(
109                            Button::new("toolbar-model", "claude-sonnet-4.6")
110                                .style(ButtonStyle::Subtle),
111                        )
112                        .usage(vec![("Cost".to_string(), "$0.02".to_string())])
113                        .into_any_element(),
114                ))
115                .into_any_element(),
116        )
117    }
118}