1mod profile_modal_header;
2
3use std::sync::Arc;
4
5use assistant_settings::{
6 AgentProfile, AgentProfileContent, AssistantSettings, AssistantSettingsContent,
7 ContextServerPresetContent, VersionedAssistantSettingsContent,
8};
9use assistant_tool::ToolWorkingSet;
10use convert_case::{Case, Casing as _};
11use editor::Editor;
12use fs::Fs;
13use gpui::{prelude::*, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Subscription};
14use settings::{update_settings_file, Settings as _};
15use ui::{prelude::*, ListItem, ListItemSpacing, ListSeparator, Navigable, NavigableEntry};
16use workspace::{ModalView, Workspace};
17
18use crate::assistant_configuration::manage_profiles_modal::profile_modal_header::ProfileModalHeader;
19use crate::assistant_configuration::profile_picker::{ProfilePicker, ProfilePickerDelegate};
20use crate::assistant_configuration::tool_picker::{ToolPicker, ToolPickerDelegate};
21use crate::{AssistantPanel, ManageProfiles};
22
23enum Mode {
24 ChooseProfile {
25 profile_picker: Entity<ProfilePicker>,
26 _subscription: Subscription,
27 },
28 NewProfile(NewProfileMode),
29 ViewProfile(ViewProfileMode),
30 ConfigureTools {
31 profile_id: Arc<str>,
32 tool_picker: Entity<ToolPicker>,
33 _subscription: Subscription,
34 },
35}
36
37impl Mode {
38 pub fn choose_profile(window: &mut Window, cx: &mut Context<ManageProfilesModal>) -> Self {
39 let this = cx.entity();
40
41 let profile_picker = cx.new(|cx| {
42 let delegate = ProfilePickerDelegate::new(
43 move |profile_id, window, cx| {
44 this.update(cx, |this, cx| {
45 this.view_profile(profile_id.clone(), window, cx);
46 })
47 },
48 cx,
49 );
50 ProfilePicker::new(delegate, window, cx)
51 });
52 let dismiss_subscription = cx.subscribe_in(
53 &profile_picker,
54 window,
55 |_this, _profile_picker, _: &DismissEvent, _window, cx| {
56 cx.emit(DismissEvent);
57 },
58 );
59
60 Self::ChooseProfile {
61 profile_picker,
62 _subscription: dismiss_subscription,
63 }
64 }
65}
66
67#[derive(Clone)]
68pub struct ViewProfileMode {
69 profile_id: Arc<str>,
70 fork_profile: NavigableEntry,
71 configure_tools: NavigableEntry,
72}
73
74#[derive(Clone)]
75pub struct NewProfileMode {
76 name_editor: Entity<Editor>,
77 base_profile_id: Option<Arc<str>>,
78}
79
80pub struct ManageProfilesModal {
81 fs: Arc<dyn Fs>,
82 tools: Arc<ToolWorkingSet>,
83 focus_handle: FocusHandle,
84 mode: Mode,
85}
86
87impl ManageProfilesModal {
88 pub fn register(
89 workspace: &mut Workspace,
90 _window: Option<&mut Window>,
91 _cx: &mut Context<Workspace>,
92 ) {
93 workspace.register_action(|workspace, _: &ManageProfiles, window, cx| {
94 if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
95 let fs = workspace.app_state().fs.clone();
96 let thread_store = panel.read(cx).thread_store().read(cx);
97 let tools = thread_store.tools();
98 workspace.toggle_modal(window, cx, |window, cx| Self::new(fs, tools, window, cx))
99 }
100 });
101 }
102
103 pub fn new(
104 fs: Arc<dyn Fs>,
105 tools: Arc<ToolWorkingSet>,
106 window: &mut Window,
107 cx: &mut Context<Self>,
108 ) -> Self {
109 let focus_handle = cx.focus_handle();
110
111 Self {
112 fs,
113 tools,
114 focus_handle,
115 mode: Mode::choose_profile(window, cx),
116 }
117 }
118
119 fn choose_profile(&mut self, window: &mut Window, cx: &mut Context<Self>) {
120 self.mode = Mode::choose_profile(window, cx);
121 self.focus_handle(cx).focus(window);
122 }
123
124 fn new_profile(
125 &mut self,
126 base_profile_id: Option<Arc<str>>,
127 window: &mut Window,
128 cx: &mut Context<Self>,
129 ) {
130 let name_editor = cx.new(|cx| Editor::single_line(window, cx));
131 name_editor.update(cx, |editor, cx| {
132 editor.set_placeholder_text("Profile name", cx);
133 });
134
135 self.mode = Mode::NewProfile(NewProfileMode {
136 name_editor,
137 base_profile_id,
138 });
139 self.focus_handle(cx).focus(window);
140 }
141
142 pub fn view_profile(
143 &mut self,
144 profile_id: Arc<str>,
145 window: &mut Window,
146 cx: &mut Context<Self>,
147 ) {
148 self.mode = Mode::ViewProfile(ViewProfileMode {
149 profile_id,
150 fork_profile: NavigableEntry::focusable(cx),
151 configure_tools: NavigableEntry::focusable(cx),
152 });
153 self.focus_handle(cx).focus(window);
154 }
155
156 fn configure_tools(
157 &mut self,
158 profile_id: Arc<str>,
159 window: &mut Window,
160 cx: &mut Context<Self>,
161 ) {
162 let settings = AssistantSettings::get_global(cx);
163 let Some(profile) = settings.profiles.get(&profile_id).cloned() else {
164 return;
165 };
166
167 let tool_picker = cx.new(|cx| {
168 let delegate = ToolPickerDelegate::new(
169 self.fs.clone(),
170 self.tools.clone(),
171 profile_id.clone(),
172 profile,
173 cx,
174 );
175 ToolPicker::new(delegate, window, cx)
176 });
177 let dismiss_subscription = cx.subscribe_in(&tool_picker, window, {
178 let profile_id = profile_id.clone();
179 move |this, _tool_picker, _: &DismissEvent, window, cx| {
180 this.view_profile(profile_id.clone(), window, cx);
181 }
182 });
183
184 self.mode = Mode::ConfigureTools {
185 profile_id,
186 tool_picker,
187 _subscription: dismiss_subscription,
188 };
189 self.focus_handle(cx).focus(window);
190 }
191
192 fn confirm(&mut self, window: &mut Window, cx: &mut Context<Self>) {
193 match &self.mode {
194 Mode::ChooseProfile { .. } => {}
195 Mode::NewProfile(mode) => {
196 let settings = AssistantSettings::get_global(cx);
197
198 let base_profile = mode
199 .base_profile_id
200 .as_ref()
201 .and_then(|profile_id| settings.profiles.get(profile_id).cloned());
202
203 let name = mode.name_editor.read(cx).text(cx);
204 let profile_id: Arc<str> = name.to_case(Case::Kebab).into();
205
206 let profile = AgentProfile {
207 name: name.into(),
208 tools: base_profile
209 .as_ref()
210 .map(|profile| profile.tools.clone())
211 .unwrap_or_default(),
212 context_servers: base_profile
213 .map(|profile| profile.context_servers)
214 .unwrap_or_default(),
215 };
216
217 self.create_profile(profile_id.clone(), profile, cx);
218 self.view_profile(profile_id, window, cx);
219 }
220 Mode::ViewProfile(_) => {}
221 Mode::ConfigureTools { .. } => {}
222 }
223 }
224
225 fn cancel(&mut self, window: &mut Window, cx: &mut Context<Self>) {
226 match &self.mode {
227 Mode::ChooseProfile { .. } => {}
228 Mode::NewProfile(mode) => {
229 if let Some(profile_id) = mode.base_profile_id.clone() {
230 self.view_profile(profile_id, window, cx);
231 } else {
232 self.choose_profile(window, cx);
233 }
234 }
235 Mode::ViewProfile(_) => self.choose_profile(window, cx),
236 Mode::ConfigureTools { .. } => {}
237 }
238 }
239
240 fn create_profile(&self, profile_id: Arc<str>, profile: AgentProfile, cx: &mut Context<Self>) {
241 update_settings_file::<AssistantSettings>(self.fs.clone(), cx, {
242 move |settings, _cx| match settings {
243 AssistantSettingsContent::Versioned(VersionedAssistantSettingsContent::V2(
244 settings,
245 )) => {
246 let profiles = settings.profiles.get_or_insert_default();
247 if profiles.contains_key(&profile_id) {
248 log::error!("profile with ID '{profile_id}' already exists");
249 return;
250 }
251
252 profiles.insert(
253 profile_id,
254 AgentProfileContent {
255 name: profile.name.into(),
256 tools: profile.tools,
257 context_servers: profile
258 .context_servers
259 .into_iter()
260 .map(|(server_id, preset)| {
261 (
262 server_id,
263 ContextServerPresetContent {
264 tools: preset.tools,
265 },
266 )
267 })
268 .collect(),
269 },
270 );
271 }
272 _ => {}
273 }
274 });
275 }
276}
277
278impl ModalView for ManageProfilesModal {}
279
280impl Focusable for ManageProfilesModal {
281 fn focus_handle(&self, cx: &App) -> FocusHandle {
282 match &self.mode {
283 Mode::ChooseProfile { profile_picker, .. } => profile_picker.focus_handle(cx),
284 Mode::NewProfile(mode) => mode.name_editor.focus_handle(cx),
285 Mode::ViewProfile(_) => self.focus_handle.clone(),
286 Mode::ConfigureTools { tool_picker, .. } => tool_picker.focus_handle(cx),
287 }
288 }
289}
290
291impl EventEmitter<DismissEvent> for ManageProfilesModal {}
292
293impl ManageProfilesModal {
294 fn render_new_profile(
295 &mut self,
296 mode: NewProfileMode,
297 _window: &mut Window,
298 cx: &mut Context<Self>,
299 ) -> impl IntoElement {
300 v_flex()
301 .id("new-profile")
302 .track_focus(&self.focus_handle(cx))
303 .child(h_flex().p_2().child(mode.name_editor.clone()))
304 }
305
306 fn render_view_profile(
307 &mut self,
308 mode: ViewProfileMode,
309 window: &mut Window,
310 cx: &mut Context<Self>,
311 ) -> impl IntoElement {
312 let settings = AssistantSettings::get_global(cx);
313
314 let profile_name = settings
315 .profiles
316 .get(&mode.profile_id)
317 .map(|profile| profile.name.clone())
318 .unwrap_or_else(|| "Unknown".into());
319
320 Navigable::new(
321 div()
322 .track_focus(&self.focus_handle(cx))
323 .size_full()
324 .child(ProfileModalHeader::new(
325 profile_name,
326 IconName::ZedAssistant,
327 ))
328 .child(
329 v_flex()
330 .pb_1()
331 .child(ListSeparator)
332 .child(
333 div()
334 .id("fork-profile")
335 .track_focus(&mode.fork_profile.focus_handle)
336 .on_action({
337 let profile_id = mode.profile_id.clone();
338 cx.listener(move |this, _: &menu::Confirm, window, cx| {
339 this.new_profile(Some(profile_id.clone()), window, cx);
340 })
341 })
342 .child(
343 ListItem::new("fork-profile")
344 .toggle_state(
345 mode.fork_profile
346 .focus_handle
347 .contains_focused(window, cx),
348 )
349 .inset(true)
350 .spacing(ListItemSpacing::Sparse)
351 .start_slot(Icon::new(IconName::GitBranch))
352 .child(Label::new("Fork Profile"))
353 .on_click({
354 let profile_id = mode.profile_id.clone();
355 cx.listener(move |this, _, window, cx| {
356 this.new_profile(
357 Some(profile_id.clone()),
358 window,
359 cx,
360 );
361 })
362 }),
363 ),
364 )
365 .child(
366 div()
367 .id("configure-tools")
368 .track_focus(&mode.configure_tools.focus_handle)
369 .on_action({
370 let profile_id = mode.profile_id.clone();
371 cx.listener(move |this, _: &menu::Confirm, window, cx| {
372 this.configure_tools(profile_id.clone(), window, cx);
373 })
374 })
375 .child(
376 ListItem::new("configure-tools")
377 .toggle_state(
378 mode.configure_tools
379 .focus_handle
380 .contains_focused(window, cx),
381 )
382 .inset(true)
383 .spacing(ListItemSpacing::Sparse)
384 .start_slot(Icon::new(IconName::Cog))
385 .child(Label::new("Configure Tools"))
386 .on_click({
387 let profile_id = mode.profile_id.clone();
388 cx.listener(move |this, _, window, cx| {
389 this.configure_tools(
390 profile_id.clone(),
391 window,
392 cx,
393 );
394 })
395 }),
396 ),
397 ),
398 )
399 .into_any_element(),
400 )
401 .entry(mode.fork_profile)
402 .entry(mode.configure_tools)
403 }
404}
405
406impl Render for ManageProfilesModal {
407 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
408 let settings = AssistantSettings::get_global(cx);
409
410 div()
411 .elevation_3(cx)
412 .w(rems(34.))
413 .key_context("ManageProfilesModal")
414 .on_action(cx.listener(|this, _: &menu::Cancel, window, cx| this.cancel(window, cx)))
415 .on_action(cx.listener(|this, _: &menu::Confirm, window, cx| this.confirm(window, cx)))
416 .capture_any_mouse_down(cx.listener(|this, _, window, cx| {
417 this.focus_handle(cx).focus(window);
418 }))
419 .on_mouse_down_out(cx.listener(|_this, _, _, cx| cx.emit(DismissEvent)))
420 .child(match &self.mode {
421 Mode::ChooseProfile { profile_picker, .. } => div()
422 .child(ProfileModalHeader::new("Profiles", IconName::ZedAssistant))
423 .child(ListSeparator)
424 .child(profile_picker.clone())
425 .into_any_element(),
426 Mode::NewProfile(mode) => self
427 .render_new_profile(mode.clone(), window, cx)
428 .into_any_element(),
429 Mode::ViewProfile(mode) => self
430 .render_view_profile(mode.clone(), window, cx)
431 .into_any_element(),
432 Mode::ConfigureTools {
433 profile_id,
434 tool_picker,
435 ..
436 } => {
437 let profile_name = settings
438 .profiles
439 .get(profile_id)
440 .map(|profile| profile.name.clone())
441 .unwrap_or_else(|| "Unknown".into());
442
443 div()
444 .child(ProfileModalHeader::new(
445 format!("{profile_name}: Configure Tools"),
446 IconName::Cog,
447 ))
448 .child(ListSeparator)
449 .child(tool_picker.clone())
450 .into_any_element()
451 }
452 })
453 }
454}