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