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