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