mode_selector.rs

  1use acp_thread::AgentSessionModes;
  2use agent_client_protocol as acp;
  3use agent_servers::AgentServer;
  4use agent_settings::AgentSettings;
  5use fs::Fs;
  6use gpui::{Context, Entity, FocusHandle, WeakEntity, Window, prelude::*};
  7use settings::Settings as _;
  8use std::{rc::Rc, sync::Arc};
  9use ui::{
 10    Button, ContextMenu, ContextMenuEntry, DocumentationEdge, DocumentationSide, KeyBinding,
 11    PopoverMenu, PopoverMenuHandle, Tooltip, prelude::*,
 12};
 13
 14use crate::{CycleModeSelector, ToggleProfileSelector};
 15
 16pub struct ModeSelector {
 17    connection: Rc<dyn AgentSessionModes>,
 18    agent_server: Rc<dyn AgentServer>,
 19    menu_handle: PopoverMenuHandle<ContextMenu>,
 20    focus_handle: FocusHandle,
 21    fs: Arc<dyn Fs>,
 22    setting_mode: bool,
 23}
 24
 25impl ModeSelector {
 26    pub fn new(
 27        session_modes: Rc<dyn AgentSessionModes>,
 28        agent_server: Rc<dyn AgentServer>,
 29        fs: Arc<dyn Fs>,
 30        focus_handle: FocusHandle,
 31    ) -> Self {
 32        Self {
 33            connection: session_modes,
 34            agent_server,
 35            menu_handle: PopoverMenuHandle::default(),
 36            fs,
 37            setting_mode: false,
 38            focus_handle,
 39        }
 40    }
 41
 42    pub fn menu_handle(&self) -> PopoverMenuHandle<ContextMenu> {
 43        self.menu_handle.clone()
 44    }
 45
 46    pub fn cycle_mode(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
 47        let all_modes = self.connection.all_modes();
 48        let current_mode = self.connection.current_mode();
 49
 50        let current_index = all_modes
 51            .iter()
 52            .position(|mode| mode.id.0 == current_mode.0)
 53            .unwrap_or(0);
 54
 55        let next_index = (current_index + 1) % all_modes.len();
 56        self.set_mode(all_modes[next_index].id.clone(), cx);
 57    }
 58
 59    pub fn mode(&self) -> acp::SessionModeId {
 60        self.connection.current_mode()
 61    }
 62
 63    pub fn set_mode(&mut self, mode: acp::SessionModeId, cx: &mut Context<Self>) {
 64        let task = self.connection.set_mode(mode, cx);
 65        self.setting_mode = true;
 66        cx.notify();
 67
 68        cx.spawn(async move |this: WeakEntity<ModeSelector>, cx| {
 69            if let Err(err) = task.await {
 70                log::error!("Failed to set session mode: {:?}", err);
 71            }
 72            this.update(cx, |this, cx| {
 73                this.setting_mode = false;
 74                cx.notify();
 75            })
 76            .ok();
 77        })
 78        .detach();
 79    }
 80
 81    fn build_context_menu(
 82        &self,
 83        window: &mut Window,
 84        cx: &mut Context<Self>,
 85    ) -> Entity<ContextMenu> {
 86        let weak_self = cx.weak_entity();
 87
 88        ContextMenu::build(window, cx, move |mut menu, _window, cx| {
 89            let all_modes = self.connection.all_modes();
 90            let current_mode = self.connection.current_mode();
 91            let default_mode = self.agent_server.default_mode(cx);
 92
 93            let settings = AgentSettings::get_global(cx);
 94            let side = match settings.dock {
 95                settings::DockPosition::Left => DocumentationSide::Right,
 96                settings::DockPosition::Bottom | settings::DockPosition::Right => {
 97                    DocumentationSide::Left
 98                }
 99            };
100
101            for mode in all_modes {
102                let is_selected = &mode.id == &current_mode;
103                let is_default = Some(&mode.id) == default_mode.as_ref();
104                let entry = ContextMenuEntry::new(mode.name.clone())
105                    .toggleable(IconPosition::End, is_selected);
106
107                let entry = if let Some(description) = &mode.description {
108                    entry.documentation_aside(side, DocumentationEdge::Bottom, {
109                        let description = description.clone();
110
111                        move |cx| {
112                            v_flex()
113                                .gap_1()
114                                .child(Label::new(description.clone()))
115                                .child(
116                                    h_flex()
117                                        .pt_1()
118                                        .border_t_1()
119                                        .border_color(cx.theme().colors().border_variant)
120                                        .gap_0p5()
121                                        .text_sm()
122                                        .text_color(Color::Muted.color(cx))
123                                        .child("Hold")
124                                        .child(h_flex().flex_shrink_0().children(
125                                            ui::render_modifiers(
126                                                &gpui::Modifiers::secondary_key(),
127                                                PlatformStyle::platform(),
128                                                None,
129                                                Some(ui::TextSize::Default.rems(cx).into()),
130                                                true,
131                                            ),
132                                        ))
133                                        .child(div().map(|this| {
134                                            if is_default {
135                                                this.child("to also unset as default")
136                                            } else {
137                                                this.child("to also set as default")
138                                            }
139                                        })),
140                                )
141                                .into_any_element()
142                        }
143                    })
144                } else {
145                    entry
146                };
147
148                menu.push_item(entry.handler({
149                    let mode_id = mode.id.clone();
150                    let weak_self = weak_self.clone();
151                    move |window, cx| {
152                        weak_self
153                            .update(cx, |this, cx| {
154                                if window.modifiers().secondary() {
155                                    this.agent_server.set_default_mode(
156                                        if is_default {
157                                            None
158                                        } else {
159                                            Some(mode_id.clone())
160                                        },
161                                        this.fs.clone(),
162                                        cx,
163                                    );
164                                }
165
166                                this.set_mode(mode_id.clone(), cx);
167                            })
168                            .ok();
169                    }
170                }));
171            }
172
173            menu.key_context("ModeSelector")
174        })
175    }
176}
177
178impl Render for ModeSelector {
179    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
180        let current_mode_id = self.connection.current_mode();
181        let current_mode_name = self
182            .connection
183            .all_modes()
184            .iter()
185            .find(|mode| mode.id == current_mode_id)
186            .map(|mode| mode.name.clone())
187            .unwrap_or_else(|| "Unknown".into());
188
189        let this = cx.entity();
190
191        let icon = if self.menu_handle.is_deployed() {
192            IconName::ChevronUp
193        } else {
194            IconName::ChevronDown
195        };
196
197        let trigger_button = Button::new("mode-selector-trigger", current_mode_name)
198            .label_size(LabelSize::Small)
199            .color(Color::Muted)
200            .icon(icon)
201            .icon_size(IconSize::XSmall)
202            .icon_position(IconPosition::End)
203            .icon_color(Color::Muted)
204            .disabled(self.setting_mode);
205
206        PopoverMenu::new("mode-selector")
207            .trigger_with_tooltip(
208                trigger_button,
209                Tooltip::element({
210                    let focus_handle = self.focus_handle.clone();
211                    move |_window, cx| {
212                        v_flex()
213                            .gap_1()
214                            .child(
215                                h_flex()
216                                    .pb_1()
217                                    .gap_2()
218                                    .justify_between()
219                                    .border_b_1()
220                                    .border_color(cx.theme().colors().border_variant)
221                                    .child(Label::new("Cycle Through Modes"))
222                                    .child(KeyBinding::for_action_in(
223                                        &CycleModeSelector,
224                                        &focus_handle,
225                                        cx,
226                                    )),
227                            )
228                            .child(
229                                h_flex()
230                                    .gap_2()
231                                    .justify_between()
232                                    .child(Label::new("Toggle Mode Menu"))
233                                    .child(KeyBinding::for_action_in(
234                                        &ToggleProfileSelector,
235                                        &focus_handle,
236                                        cx,
237                                    )),
238                            )
239                            .into_any()
240                    }
241                }),
242            )
243            .anchor(gpui::Corner::BottomRight)
244            .with_handle(self.menu_handle.clone())
245            .offset(gpui::Point {
246                x: px(0.0),
247                y: px(-2.0),
248            })
249            .menu(move |window, cx| {
250                Some(this.update(cx, |this, cx| this.build_context_menu(window, cx)))
251            })
252    }
253}