mode_selector.rs

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