mode_selector.rs

  1use acp_thread::AgentSessionModes;
  2use agent_client_protocol as acp;
  3use agent_servers::AgentServer;
  4
  5use fs::Fs;
  6use gpui::{Context, Entity, WeakEntity, Window, prelude::*};
  7
  8use std::{rc::Rc, sync::Arc};
  9use ui::{
 10    Button, ContextMenu, ContextMenuEntry, KeyBinding, PopoverMenu, PopoverMenuHandle, Tooltip,
 11    prelude::*,
 12};
 13
 14use crate::{
 15    CycleModeSelector, ToggleProfileSelector,
 16    ui::{HoldForDefault, documentation_aside_side},
 17};
 18
 19pub struct ModeSelector {
 20    connection: Rc<dyn AgentSessionModes>,
 21    agent_server: Rc<dyn AgentServer>,
 22    menu_handle: PopoverMenuHandle<ContextMenu>,
 23    fs: Arc<dyn Fs>,
 24    setting_mode: bool,
 25}
 26
 27impl ModeSelector {
 28    pub fn new(
 29        session_modes: Rc<dyn AgentSessionModes>,
 30        agent_server: Rc<dyn AgentServer>,
 31        fs: Arc<dyn Fs>,
 32    ) -> Self {
 33        Self {
 34            connection: session_modes,
 35            agent_server,
 36            menu_handle: PopoverMenuHandle::default(),
 37            fs,
 38            setting_mode: false,
 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 side = documentation_aside_side(cx);
 94
 95            for mode in all_modes {
 96                let is_selected = &mode.id == &current_mode;
 97                let is_default = Some(&mode.id) == default_mode.as_ref();
 98                let entry = ContextMenuEntry::new(mode.name.clone())
 99                    .toggleable(IconPosition::End, is_selected);
100
101                let entry = if let Some(description) = &mode.description {
102                    entry.documentation_aside(side, {
103                        let description = description.clone();
104
105                        move |_| {
106                            v_flex()
107                                .gap_1()
108                                .child(Label::new(description.clone()))
109                                .child(HoldForDefault::new(is_default))
110                                .into_any_element()
111                        }
112                    })
113                } else {
114                    entry
115                };
116
117                menu.push_item(entry.handler({
118                    let mode_id = mode.id.clone();
119                    let weak_self = weak_self.clone();
120                    move |window, cx| {
121                        weak_self
122                            .update(cx, |this, cx| {
123                                if window.modifiers().secondary() {
124                                    this.agent_server.set_default_mode(
125                                        if is_default {
126                                            None
127                                        } else {
128                                            Some(mode_id.clone())
129                                        },
130                                        this.fs.clone(),
131                                        cx,
132                                    );
133                                }
134
135                                this.set_mode(mode_id.clone(), cx);
136                            })
137                            .ok();
138                    }
139                }));
140            }
141
142            menu.key_context("ModeSelector")
143        })
144    }
145}
146
147impl Render for ModeSelector {
148    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
149        let current_mode_id = self.connection.current_mode();
150        let current_mode_name = self
151            .connection
152            .all_modes()
153            .iter()
154            .find(|mode| mode.id == current_mode_id)
155            .map(|mode| mode.name.clone())
156            .unwrap_or_else(|| "Unknown".into());
157
158        let this = cx.weak_entity();
159
160        let icon = if self.menu_handle.is_deployed() {
161            IconName::ChevronUp
162        } else {
163            IconName::ChevronDown
164        };
165
166        let trigger_button = Button::new("mode-selector-trigger", current_mode_name)
167            .label_size(LabelSize::Small)
168            .color(Color::Muted)
169            .end_icon(Icon::new(icon).size(IconSize::XSmall).color(Color::Muted))
170            .disabled(self.setting_mode);
171
172        PopoverMenu::new("mode-selector")
173            .trigger_with_tooltip(
174                trigger_button,
175                Tooltip::element({
176                    move |_window, cx| {
177                        v_flex()
178                            .gap_1()
179                            .child(
180                                h_flex()
181                                    .gap_2()
182                                    .justify_between()
183                                    .child(Label::new("Change Mode"))
184                                    .child(KeyBinding::for_action(&ToggleProfileSelector, cx)),
185                            )
186                            .child(
187                                h_flex()
188                                    .pt_1()
189                                    .gap_2()
190                                    .border_t_1()
191                                    .border_color(cx.theme().colors().border_variant)
192                                    .justify_between()
193                                    .child(Label::new("Cycle Through Modes"))
194                                    .child(KeyBinding::for_action(&CycleModeSelector, cx)),
195                            )
196                            .into_any()
197                    }
198                }),
199            )
200            .anchor(gpui::Corner::BottomRight)
201            .with_handle(self.menu_handle.clone())
202            .offset(gpui::Point {
203                x: px(0.0),
204                y: px(-2.0),
205            })
206            .menu(move |window, cx| {
207                this.update(cx, |this, cx| this.build_context_menu(window, cx))
208                    .ok()
209            })
210    }
211}