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}