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, ui::HoldForDefault};
 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 |_| {
112                            v_flex()
113                                .gap_1()
114                                .child(Label::new(description.clone()))
115                                .child(HoldForDefault::new(is_default))
116                                .into_any_element()
117                        }
118                    })
119                } else {
120                    entry
121                };
122
123                menu.push_item(entry.handler({
124                    let mode_id = mode.id.clone();
125                    let weak_self = weak_self.clone();
126                    move |window, cx| {
127                        weak_self
128                            .update(cx, |this, cx| {
129                                if window.modifiers().secondary() {
130                                    this.agent_server.set_default_mode(
131                                        if is_default {
132                                            None
133                                        } else {
134                                            Some(mode_id.clone())
135                                        },
136                                        this.fs.clone(),
137                                        cx,
138                                    );
139                                }
140
141                                this.set_mode(mode_id.clone(), cx);
142                            })
143                            .ok();
144                    }
145                }));
146            }
147
148            menu.key_context("ModeSelector")
149        })
150    }
151}
152
153impl Render for ModeSelector {
154    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
155        let current_mode_id = self.connection.current_mode();
156        let current_mode_name = self
157            .connection
158            .all_modes()
159            .iter()
160            .find(|mode| mode.id == current_mode_id)
161            .map(|mode| mode.name.clone())
162            .unwrap_or_else(|| "Unknown".into());
163
164        let this = cx.entity();
165
166        let icon = if self.menu_handle.is_deployed() {
167            IconName::ChevronUp
168        } else {
169            IconName::ChevronDown
170        };
171
172        let trigger_button = Button::new("mode-selector-trigger", current_mode_name)
173            .label_size(LabelSize::Small)
174            .color(Color::Muted)
175            .icon(icon)
176            .icon_size(IconSize::XSmall)
177            .icon_position(IconPosition::End)
178            .icon_color(Color::Muted)
179            .disabled(self.setting_mode);
180
181        PopoverMenu::new("mode-selector")
182            .trigger_with_tooltip(
183                trigger_button,
184                Tooltip::element({
185                    let focus_handle = self.focus_handle.clone();
186                    move |_window, cx| {
187                        v_flex()
188                            .gap_1()
189                            .child(
190                                h_flex()
191                                    .pb_1()
192                                    .gap_2()
193                                    .justify_between()
194                                    .border_b_1()
195                                    .border_color(cx.theme().colors().border_variant)
196                                    .child(Label::new("Cycle Through Modes"))
197                                    .child(KeyBinding::for_action_in(
198                                        &CycleModeSelector,
199                                        &focus_handle,
200                                        cx,
201                                    )),
202                            )
203                            .child(
204                                h_flex()
205                                    .gap_2()
206                                    .justify_between()
207                                    .child(Label::new("Toggle Mode Menu"))
208                                    .child(KeyBinding::for_action_in(
209                                        &ToggleProfileSelector,
210                                        &focus_handle,
211                                        cx,
212                                    )),
213                            )
214                            .into_any()
215                    }
216                }),
217            )
218            .anchor(gpui::Corner::BottomRight)
219            .with_handle(self.menu_handle.clone())
220            .offset(gpui::Point {
221                x: px(0.0),
222                y: px(-2.0),
223            })
224            .menu(move |window, cx| {
225                Some(this.update(cx, |this, cx| this.build_context_menu(window, cx)))
226            })
227    }
228}