Skip to main content

ui/components/ai/
mode_selector.rs

1use std::sync::Arc;
2
3use gpui::SharedString;
4
5use crate::prelude::*;
6use crate::{ContextMenu, DropdownMenu, DropdownStyle};
7
8/// The lifecycle an agent thread's session runs under. Plain, protocol-agnostic
9/// data: the caller maps this to whatever session-mode concept its own
10/// runtime uses.
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum SessionModeChoice {
13    /// The session stays alive across multiple requests.
14    Persistent,
15    /// The session is torn down after a single request.
16    Oneshot,
17}
18
19impl SessionModeChoice {
20    const ALL: [SessionModeChoice; 2] = [SessionModeChoice::Persistent, SessionModeChoice::Oneshot];
21
22    fn label(self) -> &'static str {
23        match self {
24            SessionModeChoice::Persistent => "Persistent",
25            SessionModeChoice::Oneshot => "One-shot",
26        }
27    }
28}
29
30/// A dropdown for picking whether an agent thread's session persists across
31/// requests or runs as a single one-shot exchange.
32///
33/// Pure builder: the caller supplies the current choice and is notified via
34/// `on_select` when the user picks a different one. No data fetching happens
35/// here.
36#[derive(IntoElement, RegisterComponent)]
37pub struct ModeSelector {
38    id: ElementId,
39    current: Option<SessionModeChoice>,
40    on_select: Option<Arc<dyn Fn(SessionModeChoice, &mut Window, &mut App) + 'static>>,
41}
42
43impl ModeSelector {
44    pub fn new(id: impl Into<ElementId>, current: Option<SessionModeChoice>) -> Self {
45        Self {
46            id: id.into(),
47            current,
48            on_select: None,
49        }
50    }
51
52    pub fn on_select(
53        mut self,
54        handler: impl Fn(SessionModeChoice, &mut Window, &mut App) + 'static,
55    ) -> Self {
56        self.on_select = Some(Arc::new(handler));
57        self
58    }
59}
60
61impl RenderOnce for ModeSelector {
62    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
63        let label: SharedString = self
64            .current
65            .map(SessionModeChoice::label)
66            .unwrap_or("Select mode")
67            .into();
68        let current = self.current;
69        let on_select = self.on_select.clone();
70
71        let menu = ContextMenu::build(window, cx, move |mut menu, _window, _cx| {
72            for mode in SessionModeChoice::ALL {
73                let is_current = current == Some(mode);
74                let on_select = on_select.clone();
75                menu = menu.toggleable_entry(
76                    mode.label(),
77                    is_current,
78                    IconPosition::End,
79                    None,
80                    move |window, cx| {
81                        if let Some(on_select) = &on_select {
82                            on_select(mode, window, cx);
83                        }
84                    },
85                );
86            }
87            menu
88        });
89
90        DropdownMenu::new(self.id, label, menu).style(DropdownStyle::Subtle)
91    }
92}
93
94impl Component for ModeSelector {
95    fn scope() -> ComponentScope {
96        ComponentScope::Agent
97    }
98
99    fn name() -> &'static str {
100        "ModeSelector"
101    }
102
103    fn description() -> Option<&'static str> {
104        Some(
105            "A dropdown for picking whether an agent thread's session \
106             persists across requests or runs as a single one-shot exchange.",
107        )
108    }
109
110    fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
111        Some(
112            v_flex()
113                .gap_4()
114                .child(single_example(
115                    "Persistent selected",
116                    ModeSelector::new("mode-selector-1", Some(SessionModeChoice::Persistent))
117                        .into_any_element(),
118                ))
119                .child(single_example(
120                    "One-shot selected",
121                    ModeSelector::new("mode-selector-2", Some(SessionModeChoice::Oneshot))
122                        .into_any_element(),
123                ))
124                .child(single_example(
125                    "No selection",
126                    ModeSelector::new("mode-selector-3", None).into_any_element(),
127                ))
128                .into_any_element(),
129        )
130    }
131}