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