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, WeakEntity, Window, prelude::*};
7use settings::Settings as _;
8use std::{rc::Rc, sync::Arc};
9use ui::{
10 Button, ContextMenu, ContextMenuEntry, DocumentationSide, KeyBinding, PopoverMenu,
11 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 fs: Arc<dyn Fs>,
21 setting_mode: bool,
22}
23
24impl ModeSelector {
25 pub fn new(
26 session_modes: Rc<dyn AgentSessionModes>,
27 agent_server: Rc<dyn AgentServer>,
28 fs: Arc<dyn Fs>,
29 ) -> Self {
30 Self {
31 connection: session_modes,
32 agent_server,
33 menu_handle: PopoverMenuHandle::default(),
34 fs,
35 setting_mode: false,
36 }
37 }
38
39 pub fn menu_handle(&self) -> PopoverMenuHandle<ContextMenu> {
40 self.menu_handle.clone()
41 }
42
43 pub fn cycle_mode(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
44 let all_modes = self.connection.all_modes();
45 let current_mode = self.connection.current_mode();
46
47 let current_index = all_modes
48 .iter()
49 .position(|mode| mode.id.0 == current_mode.0)
50 .unwrap_or(0);
51
52 let next_index = (current_index + 1) % all_modes.len();
53 self.set_mode(all_modes[next_index].id.clone(), cx);
54 }
55
56 pub fn mode(&self) -> acp::SessionModeId {
57 self.connection.current_mode()
58 }
59
60 pub fn set_mode(&mut self, mode: acp::SessionModeId, cx: &mut Context<Self>) {
61 let task = self.connection.set_mode(mode, cx);
62 self.setting_mode = true;
63 cx.notify();
64
65 cx.spawn(async move |this: WeakEntity<ModeSelector>, cx| {
66 if let Err(err) = task.await {
67 log::error!("Failed to set session mode: {:?}", err);
68 }
69 this.update(cx, |this, cx| {
70 this.setting_mode = false;
71 cx.notify();
72 })
73 .ok();
74 })
75 .detach();
76 }
77
78 fn build_context_menu(
79 &self,
80 window: &mut Window,
81 cx: &mut Context<Self>,
82 ) -> Entity<ContextMenu> {
83 let weak_self = cx.weak_entity();
84
85 ContextMenu::build(window, cx, move |mut menu, _window, cx| {
86 let all_modes = self.connection.all_modes();
87 let current_mode = self.connection.current_mode();
88 let default_mode = self.agent_server.default_mode(cx);
89
90 let settings = AgentSettings::get_global(cx);
91 let side = match settings.dock {
92 settings::DockPosition::Left => DocumentationSide::Right,
93 settings::DockPosition::Bottom | settings::DockPosition::Right => {
94 DocumentationSide::Left
95 }
96 };
97
98 for mode in all_modes {
99 let is_selected = &mode.id == ¤t_mode;
100 let is_default = Some(&mode.id) == default_mode.as_ref();
101 let entry = ContextMenuEntry::new(mode.name.clone())
102 .toggleable(IconPosition::End, is_selected);
103
104 let entry = if let Some(description) = &mode.description {
105 entry.documentation_aside(side, {
106 let description = description.clone();
107
108 move |_| {
109 v_flex()
110 .gap_1()
111 .child(Label::new(description.clone()))
112 .child(HoldForDefault::new(is_default))
113 .into_any_element()
114 }
115 })
116 } else {
117 entry
118 };
119
120 menu.push_item(entry.handler({
121 let mode_id = mode.id.clone();
122 let weak_self = weak_self.clone();
123 move |window, cx| {
124 weak_self
125 .update(cx, |this, cx| {
126 if window.modifiers().secondary() {
127 this.agent_server.set_default_mode(
128 if is_default {
129 None
130 } else {
131 Some(mode_id.clone())
132 },
133 this.fs.clone(),
134 cx,
135 );
136 }
137
138 this.set_mode(mode_id.clone(), cx);
139 })
140 .ok();
141 }
142 }));
143 }
144
145 menu.key_context("ModeSelector")
146 })
147 }
148}
149
150impl Render for ModeSelector {
151 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
152 let current_mode_id = self.connection.current_mode();
153 let current_mode_name = self
154 .connection
155 .all_modes()
156 .iter()
157 .find(|mode| mode.id == current_mode_id)
158 .map(|mode| mode.name.clone())
159 .unwrap_or_else(|| "Unknown".into());
160
161 let this = cx.weak_entity();
162
163 let icon = if self.menu_handle.is_deployed() {
164 IconName::ChevronUp
165 } else {
166 IconName::ChevronDown
167 };
168
169 let trigger_button = Button::new("mode-selector-trigger", current_mode_name)
170 .label_size(LabelSize::Small)
171 .color(Color::Muted)
172 .icon(icon)
173 .icon_size(IconSize::XSmall)
174 .icon_position(IconPosition::End)
175 .icon_color(Color::Muted)
176 .disabled(self.setting_mode);
177
178 PopoverMenu::new("mode-selector")
179 .trigger_with_tooltip(
180 trigger_button,
181 Tooltip::element({
182 move |_window, cx| {
183 v_flex()
184 .gap_1()
185 .child(
186 h_flex()
187 .gap_2()
188 .justify_between()
189 .child(Label::new("Change Mode"))
190 .child(KeyBinding::for_action(&ToggleProfileSelector, cx)),
191 )
192 .child(
193 h_flex()
194 .pt_1()
195 .gap_2()
196 .border_t_1()
197 .border_color(cx.theme().colors().border_variant)
198 .justify_between()
199 .child(Label::new("Cycle Through Modes"))
200 .child(KeyBinding::for_action(&CycleModeSelector, cx)),
201 )
202 .into_any()
203 }
204 }),
205 )
206 .anchor(gpui::Corner::BottomRight)
207 .with_handle(self.menu_handle.clone())
208 .offset(gpui::Point {
209 x: px(0.0),
210 y: px(-2.0),
211 })
212 .menu(move |window, cx| {
213 this.update(cx, |this, cx| this.build_context_menu(window, cx))
214 .ok()
215 })
216 }
217}