Skip to main content

ui/components/ai/
profile_selector.rs

1use std::sync::Arc;
2
3use gpui::SharedString;
4
5use crate::prelude::*;
6use crate::{ContextMenu, DropdownMenu, DropdownStyle};
7
8/// A single selectable profile/config option: a stable `key` (passed to
9/// `on_select`) and a human-readable `label` shown in the menu.
10#[derive(Debug, Clone, PartialEq, Eq)]
11pub struct ProfileOption {
12    pub key: SharedString,
13    pub label: SharedString,
14}
15
16impl ProfileOption {
17    pub fn new(key: impl Into<SharedString>, label: impl Into<SharedString>) -> Self {
18        Self {
19            key: key.into(),
20            label: label.into(),
21        }
22    }
23}
24
25impl From<(String, String)> for ProfileOption {
26    fn from((key, label): (String, String)) -> Self {
27        Self::new(key, label)
28    }
29}
30
31impl From<String> for ProfileOption {
32    fn from(key: String) -> Self {
33        let label = key.clone();
34        Self::new(key, label)
35    }
36}
37
38impl From<&str> for ProfileOption {
39    fn from(key: &str) -> Self {
40        Self::new(key, key)
41    }
42}
43
44/// A dropdown for picking an agent's active profile/config option out of a
45/// caller-supplied list of keys (optionally paired with display labels).
46///
47/// Pure builder: the caller supplies the current key and the full option
48/// list (no fetching happens here), and is notified via `on_select` with the
49/// chosen option's `key` when the user picks a different one.
50#[derive(IntoElement, RegisterComponent)]
51pub struct ProfileSelector {
52    id: ElementId,
53    current_key: Option<SharedString>,
54    options: Vec<ProfileOption>,
55    on_select: Option<Arc<dyn Fn(SharedString, &mut Window, &mut App) + 'static>>,
56}
57
58impl ProfileSelector {
59    pub fn new(
60        id: impl Into<ElementId>,
61        current_key: Option<&str>,
62        options: impl IntoIterator<Item = impl Into<ProfileOption>>,
63    ) -> Self {
64        Self {
65            id: id.into(),
66            current_key: current_key.map(SharedString::from),
67            options: options.into_iter().map(Into::into).collect(),
68            on_select: None,
69        }
70    }
71
72    pub fn on_select(
73        mut self,
74        handler: impl Fn(SharedString, &mut Window, &mut App) + 'static,
75    ) -> Self {
76        self.on_select = Some(Arc::new(handler));
77        self
78    }
79}
80
81impl RenderOnce for ProfileSelector {
82    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
83        let current_label = self
84            .current_key
85            .as_ref()
86            .and_then(|key| self.options.iter().find(|option| &option.key == key))
87            .map(|option| option.label.clone());
88        let label = current_label.unwrap_or_else(|| "Select profile".into());
89        let current_key = self.current_key.clone();
90        let on_select = self.on_select.clone();
91        let options = self.options.clone();
92
93        let menu = ContextMenu::build(window, cx, move |mut menu, _window, _cx| {
94            for option in &options {
95                let is_current = current_key.as_ref() == Some(&option.key);
96                let key = option.key.clone();
97                let on_select = on_select.clone();
98                menu = menu.toggleable_entry(
99                    option.label.clone(),
100                    is_current,
101                    IconPosition::End,
102                    None,
103                    move |window, cx| {
104                        if let Some(on_select) = &on_select {
105                            on_select(key.clone(), window, cx);
106                        }
107                    },
108                );
109            }
110            menu
111        });
112
113        DropdownMenu::new(self.id, label, menu).style(DropdownStyle::Subtle)
114    }
115}
116
117impl Component for ProfileSelector {
118    fn scope() -> ComponentScope {
119        ComponentScope::Agent
120    }
121
122    fn name() -> &'static str {
123        "ProfileSelector"
124    }
125
126    fn description() -> Option<&'static str> {
127        Some(
128            "A dropdown for picking an agent's active profile/config option \
129             out of a caller-supplied list of keys, optionally paired with \
130             display labels. No data fetching happens here.",
131        )
132    }
133
134    fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
135        let plain_keys = vec!["default", "careful", "yolo"];
136        let labeled_keys = vec![
137            ("default".to_string(), "Default".to_string()),
138            (
139                "careful".to_string(),
140                "Careful (read-only tools)".to_string(),
141            ),
142            ("yolo".to_string(), "Yolo (all tools)".to_string()),
143        ];
144
145        Some(
146            v_flex()
147                .gap_4()
148                .child(single_example(
149                    "Plain keys",
150                    ProfileSelector::new("profile-selector-1", Some("default"), plain_keys)
151                        .into_any_element(),
152                ))
153                .child(single_example(
154                    "Key + label pairs",
155                    ProfileSelector::new("profile-selector-2", Some("careful"), labeled_keys)
156                        .into_any_element(),
157                ))
158                .child(single_example(
159                    "No selection",
160                    ProfileSelector::new(
161                        "profile-selector-3",
162                        None,
163                        vec!["default", "careful", "yolo"],
164                    )
165                    .into_any_element(),
166                ))
167                .into_any_element(),
168        )
169    }
170}