1use acp_thread::AgentSessionModes;
2use agent_client_protocol as acp;
3use agent_servers::AgentServer;
4
5use fs::Fs;
6use gpui::{Context, Entity, WeakEntity, Window, prelude::*};
7
8use std::{rc::Rc, sync::Arc};
9use ui::{
10 Button, ContextMenu, ContextMenuEntry, KeyBinding, PopoverMenu, PopoverMenuHandle, Tooltip,
11 prelude::*,
12};
13
14use crate::{
15 CycleModeSelector, ToggleProfileSelector,
16 ui::{HoldForDefault, documentation_aside_side},
17};
18
19pub struct ModeSelector {
20 connection: Rc<dyn AgentSessionModes>,
21 agent_server: Rc<dyn AgentServer>,
22 menu_handle: PopoverMenuHandle<ContextMenu>,
23 fs: Arc<dyn Fs>,
24 setting_mode: bool,
25}
26
27impl ModeSelector {
28 pub fn new(
29 session_modes: Rc<dyn AgentSessionModes>,
30 agent_server: Rc<dyn AgentServer>,
31 fs: Arc<dyn Fs>,
32 ) -> Self {
33 Self {
34 connection: session_modes,
35 agent_server,
36 menu_handle: PopoverMenuHandle::default(),
37 fs,
38 setting_mode: false,
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 side = documentation_aside_side(cx);
94
95 for mode in all_modes {
96 let is_selected = &mode.id == ¤t_mode;
97 let is_default = Some(&mode.id) == default_mode.as_ref();
98 let entry = ContextMenuEntry::new(mode.name.clone())
99 .toggleable(IconPosition::End, is_selected);
100
101 let entry = if let Some(description) = &mode.description {
102 entry.documentation_aside(side, {
103 let description = description.clone();
104
105 move |_| {
106 v_flex()
107 .gap_1()
108 .child(Label::new(description.clone()))
109 .child(HoldForDefault::new(is_default))
110 .into_any_element()
111 }
112 })
113 } else {
114 entry
115 };
116
117 menu.push_item(entry.handler({
118 let mode_id = mode.id.clone();
119 let weak_self = weak_self.clone();
120 move |window, cx| {
121 weak_self
122 .update(cx, |this, cx| {
123 if window.modifiers().secondary() {
124 this.agent_server.set_default_mode(
125 if is_default {
126 None
127 } else {
128 Some(mode_id.clone())
129 },
130 this.fs.clone(),
131 cx,
132 );
133 }
134
135 this.set_mode(mode_id.clone(), cx);
136 })
137 .ok();
138 }
139 }));
140 }
141
142 menu.key_context("ModeSelector")
143 })
144 }
145}
146
147impl Render for ModeSelector {
148 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
149 let current_mode_id = self.connection.current_mode();
150 let current_mode_name = self
151 .connection
152 .all_modes()
153 .iter()
154 .find(|mode| mode.id == current_mode_id)
155 .map(|mode| mode.name.clone())
156 .unwrap_or_else(|| "Unknown".into());
157
158 let this = cx.weak_entity();
159
160 let icon = if self.menu_handle.is_deployed() {
161 IconName::ChevronUp
162 } else {
163 IconName::ChevronDown
164 };
165
166 let trigger_button = Button::new("mode-selector-trigger", current_mode_name)
167 .label_size(LabelSize::Small)
168 .color(Color::Muted)
169 .end_icon(Icon::new(icon).size(IconSize::XSmall).color(Color::Muted))
170 .disabled(self.setting_mode);
171
172 PopoverMenu::new("mode-selector")
173 .trigger_with_tooltip(
174 trigger_button,
175 Tooltip::element({
176 move |_window, cx| {
177 v_flex()
178 .gap_1()
179 .child(
180 h_flex()
181 .gap_2()
182 .justify_between()
183 .child(Label::new("Change Mode"))
184 .child(KeyBinding::for_action(&ToggleProfileSelector, cx)),
185 )
186 .child(
187 h_flex()
188 .pt_1()
189 .gap_2()
190 .border_t_1()
191 .border_color(cx.theme().colors().border_variant)
192 .justify_between()
193 .child(Label::new("Cycle Through Modes"))
194 .child(KeyBinding::for_action(&CycleModeSelector, cx)),
195 )
196 .into_any()
197 }
198 }),
199 )
200 .anchor(gpui::Corner::BottomRight)
201 .with_handle(self.menu_handle.clone())
202 .offset(gpui::Point {
203 x: px(0.0),
204 y: px(-2.0),
205 })
206 .menu(move |window, cx| {
207 this.update(cx, |this, cx| this.build_context_menu(window, cx))
208 .ok()
209 })
210 }
211}