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