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 icon = if self.menu_handle.is_deployed() {
178            IconName::ChevronUp
179        } else {
180            IconName::ChevronDown
181        };
182
183        let trigger_button = Button::new("mode-selector-trigger", current_mode_name)
184            .label_size(LabelSize::Small)
185            .color(Color::Muted)
186            .icon(icon)
187            .icon_size(IconSize::XSmall)
188            .icon_position(IconPosition::End)
189            .icon_color(Color::Muted)
190            .disabled(self.setting_mode);
191
192        PopoverMenu::new("mode-selector")
193            .trigger_with_tooltip(
194                trigger_button,
195                Tooltip::element({
196                    let focus_handle = self.focus_handle.clone();
197                    move |window, cx| {
198                        v_flex()
199                            .gap_1()
200                            .child(
201                                h_flex()
202                                    .pb_1()
203                                    .gap_2()
204                                    .justify_between()
205                                    .border_b_1()
206                                    .border_color(cx.theme().colors().border_variant)
207                                    .child(Label::new("Cycle Through Modes"))
208                                    .children(KeyBinding::for_action_in(
209                                        &CycleModeSelector,
210                                        &focus_handle,
211                                        window,
212                                        cx,
213                                    )),
214                            )
215                            .child(
216                                h_flex()
217                                    .gap_2()
218                                    .justify_between()
219                                    .child(Label::new("Toggle Mode Menu"))
220                                    .children(KeyBinding::for_action_in(
221                                        &ToggleProfileSelector,
222                                        &focus_handle,
223                                        window,
224                                        cx,
225                                    )),
226                            )
227                            .into_any()
228                    }
229                }),
230            )
231            .anchor(gpui::Corner::BottomRight)
232            .with_handle(self.menu_handle.clone())
233            .offset(gpui::Point {
234                x: px(0.0),
235                y: px(-2.0),
236            })
237            .menu(move |window, cx| {
238                Some(this.update(cx, |this, cx| this.build_context_menu(window, cx)))
239            })
240    }
241}