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