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 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 settings = AgentSettings::get_global(cx);
94 let side = match settings.dock {
95 settings::DockPosition::Left => DocumentationSide::Right,
96 settings::DockPosition::Bottom | settings::DockPosition::Right => {
97 DocumentationSide::Left
98 }
99 };
100
101 for mode in all_modes {
102 let is_selected = &mode.id == ¤t_mode;
103 let is_default = Some(&mode.id) == default_mode.as_ref();
104 let entry = ContextMenuEntry::new(mode.name.clone())
105 .toggleable(IconPosition::End, is_selected);
106
107 let entry = if let Some(description) = &mode.description {
108 entry.documentation_aside(side, DocumentationEdge::Bottom, {
109 let description = description.clone();
110
111 move |cx| {
112 v_flex()
113 .gap_1()
114 .child(Label::new(description.clone()))
115 .child(
116 h_flex()
117 .pt_1()
118 .border_t_1()
119 .border_color(cx.theme().colors().border_variant)
120 .gap_0p5()
121 .text_sm()
122 .text_color(Color::Muted.color(cx))
123 .child("Hold")
124 .child(h_flex().flex_shrink_0().children(
125 ui::render_modifiers(
126 &gpui::Modifiers::secondary_key(),
127 PlatformStyle::platform(),
128 None,
129 Some(ui::TextSize::Default.rems(cx).into()),
130 true,
131 ),
132 ))
133 .child(div().map(|this| {
134 if is_default {
135 this.child("to also unset as default")
136 } else {
137 this.child("to also set as default")
138 }
139 })),
140 )
141 .into_any_element()
142 }
143 })
144 } else {
145 entry
146 };
147
148 menu.push_item(entry.handler({
149 let mode_id = mode.id.clone();
150 let weak_self = weak_self.clone();
151 move |window, cx| {
152 weak_self
153 .update(cx, |this, cx| {
154 if window.modifiers().secondary() {
155 this.agent_server.set_default_mode(
156 if is_default {
157 None
158 } else {
159 Some(mode_id.clone())
160 },
161 this.fs.clone(),
162 cx,
163 );
164 }
165
166 this.set_mode(mode_id.clone(), cx);
167 })
168 .ok();
169 }
170 }));
171 }
172
173 menu.key_context("ModeSelector")
174 })
175 }
176}
177
178impl Render for ModeSelector {
179 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
180 let current_mode_id = self.connection.current_mode();
181 let current_mode_name = self
182 .connection
183 .all_modes()
184 .iter()
185 .find(|mode| mode.id == current_mode_id)
186 .map(|mode| mode.name.clone())
187 .unwrap_or_else(|| "Unknown".into());
188
189 let this = cx.entity();
190
191 let icon = if self.menu_handle.is_deployed() {
192 IconName::ChevronUp
193 } else {
194 IconName::ChevronDown
195 };
196
197 let trigger_button = Button::new("mode-selector-trigger", current_mode_name)
198 .label_size(LabelSize::Small)
199 .color(Color::Muted)
200 .icon(icon)
201 .icon_size(IconSize::XSmall)
202 .icon_position(IconPosition::End)
203 .icon_color(Color::Muted)
204 .disabled(self.setting_mode);
205
206 PopoverMenu::new("mode-selector")
207 .trigger_with_tooltip(
208 trigger_button,
209 Tooltip::element({
210 let focus_handle = self.focus_handle.clone();
211 move |_window, cx| {
212 v_flex()
213 .gap_1()
214 .child(
215 h_flex()
216 .pb_1()
217 .gap_2()
218 .justify_between()
219 .border_b_1()
220 .border_color(cx.theme().colors().border_variant)
221 .child(Label::new("Cycle Through Modes"))
222 .child(KeyBinding::for_action_in(
223 &CycleModeSelector,
224 &focus_handle,
225 cx,
226 )),
227 )
228 .child(
229 h_flex()
230 .gap_2()
231 .justify_between()
232 .child(Label::new("Toggle Mode Menu"))
233 .child(KeyBinding::for_action_in(
234 &ToggleProfileSelector,
235 &focus_handle,
236 cx,
237 )),
238 )
239 .into_any()
240 }
241 }),
242 )
243 .anchor(gpui::Corner::BottomRight)
244 .with_handle(self.menu_handle.clone())
245 .offset(gpui::Point {
246 x: px(0.0),
247 y: px(-2.0),
248 })
249 .menu(move |window, cx| {
250 Some(this.update(cx, |this, cx| this.build_context_menu(window, cx)))
251 })
252 }
253}