Skip to main content

ui/components/ai/
agent_model_selector.rs

1use std::sync::Arc;
2
3use gpui::SharedString;
4
5use crate::prelude::*;
6use crate::{ContextMenu, DropdownMenu, DropdownStyle};
7
8/// A dropdown for picking the active model for an agent thread.
9///
10/// This is a pure builder: the caller supplies the current selection and the
11/// full list of selectable model ids (no fetching happens here), and is
12/// notified via `on_select` when the user picks a different one.
13#[derive(IntoElement, RegisterComponent)]
14pub struct AgentModelSelector {
15    id: ElementId,
16    current_model_id: Option<SharedString>,
17    available_model_ids: Vec<SharedString>,
18    on_select: Option<Arc<dyn Fn(SharedString, &mut Window, &mut App) + 'static>>,
19}
20
21impl AgentModelSelector {
22    pub fn new(
23        id: impl Into<ElementId>,
24        current_model_id: Option<&str>,
25        available_model_ids: &[String],
26    ) -> Self {
27        Self {
28            id: id.into(),
29            current_model_id: current_model_id.map(SharedString::from),
30            available_model_ids: available_model_ids
31                .iter()
32                .map(|id| SharedString::from(id.clone()))
33                .collect(),
34            on_select: None,
35        }
36    }
37
38    pub fn on_select(
39        mut self,
40        handler: impl Fn(SharedString, &mut Window, &mut App) + 'static,
41    ) -> Self {
42        self.on_select = Some(Arc::new(handler));
43        self
44    }
45}
46
47impl RenderOnce for AgentModelSelector {
48    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
49        let label = self
50            .current_model_id
51            .clone()
52            .unwrap_or_else(|| "Select model".into());
53        let current = self.current_model_id.clone();
54        let on_select = self.on_select.clone();
55        let available_model_ids = self.available_model_ids.clone();
56
57        let menu = ContextMenu::build(window, cx, move |mut menu, _window, _cx| {
58            for model_id in &available_model_ids {
59                let is_current = current.as_ref() == Some(model_id);
60                let model_id = model_id.clone();
61                let on_select = on_select.clone();
62                menu = menu.toggleable_entry(
63                    model_id.clone(),
64                    is_current,
65                    IconPosition::End,
66                    None,
67                    move |window, cx| {
68                        if let Some(on_select) = &on_select {
69                            on_select(model_id.clone(), window, cx);
70                        }
71                    },
72                );
73            }
74            menu
75        });
76
77        DropdownMenu::new(self.id, label, menu).style(DropdownStyle::Subtle)
78    }
79}
80
81impl Component for AgentModelSelector {
82    fn scope() -> ComponentScope {
83        ComponentScope::Agent
84    }
85
86    fn name() -> &'static str {
87        "AgentModelSelector"
88    }
89
90    fn description() -> Option<&'static str> {
91        Some(
92            "A dropdown for picking the active model for an agent thread. \
93             The caller supplies the current model id and the full list of \
94             available model ids; no data fetching happens here.",
95        )
96    }
97
98    fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
99        let models = vec![
100            "claude-opus-4.6".to_string(),
101            "claude-sonnet-4.6".to_string(),
102            "gpt-5".to_string(),
103        ];
104
105        Some(
106            v_flex()
107                .gap_4()
108                .child(single_example(
109                    "With selection",
110                    AgentModelSelector::new("model-selector-1", Some("claude-sonnet-4.6"), &models)
111                        .into_any_element(),
112                ))
113                .child(single_example(
114                    "No selection",
115                    AgentModelSelector::new("model-selector-2", None, &models).into_any_element(),
116                ))
117                .into_any_element(),
118        )
119    }
120}