1use crate::{
2 terminal_inline_assistant::TerminalInlineAssistant, DeployHistory, DeployPromptLibrary,
3 InlineAssistant, NewContext,
4};
5use anyhow::{anyhow, Result};
6use assistant_context_editor::{
7 make_lsp_adapter_delegate, AssistantPanelDelegate, Context, ContextEditor,
8 ContextEditorToolbarItem, ContextEditorToolbarItemEvent, ContextHistory, ContextId,
9 ContextStore, ContextStoreEvent, InsertDraggedFiles, SlashCommandCompletionProvider,
10 ToggleModelSelector, DEFAULT_TAB_TITLE,
11};
12use assistant_settings::{AssistantDockPosition, AssistantSettings};
13use assistant_slash_command::SlashCommandWorkingSet;
14use assistant_tool::ToolWorkingSet;
15use client::{proto, Client, Status};
16use collections::HashMap;
17use editor::{Editor, EditorEvent};
18use fs::Fs;
19use gpui::{
20 canvas, div, prelude::*, Action, AnyView, AppContext, AsyncWindowContext, EventEmitter,
21 ExternalPaths, FocusHandle, FocusableView, InteractiveElement, IntoElement, Model,
22 ParentElement, Pixels, Render, SharedString, StatefulInteractiveElement, Styled, Subscription,
23 Task, UpdateGlobal, View, WeakView,
24};
25use language::LanguageRegistry;
26use language_model::{
27 LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry, ZED_CLOUD_PROVIDER_ID,
28};
29use language_model_selector::LanguageModelSelector;
30use project::Project;
31use prompt_library::{open_prompt_library, PromptBuilder, PromptLibrary};
32use search::{buffer_search::DivRegistrar, BufferSearchBar};
33use settings::{update_settings_file, Settings};
34use smol::stream::StreamExt;
35use std::{ops::ControlFlow, path::PathBuf, sync::Arc};
36use terminal_view::{terminal_panel::TerminalPanel, TerminalView};
37use ui::{prelude::*, ContextMenu, ElevationIndex, PopoverMenu, PopoverMenuHandle, Tooltip};
38use util::{maybe, ResultExt};
39use workspace::DraggedTab;
40use workspace::{
41 dock::{DockPosition, Panel, PanelEvent},
42 item::Item,
43 pane, DraggedSelection, Pane, ShowConfiguration, ToggleZoom, Workspace,
44};
45use zed_actions::assistant::{InlineAssist, ToggleFocus};
46
47pub fn init(cx: &mut AppContext) {
48 workspace::FollowableViewRegistry::register::<ContextEditor>(cx);
49 cx.observe_new_views(
50 |workspace: &mut Workspace, _cx: &mut ViewContext<Workspace>| {
51 workspace
52 .register_action(ContextEditor::quote_selection)
53 .register_action(ContextEditor::insert_selection)
54 .register_action(ContextEditor::copy_code)
55 .register_action(ContextEditor::insert_dragged_files)
56 .register_action(AssistantPanel::show_configuration)
57 .register_action(AssistantPanel::create_new_context)
58 .register_action(AssistantPanel::restart_context_servers);
59 },
60 )
61 .detach();
62
63 cx.observe_new_views(
64 |terminal_panel: &mut TerminalPanel, cx: &mut ViewContext<TerminalPanel>| {
65 let settings = AssistantSettings::get_global(cx);
66 terminal_panel.set_assistant_enabled(settings.enabled, cx);
67 },
68 )
69 .detach();
70}
71
72pub enum AssistantPanelEvent {
73 ContextEdited,
74}
75
76pub struct AssistantPanel {
77 pane: View<Pane>,
78 workspace: WeakView<Workspace>,
79 width: Option<Pixels>,
80 height: Option<Pixels>,
81 project: Model<Project>,
82 context_store: Model<ContextStore>,
83 languages: Arc<LanguageRegistry>,
84 fs: Arc<dyn Fs>,
85 subscriptions: Vec<Subscription>,
86 model_selector_menu_handle: PopoverMenuHandle<LanguageModelSelector>,
87 model_summary_editor: View<Editor>,
88 authenticate_provider_task: Option<(LanguageModelProviderId, Task<()>)>,
89 configuration_subscription: Option<Subscription>,
90 client_status: Option<client::Status>,
91 watch_client_status: Option<Task<()>>,
92 pub(crate) show_zed_ai_notice: bool,
93}
94
95enum InlineAssistTarget {
96 Editor(View<Editor>, bool),
97 Terminal(View<TerminalView>),
98}
99
100impl AssistantPanel {
101 pub fn load(
102 workspace: WeakView<Workspace>,
103 prompt_builder: Arc<PromptBuilder>,
104 cx: AsyncWindowContext,
105 ) -> Task<Result<View<Self>>> {
106 cx.spawn(|mut cx| async move {
107 let slash_commands = Arc::new(SlashCommandWorkingSet::default());
108 let tools = Arc::new(ToolWorkingSet::default());
109 let context_store = workspace
110 .update(&mut cx, |workspace, cx| {
111 let project = workspace.project().clone();
112 ContextStore::new(project, prompt_builder.clone(), slash_commands, tools, cx)
113 })?
114 .await?;
115
116 workspace.update(&mut cx, |workspace, cx| {
117 // TODO: deserialize state.
118 cx.new_view(|cx| Self::new(workspace, context_store, cx))
119 })
120 })
121 }
122
123 fn new(
124 workspace: &Workspace,
125 context_store: Model<ContextStore>,
126 cx: &mut ViewContext<Self>,
127 ) -> Self {
128 let model_selector_menu_handle = PopoverMenuHandle::default();
129 let model_summary_editor = cx.new_view(Editor::single_line);
130 let context_editor_toolbar = cx.new_view(|cx| {
131 ContextEditorToolbarItem::new(
132 workspace,
133 model_selector_menu_handle.clone(),
134 model_summary_editor.clone(),
135 cx,
136 )
137 });
138
139 let pane = cx.new_view(|cx| {
140 let mut pane = Pane::new(
141 workspace.weak_handle(),
142 workspace.project().clone(),
143 Default::default(),
144 None,
145 NewContext.boxed_clone(),
146 cx,
147 );
148
149 let project = workspace.project().clone();
150 pane.set_custom_drop_handle(cx, move |_, dropped_item, cx| {
151 let action = maybe!({
152 if project.read(cx).is_local() {
153 if let Some(paths) = dropped_item.downcast_ref::<ExternalPaths>() {
154 return Some(InsertDraggedFiles::ExternalFiles(paths.paths().to_vec()));
155 }
156 }
157
158 let project_paths = if let Some(tab) = dropped_item.downcast_ref::<DraggedTab>()
159 {
160 if &tab.pane == cx.view() {
161 return None;
162 }
163 let item = tab.pane.read(cx).item_for_index(tab.ix);
164 Some(
165 item.and_then(|item| item.project_path(cx))
166 .into_iter()
167 .collect::<Vec<_>>(),
168 )
169 } else if let Some(selection) = dropped_item.downcast_ref::<DraggedSelection>()
170 {
171 Some(
172 selection
173 .items()
174 .filter_map(|item| {
175 project.read(cx).path_for_entry(item.entry_id, cx)
176 })
177 .collect::<Vec<_>>(),
178 )
179 } else {
180 None
181 }?;
182
183 let paths = project_paths
184 .into_iter()
185 .filter_map(|project_path| {
186 let worktree = project
187 .read(cx)
188 .worktree_for_id(project_path.worktree_id, cx)?;
189
190 let mut full_path = PathBuf::from(worktree.read(cx).root_name());
191 full_path.push(&project_path.path);
192 Some(full_path)
193 })
194 .collect::<Vec<_>>();
195
196 Some(InsertDraggedFiles::ProjectPaths(paths))
197 });
198
199 if let Some(action) = action {
200 cx.dispatch_action(action.boxed_clone());
201 }
202
203 ControlFlow::Break(())
204 });
205
206 pane.set_can_navigate(true, cx);
207 pane.display_nav_history_buttons(None);
208 pane.set_should_display_tab_bar(|_| true);
209 pane.set_render_tab_bar_buttons(cx, move |pane, cx| {
210 let focus_handle = pane.focus_handle(cx);
211 let left_children = IconButton::new("history", IconName::HistoryRerun)
212 .icon_size(IconSize::Small)
213 .on_click(cx.listener({
214 let focus_handle = focus_handle.clone();
215 move |_, _, cx| {
216 focus_handle.focus(cx);
217 cx.dispatch_action(DeployHistory.boxed_clone())
218 }
219 }))
220 .tooltip({
221 let focus_handle = focus_handle.clone();
222 move |cx| {
223 Tooltip::for_action_in(
224 "Open History",
225 &DeployHistory,
226 &focus_handle,
227 cx,
228 )
229 }
230 })
231 .toggle_state(
232 pane.active_item()
233 .map_or(false, |item| item.downcast::<ContextHistory>().is_some()),
234 );
235 let _pane = cx.view().clone();
236 let right_children = h_flex()
237 .gap(DynamicSpacing::Base02.rems(cx))
238 .child(
239 IconButton::new("new-chat", IconName::Plus)
240 .icon_size(IconSize::Small)
241 .on_click(
242 cx.listener(|_, _, cx| {
243 cx.dispatch_action(NewContext.boxed_clone())
244 }),
245 )
246 .tooltip(move |cx| {
247 Tooltip::for_action_in("New Chat", &NewContext, &focus_handle, cx)
248 }),
249 )
250 .child(
251 PopoverMenu::new("assistant-panel-popover-menu")
252 .trigger(
253 IconButton::new("menu", IconName::EllipsisVertical)
254 .icon_size(IconSize::Small)
255 .tooltip(|cx| Tooltip::text("Toggle Assistant Menu", cx)),
256 )
257 .menu(move |cx| {
258 let zoom_label = if _pane.read(cx).is_zoomed() {
259 "Zoom Out"
260 } else {
261 "Zoom In"
262 };
263 let focus_handle = _pane.focus_handle(cx);
264 Some(ContextMenu::build(cx, move |menu, _| {
265 menu.context(focus_handle.clone())
266 .action("New Chat", Box::new(NewContext))
267 .action("History", Box::new(DeployHistory))
268 .action("Prompt Library", Box::new(DeployPromptLibrary))
269 .action("Configure", Box::new(ShowConfiguration))
270 .action(zoom_label, Box::new(ToggleZoom))
271 }))
272 }),
273 )
274 .into_any_element()
275 .into();
276
277 (Some(left_children.into_any_element()), right_children)
278 });
279 pane.toolbar().update(cx, |toolbar, cx| {
280 toolbar.add_item(context_editor_toolbar.clone(), cx);
281 toolbar.add_item(cx.new_view(BufferSearchBar::new), cx)
282 });
283 pane
284 });
285
286 let subscriptions = vec![
287 cx.observe(&pane, |_, _, cx| cx.notify()),
288 cx.subscribe(&pane, Self::handle_pane_event),
289 cx.subscribe(&context_editor_toolbar, Self::handle_toolbar_event),
290 cx.subscribe(&model_summary_editor, Self::handle_summary_editor_event),
291 cx.subscribe(&context_store, Self::handle_context_store_event),
292 cx.subscribe(
293 &LanguageModelRegistry::global(cx),
294 |this, _, event: &language_model::Event, cx| match event {
295 language_model::Event::ActiveModelChanged => {
296 this.completion_provider_changed(cx);
297 }
298 language_model::Event::ProviderStateChanged => {
299 this.ensure_authenticated(cx);
300 cx.notify()
301 }
302 language_model::Event::AddedProvider(_)
303 | language_model::Event::RemovedProvider(_) => {
304 this.ensure_authenticated(cx);
305 }
306 },
307 ),
308 ];
309
310 let watch_client_status = Self::watch_client_status(workspace.client().clone(), cx);
311
312 let mut this = Self {
313 pane,
314 workspace: workspace.weak_handle(),
315 width: None,
316 height: None,
317 project: workspace.project().clone(),
318 context_store,
319 languages: workspace.app_state().languages.clone(),
320 fs: workspace.app_state().fs.clone(),
321 subscriptions,
322 model_selector_menu_handle,
323 model_summary_editor,
324 authenticate_provider_task: None,
325 configuration_subscription: None,
326 client_status: None,
327 watch_client_status: Some(watch_client_status),
328 show_zed_ai_notice: false,
329 };
330 this.new_context(cx);
331 this
332 }
333
334 pub fn toggle_focus(
335 workspace: &mut Workspace,
336 _: &ToggleFocus,
337 cx: &mut ViewContext<Workspace>,
338 ) {
339 let settings = AssistantSettings::get_global(cx);
340 if !settings.enabled {
341 return;
342 }
343
344 workspace.toggle_panel_focus::<Self>(cx);
345 }
346
347 fn watch_client_status(client: Arc<Client>, cx: &mut ViewContext<Self>) -> Task<()> {
348 let mut status_rx = client.status();
349
350 cx.spawn(|this, mut cx| async move {
351 while let Some(status) = status_rx.next().await {
352 this.update(&mut cx, |this, cx| {
353 if this.client_status.is_none()
354 || this
355 .client_status
356 .map_or(false, |old_status| old_status != status)
357 {
358 this.update_zed_ai_notice_visibility(status, cx);
359 }
360 this.client_status = Some(status);
361 })
362 .log_err();
363 }
364 this.update(&mut cx, |this, _cx| this.watch_client_status = None)
365 .log_err();
366 })
367 }
368
369 fn handle_pane_event(
370 &mut self,
371 pane: View<Pane>,
372 event: &pane::Event,
373 cx: &mut ViewContext<Self>,
374 ) {
375 let update_model_summary = match event {
376 pane::Event::Remove { .. } => {
377 cx.emit(PanelEvent::Close);
378 false
379 }
380 pane::Event::ZoomIn => {
381 cx.emit(PanelEvent::ZoomIn);
382 false
383 }
384 pane::Event::ZoomOut => {
385 cx.emit(PanelEvent::ZoomOut);
386 false
387 }
388
389 pane::Event::AddItem { item } => {
390 self.workspace
391 .update(cx, |workspace, cx| {
392 item.added_to_pane(workspace, self.pane.clone(), cx)
393 })
394 .ok();
395 true
396 }
397
398 pane::Event::ActivateItem { local, .. } => {
399 if *local {
400 self.workspace
401 .update(cx, |workspace, cx| {
402 workspace.unfollow_in_pane(&pane, cx);
403 })
404 .ok();
405 }
406 cx.emit(AssistantPanelEvent::ContextEdited);
407 true
408 }
409 pane::Event::RemovedItem { .. } => {
410 let has_configuration_view = self
411 .pane
412 .read(cx)
413 .items_of_type::<ConfigurationView>()
414 .next()
415 .is_some();
416
417 if !has_configuration_view {
418 self.configuration_subscription = None;
419 }
420
421 cx.emit(AssistantPanelEvent::ContextEdited);
422 true
423 }
424
425 _ => false,
426 };
427
428 if update_model_summary {
429 if let Some(editor) = self.active_context_editor(cx) {
430 self.show_updated_summary(&editor, cx)
431 }
432 }
433 }
434
435 fn handle_summary_editor_event(
436 &mut self,
437 model_summary_editor: View<Editor>,
438 event: &EditorEvent,
439 cx: &mut ViewContext<Self>,
440 ) {
441 if matches!(event, EditorEvent::Edited { .. }) {
442 if let Some(context_editor) = self.active_context_editor(cx) {
443 let new_summary = model_summary_editor.read(cx).text(cx);
444 context_editor.update(cx, |context_editor, cx| {
445 context_editor.context().update(cx, |context, cx| {
446 if context.summary().is_none()
447 && (new_summary == DEFAULT_TAB_TITLE || new_summary.trim().is_empty())
448 {
449 return;
450 }
451 context.custom_summary(new_summary, cx)
452 });
453 });
454 }
455 }
456 }
457
458 fn update_zed_ai_notice_visibility(
459 &mut self,
460 client_status: Status,
461 cx: &mut ViewContext<Self>,
462 ) {
463 let active_provider = LanguageModelRegistry::read_global(cx).active_provider();
464
465 // If we're signed out and don't have a provider configured, or we're signed-out AND Zed.dev is
466 // the provider, we want to show a nudge to sign in.
467 let show_zed_ai_notice = client_status.is_signed_out()
468 && active_provider.map_or(true, |provider| provider.id().0 == ZED_CLOUD_PROVIDER_ID);
469
470 self.show_zed_ai_notice = show_zed_ai_notice;
471 cx.notify();
472 }
473
474 fn handle_toolbar_event(
475 &mut self,
476 _: View<ContextEditorToolbarItem>,
477 _: &ContextEditorToolbarItemEvent,
478 cx: &mut ViewContext<Self>,
479 ) {
480 if let Some(context_editor) = self.active_context_editor(cx) {
481 context_editor.update(cx, |context_editor, cx| {
482 context_editor.context().update(cx, |context, cx| {
483 context.summarize(true, cx);
484 })
485 })
486 }
487 }
488
489 fn handle_context_store_event(
490 &mut self,
491 _context_store: Model<ContextStore>,
492 event: &ContextStoreEvent,
493 cx: &mut ViewContext<Self>,
494 ) {
495 let ContextStoreEvent::ContextCreated(context_id) = event;
496 let Some(context) = self
497 .context_store
498 .read(cx)
499 .loaded_context_for_id(&context_id, cx)
500 else {
501 log::error!("no context found with ID: {}", context_id.to_proto());
502 return;
503 };
504 let lsp_adapter_delegate = make_lsp_adapter_delegate(&self.project, cx)
505 .log_err()
506 .flatten();
507
508 let editor = cx.new_view(|cx| {
509 let mut editor = ContextEditor::for_context(
510 context,
511 self.fs.clone(),
512 self.workspace.clone(),
513 self.project.clone(),
514 lsp_adapter_delegate,
515 cx,
516 );
517 editor.insert_default_prompt(cx);
518 editor
519 });
520
521 self.show_context(editor.clone(), cx);
522 }
523
524 fn completion_provider_changed(&mut self, cx: &mut ViewContext<Self>) {
525 if let Some(editor) = self.active_context_editor(cx) {
526 editor.update(cx, |active_context, cx| {
527 active_context
528 .context()
529 .update(cx, |context, cx| context.completion_provider_changed(cx))
530 })
531 }
532
533 let Some(new_provider_id) = LanguageModelRegistry::read_global(cx)
534 .active_provider()
535 .map(|p| p.id())
536 else {
537 return;
538 };
539
540 if self
541 .authenticate_provider_task
542 .as_ref()
543 .map_or(true, |(old_provider_id, _)| {
544 *old_provider_id != new_provider_id
545 })
546 {
547 self.authenticate_provider_task = None;
548 self.ensure_authenticated(cx);
549 }
550
551 if let Some(status) = self.client_status {
552 self.update_zed_ai_notice_visibility(status, cx);
553 }
554 }
555
556 fn ensure_authenticated(&mut self, cx: &mut ViewContext<Self>) {
557 if self.is_authenticated(cx) {
558 return;
559 }
560
561 let Some(provider) = LanguageModelRegistry::read_global(cx).active_provider() else {
562 return;
563 };
564
565 let load_credentials = self.authenticate(cx);
566
567 if self.authenticate_provider_task.is_none() {
568 self.authenticate_provider_task = Some((
569 provider.id(),
570 cx.spawn(|this, mut cx| async move {
571 if let Some(future) = load_credentials {
572 let _ = future.await;
573 }
574 this.update(&mut cx, |this, _cx| {
575 this.authenticate_provider_task = None;
576 })
577 .log_err();
578 }),
579 ));
580 }
581 }
582
583 pub fn inline_assist(
584 workspace: &mut Workspace,
585 action: &InlineAssist,
586 cx: &mut ViewContext<Workspace>,
587 ) {
588 let settings = AssistantSettings::get_global(cx);
589 if !settings.enabled {
590 return;
591 }
592
593 let Some(assistant_panel) = workspace.panel::<AssistantPanel>(cx) else {
594 return;
595 };
596
597 let Some(inline_assist_target) =
598 Self::resolve_inline_assist_target(workspace, &assistant_panel, cx)
599 else {
600 return;
601 };
602
603 let initial_prompt = action.prompt.clone();
604
605 if assistant_panel.update(cx, |assistant, cx| assistant.is_authenticated(cx)) {
606 match inline_assist_target {
607 InlineAssistTarget::Editor(active_editor, include_context) => {
608 InlineAssistant::update_global(cx, |assistant, cx| {
609 assistant.assist(
610 &active_editor,
611 Some(cx.view().downgrade()),
612 include_context.then_some(&assistant_panel),
613 initial_prompt,
614 cx,
615 )
616 })
617 }
618 InlineAssistTarget::Terminal(active_terminal) => {
619 TerminalInlineAssistant::update_global(cx, |assistant, cx| {
620 assistant.assist(
621 &active_terminal,
622 Some(cx.view().downgrade()),
623 Some(&assistant_panel),
624 initial_prompt,
625 cx,
626 )
627 })
628 }
629 }
630 } else {
631 let assistant_panel = assistant_panel.downgrade();
632 cx.spawn(|workspace, mut cx| async move {
633 let Some(task) =
634 assistant_panel.update(&mut cx, |assistant, cx| assistant.authenticate(cx))?
635 else {
636 let answer = cx
637 .prompt(
638 gpui::PromptLevel::Warning,
639 "No language model provider configured",
640 None,
641 &["Configure", "Cancel"],
642 )
643 .await
644 .ok();
645 if let Some(answer) = answer {
646 if answer == 0 {
647 cx.update(|cx| cx.dispatch_action(Box::new(ShowConfiguration)))
648 .ok();
649 }
650 }
651 return Ok(());
652 };
653 task.await?;
654 if assistant_panel.update(&mut cx, |panel, cx| panel.is_authenticated(cx))? {
655 cx.update(|cx| match inline_assist_target {
656 InlineAssistTarget::Editor(active_editor, include_context) => {
657 let assistant_panel = if include_context {
658 assistant_panel.upgrade()
659 } else {
660 None
661 };
662 InlineAssistant::update_global(cx, |assistant, cx| {
663 assistant.assist(
664 &active_editor,
665 Some(workspace),
666 assistant_panel.as_ref(),
667 initial_prompt,
668 cx,
669 )
670 })
671 }
672 InlineAssistTarget::Terminal(active_terminal) => {
673 TerminalInlineAssistant::update_global(cx, |assistant, cx| {
674 assistant.assist(
675 &active_terminal,
676 Some(workspace),
677 assistant_panel.upgrade().as_ref(),
678 initial_prompt,
679 cx,
680 )
681 })
682 }
683 })?
684 } else {
685 workspace.update(&mut cx, |workspace, cx| {
686 workspace.focus_panel::<AssistantPanel>(cx)
687 })?;
688 }
689
690 anyhow::Ok(())
691 })
692 .detach_and_log_err(cx)
693 }
694 }
695
696 fn resolve_inline_assist_target(
697 workspace: &mut Workspace,
698 assistant_panel: &View<AssistantPanel>,
699 cx: &mut WindowContext,
700 ) -> Option<InlineAssistTarget> {
701 if let Some(terminal_panel) = workspace.panel::<TerminalPanel>(cx) {
702 if terminal_panel
703 .read(cx)
704 .focus_handle(cx)
705 .contains_focused(cx)
706 {
707 if let Some(terminal_view) = terminal_panel.read(cx).pane().and_then(|pane| {
708 pane.read(cx)
709 .active_item()
710 .and_then(|t| t.downcast::<TerminalView>())
711 }) {
712 return Some(InlineAssistTarget::Terminal(terminal_view));
713 }
714 }
715 }
716 let context_editor =
717 assistant_panel
718 .read(cx)
719 .active_context_editor(cx)
720 .and_then(|editor| {
721 let editor = &editor.read(cx).editor().clone();
722 if editor.read(cx).is_focused(cx) {
723 Some(editor.clone())
724 } else {
725 None
726 }
727 });
728
729 if let Some(context_editor) = context_editor {
730 Some(InlineAssistTarget::Editor(context_editor, false))
731 } else if let Some(workspace_editor) = workspace
732 .active_item(cx)
733 .and_then(|item| item.act_as::<Editor>(cx))
734 {
735 Some(InlineAssistTarget::Editor(workspace_editor, true))
736 } else if let Some(terminal_view) = workspace
737 .active_item(cx)
738 .and_then(|item| item.act_as::<TerminalView>(cx))
739 {
740 Some(InlineAssistTarget::Terminal(terminal_view))
741 } else {
742 None
743 }
744 }
745
746 pub fn create_new_context(
747 workspace: &mut Workspace,
748 _: &NewContext,
749 cx: &mut ViewContext<Workspace>,
750 ) {
751 if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
752 let did_create_context = panel
753 .update(cx, |panel, cx| {
754 panel.new_context(cx)?;
755
756 Some(())
757 })
758 .is_some();
759 if did_create_context {
760 ContextEditor::quote_selection(workspace, &Default::default(), cx);
761 }
762 }
763 }
764
765 pub fn new_context(&mut self, cx: &mut ViewContext<Self>) -> Option<View<ContextEditor>> {
766 let project = self.project.read(cx);
767 if project.is_via_collab() {
768 let task = self
769 .context_store
770 .update(cx, |store, cx| store.create_remote_context(cx));
771
772 cx.spawn(|this, mut cx| async move {
773 let context = task.await?;
774
775 this.update(&mut cx, |this, cx| {
776 let workspace = this.workspace.clone();
777 let project = this.project.clone();
778 let lsp_adapter_delegate =
779 make_lsp_adapter_delegate(&project, cx).log_err().flatten();
780
781 let fs = this.fs.clone();
782 let project = this.project.clone();
783
784 let editor = cx.new_view(|cx| {
785 ContextEditor::for_context(
786 context,
787 fs,
788 workspace,
789 project,
790 lsp_adapter_delegate,
791 cx,
792 )
793 });
794
795 this.show_context(editor, cx);
796
797 anyhow::Ok(())
798 })??;
799
800 anyhow::Ok(())
801 })
802 .detach_and_log_err(cx);
803
804 None
805 } else {
806 let context = self.context_store.update(cx, |store, cx| store.create(cx));
807 let lsp_adapter_delegate = make_lsp_adapter_delegate(&self.project, cx)
808 .log_err()
809 .flatten();
810
811 let editor = cx.new_view(|cx| {
812 let mut editor = ContextEditor::for_context(
813 context,
814 self.fs.clone(),
815 self.workspace.clone(),
816 self.project.clone(),
817 lsp_adapter_delegate,
818 cx,
819 );
820 editor.insert_default_prompt(cx);
821 editor
822 });
823
824 self.show_context(editor.clone(), cx);
825 let workspace = self.workspace.clone();
826 cx.spawn(move |_, mut cx| async move {
827 workspace
828 .update(&mut cx, |workspace, cx| {
829 workspace.focus_panel::<AssistantPanel>(cx);
830 })
831 .ok();
832 })
833 .detach();
834 Some(editor)
835 }
836 }
837
838 fn show_context(&mut self, context_editor: View<ContextEditor>, cx: &mut ViewContext<Self>) {
839 let focus = self.focus_handle(cx).contains_focused(cx);
840 let prev_len = self.pane.read(cx).items_len();
841 self.pane.update(cx, |pane, cx| {
842 pane.add_item(Box::new(context_editor.clone()), focus, focus, None, cx)
843 });
844
845 if prev_len != self.pane.read(cx).items_len() {
846 self.subscriptions
847 .push(cx.subscribe(&context_editor, Self::handle_context_editor_event));
848 }
849
850 self.show_updated_summary(&context_editor, cx);
851
852 cx.emit(AssistantPanelEvent::ContextEdited);
853 cx.notify();
854 }
855
856 fn show_updated_summary(
857 &self,
858 context_editor: &View<ContextEditor>,
859 cx: &mut ViewContext<Self>,
860 ) {
861 context_editor.update(cx, |context_editor, cx| {
862 let new_summary = context_editor.title(cx).to_string();
863 self.model_summary_editor.update(cx, |summary_editor, cx| {
864 if summary_editor.text(cx) != new_summary {
865 summary_editor.set_text(new_summary, cx);
866 }
867 });
868 });
869 }
870
871 fn handle_context_editor_event(
872 &mut self,
873 context_editor: View<ContextEditor>,
874 event: &EditorEvent,
875 cx: &mut ViewContext<Self>,
876 ) {
877 match event {
878 EditorEvent::TitleChanged => {
879 self.show_updated_summary(&context_editor, cx);
880 cx.notify()
881 }
882 EditorEvent::Edited { .. } => {
883 self.workspace
884 .update(cx, |workspace, cx| {
885 let is_via_ssh = workspace
886 .project()
887 .update(cx, |project, _| project.is_via_ssh());
888
889 workspace
890 .client()
891 .telemetry()
892 .log_edit_event("assistant panel", is_via_ssh);
893 })
894 .log_err();
895 cx.emit(AssistantPanelEvent::ContextEdited)
896 }
897 _ => {}
898 }
899 }
900
901 fn show_configuration(
902 workspace: &mut Workspace,
903 _: &ShowConfiguration,
904 cx: &mut ViewContext<Workspace>,
905 ) {
906 let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
907 return;
908 };
909
910 if !panel.focus_handle(cx).contains_focused(cx) {
911 workspace.toggle_panel_focus::<AssistantPanel>(cx);
912 }
913
914 panel.update(cx, |this, cx| {
915 this.show_configuration_tab(cx);
916 })
917 }
918
919 fn show_configuration_tab(&mut self, cx: &mut ViewContext<Self>) {
920 let configuration_item_ix = self
921 .pane
922 .read(cx)
923 .items()
924 .position(|item| item.downcast::<ConfigurationView>().is_some());
925
926 if let Some(configuration_item_ix) = configuration_item_ix {
927 self.pane.update(cx, |pane, cx| {
928 pane.activate_item(configuration_item_ix, true, true, cx);
929 });
930 } else {
931 let configuration = cx.new_view(ConfigurationView::new);
932 self.configuration_subscription = Some(cx.subscribe(
933 &configuration,
934 |this, _, event: &ConfigurationViewEvent, cx| match event {
935 ConfigurationViewEvent::NewProviderContextEditor(provider) => {
936 if LanguageModelRegistry::read_global(cx)
937 .active_provider()
938 .map_or(true, |p| p.id() != provider.id())
939 {
940 if let Some(model) = provider.provided_models(cx).first().cloned() {
941 update_settings_file::<AssistantSettings>(
942 this.fs.clone(),
943 cx,
944 move |settings, _| settings.set_model(model),
945 );
946 }
947 }
948
949 this.new_context(cx);
950 }
951 },
952 ));
953 self.pane.update(cx, |pane, cx| {
954 pane.add_item(Box::new(configuration), true, true, None, cx);
955 });
956 }
957 }
958
959 fn deploy_history(&mut self, _: &DeployHistory, cx: &mut ViewContext<Self>) {
960 let history_item_ix = self
961 .pane
962 .read(cx)
963 .items()
964 .position(|item| item.downcast::<ContextHistory>().is_some());
965
966 if let Some(history_item_ix) = history_item_ix {
967 self.pane.update(cx, |pane, cx| {
968 pane.activate_item(history_item_ix, true, true, cx);
969 });
970 } else {
971 let history = cx.new_view(|cx| {
972 ContextHistory::new(
973 self.project.clone(),
974 self.context_store.clone(),
975 self.workspace.clone(),
976 cx,
977 )
978 });
979 self.pane.update(cx, |pane, cx| {
980 pane.add_item(Box::new(history), true, true, None, cx);
981 });
982 }
983 }
984
985 fn deploy_prompt_library(&mut self, _: &DeployPromptLibrary, cx: &mut ViewContext<Self>) {
986 open_prompt_library(
987 self.languages.clone(),
988 Box::new(PromptLibraryInlineAssist),
989 Arc::new(|| {
990 Box::new(SlashCommandCompletionProvider::new(
991 Arc::new(SlashCommandWorkingSet::default()),
992 None,
993 None,
994 ))
995 }),
996 cx,
997 )
998 .detach_and_log_err(cx);
999 }
1000
1001 fn toggle_model_selector(&mut self, _: &ToggleModelSelector, cx: &mut ViewContext<Self>) {
1002 self.model_selector_menu_handle.toggle(cx);
1003 }
1004
1005 pub(crate) fn active_context_editor(&self, cx: &AppContext) -> Option<View<ContextEditor>> {
1006 self.pane
1007 .read(cx)
1008 .active_item()?
1009 .downcast::<ContextEditor>()
1010 }
1011
1012 pub fn active_context(&self, cx: &AppContext) -> Option<Model<Context>> {
1013 Some(self.active_context_editor(cx)?.read(cx).context().clone())
1014 }
1015
1016 pub fn open_saved_context(
1017 &mut self,
1018 path: PathBuf,
1019 cx: &mut ViewContext<Self>,
1020 ) -> Task<Result<()>> {
1021 let existing_context = self.pane.read(cx).items().find_map(|item| {
1022 item.downcast::<ContextEditor>()
1023 .filter(|editor| editor.read(cx).context().read(cx).path() == Some(&path))
1024 });
1025 if let Some(existing_context) = existing_context {
1026 return cx.spawn(|this, mut cx| async move {
1027 this.update(&mut cx, |this, cx| this.show_context(existing_context, cx))
1028 });
1029 }
1030
1031 let context = self
1032 .context_store
1033 .update(cx, |store, cx| store.open_local_context(path.clone(), cx));
1034 let fs = self.fs.clone();
1035 let project = self.project.clone();
1036 let workspace = self.workspace.clone();
1037
1038 let lsp_adapter_delegate = make_lsp_adapter_delegate(&project, cx).log_err().flatten();
1039
1040 cx.spawn(|this, mut cx| async move {
1041 let context = context.await?;
1042 this.update(&mut cx, |this, cx| {
1043 let editor = cx.new_view(|cx| {
1044 ContextEditor::for_context(
1045 context,
1046 fs,
1047 workspace,
1048 project,
1049 lsp_adapter_delegate,
1050 cx,
1051 )
1052 });
1053 this.show_context(editor, cx);
1054 anyhow::Ok(())
1055 })??;
1056 Ok(())
1057 })
1058 }
1059
1060 pub fn open_remote_context(
1061 &mut self,
1062 id: ContextId,
1063 cx: &mut ViewContext<Self>,
1064 ) -> Task<Result<View<ContextEditor>>> {
1065 let existing_context = self.pane.read(cx).items().find_map(|item| {
1066 item.downcast::<ContextEditor>()
1067 .filter(|editor| *editor.read(cx).context().read(cx).id() == id)
1068 });
1069 if let Some(existing_context) = existing_context {
1070 return cx.spawn(|this, mut cx| async move {
1071 this.update(&mut cx, |this, cx| {
1072 this.show_context(existing_context.clone(), cx)
1073 })?;
1074 Ok(existing_context)
1075 });
1076 }
1077
1078 let context = self
1079 .context_store
1080 .update(cx, |store, cx| store.open_remote_context(id, cx));
1081 let fs = self.fs.clone();
1082 let workspace = self.workspace.clone();
1083 let lsp_adapter_delegate = make_lsp_adapter_delegate(&self.project, cx)
1084 .log_err()
1085 .flatten();
1086
1087 cx.spawn(|this, mut cx| async move {
1088 let context = context.await?;
1089 this.update(&mut cx, |this, cx| {
1090 let editor = cx.new_view(|cx| {
1091 ContextEditor::for_context(
1092 context,
1093 fs,
1094 workspace,
1095 this.project.clone(),
1096 lsp_adapter_delegate,
1097 cx,
1098 )
1099 });
1100 this.show_context(editor.clone(), cx);
1101 anyhow::Ok(editor)
1102 })?
1103 })
1104 }
1105
1106 fn is_authenticated(&mut self, cx: &mut ViewContext<Self>) -> bool {
1107 LanguageModelRegistry::read_global(cx)
1108 .active_provider()
1109 .map_or(false, |provider| provider.is_authenticated(cx))
1110 }
1111
1112 fn authenticate(&mut self, cx: &mut ViewContext<Self>) -> Option<Task<Result<()>>> {
1113 LanguageModelRegistry::read_global(cx)
1114 .active_provider()
1115 .map_or(None, |provider| Some(provider.authenticate(cx)))
1116 }
1117
1118 fn restart_context_servers(
1119 workspace: &mut Workspace,
1120 _action: &context_server::Restart,
1121 cx: &mut ViewContext<Workspace>,
1122 ) {
1123 let Some(assistant_panel) = workspace.panel::<AssistantPanel>(cx) else {
1124 return;
1125 };
1126
1127 assistant_panel.update(cx, |assistant_panel, cx| {
1128 assistant_panel
1129 .context_store
1130 .update(cx, |context_store, cx| {
1131 context_store.restart_context_servers(cx);
1132 });
1133 });
1134 }
1135}
1136
1137impl Render for AssistantPanel {
1138 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
1139 let mut registrar = DivRegistrar::new(
1140 |panel, cx| {
1141 panel
1142 .pane
1143 .read(cx)
1144 .toolbar()
1145 .read(cx)
1146 .item_of_type::<BufferSearchBar>()
1147 },
1148 cx,
1149 );
1150 BufferSearchBar::register(&mut registrar);
1151 let registrar = registrar.into_div();
1152
1153 v_flex()
1154 .key_context("AssistantPanel")
1155 .size_full()
1156 .on_action(cx.listener(|this, _: &NewContext, cx| {
1157 this.new_context(cx);
1158 }))
1159 .on_action(
1160 cx.listener(|this, _: &ShowConfiguration, cx| this.show_configuration_tab(cx)),
1161 )
1162 .on_action(cx.listener(AssistantPanel::deploy_history))
1163 .on_action(cx.listener(AssistantPanel::deploy_prompt_library))
1164 .on_action(cx.listener(AssistantPanel::toggle_model_selector))
1165 .child(registrar.size_full().child(self.pane.clone()))
1166 .into_any_element()
1167 }
1168}
1169
1170impl Panel for AssistantPanel {
1171 fn persistent_name() -> &'static str {
1172 "AssistantPanel"
1173 }
1174
1175 fn position(&self, cx: &WindowContext) -> DockPosition {
1176 match AssistantSettings::get_global(cx).dock {
1177 AssistantDockPosition::Left => DockPosition::Left,
1178 AssistantDockPosition::Bottom => DockPosition::Bottom,
1179 AssistantDockPosition::Right => DockPosition::Right,
1180 }
1181 }
1182
1183 fn position_is_valid(&self, _: DockPosition) -> bool {
1184 true
1185 }
1186
1187 fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
1188 settings::update_settings_file::<AssistantSettings>(
1189 self.fs.clone(),
1190 cx,
1191 move |settings, _| {
1192 let dock = match position {
1193 DockPosition::Left => AssistantDockPosition::Left,
1194 DockPosition::Bottom => AssistantDockPosition::Bottom,
1195 DockPosition::Right => AssistantDockPosition::Right,
1196 };
1197 settings.set_dock(dock);
1198 },
1199 );
1200 }
1201
1202 fn size(&self, cx: &WindowContext) -> Pixels {
1203 let settings = AssistantSettings::get_global(cx);
1204 match self.position(cx) {
1205 DockPosition::Left | DockPosition::Right => {
1206 self.width.unwrap_or(settings.default_width)
1207 }
1208 DockPosition::Bottom => self.height.unwrap_or(settings.default_height),
1209 }
1210 }
1211
1212 fn set_size(&mut self, size: Option<Pixels>, cx: &mut ViewContext<Self>) {
1213 match self.position(cx) {
1214 DockPosition::Left | DockPosition::Right => self.width = size,
1215 DockPosition::Bottom => self.height = size,
1216 }
1217 cx.notify();
1218 }
1219
1220 fn is_zoomed(&self, cx: &WindowContext) -> bool {
1221 self.pane.read(cx).is_zoomed()
1222 }
1223
1224 fn set_zoomed(&mut self, zoomed: bool, cx: &mut ViewContext<Self>) {
1225 self.pane.update(cx, |pane, cx| pane.set_zoomed(zoomed, cx));
1226 }
1227
1228 fn set_active(&mut self, active: bool, cx: &mut ViewContext<Self>) {
1229 if active {
1230 if self.pane.read(cx).items_len() == 0 {
1231 self.new_context(cx);
1232 }
1233
1234 self.ensure_authenticated(cx);
1235 }
1236 }
1237
1238 fn pane(&self) -> Option<View<Pane>> {
1239 Some(self.pane.clone())
1240 }
1241
1242 fn remote_id() -> Option<proto::PanelId> {
1243 Some(proto::PanelId::AssistantPanel)
1244 }
1245
1246 fn icon(&self, cx: &WindowContext) -> Option<IconName> {
1247 let settings = AssistantSettings::get_global(cx);
1248 if !settings.enabled || !settings.button {
1249 return None;
1250 }
1251
1252 Some(IconName::ZedAssistant)
1253 }
1254
1255 fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {
1256 Some("Assistant Panel")
1257 }
1258
1259 fn toggle_action(&self) -> Box<dyn Action> {
1260 Box::new(ToggleFocus)
1261 }
1262
1263 fn activation_priority(&self) -> u32 {
1264 4
1265 }
1266}
1267
1268impl EventEmitter<PanelEvent> for AssistantPanel {}
1269impl EventEmitter<AssistantPanelEvent> for AssistantPanel {}
1270
1271impl FocusableView for AssistantPanel {
1272 fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
1273 self.pane.focus_handle(cx)
1274 }
1275}
1276
1277struct PromptLibraryInlineAssist;
1278
1279impl prompt_library::InlineAssistDelegate for PromptLibraryInlineAssist {
1280 fn assist(
1281 &self,
1282 prompt_editor: &View<Editor>,
1283 initial_prompt: Option<String>,
1284 cx: &mut ViewContext<PromptLibrary>,
1285 ) {
1286 InlineAssistant::update_global(cx, |assistant, cx| {
1287 assistant.assist(&prompt_editor, None, None, initial_prompt, cx)
1288 })
1289 }
1290
1291 fn focus_assistant_panel(
1292 &self,
1293 workspace: &mut Workspace,
1294 cx: &mut ViewContext<Workspace>,
1295 ) -> bool {
1296 workspace.focus_panel::<AssistantPanel>(cx).is_some()
1297 }
1298}
1299
1300pub struct ConcreteAssistantPanelDelegate;
1301
1302impl AssistantPanelDelegate for ConcreteAssistantPanelDelegate {
1303 fn active_context_editor(
1304 &self,
1305 workspace: &mut Workspace,
1306 cx: &mut ViewContext<Workspace>,
1307 ) -> Option<View<ContextEditor>> {
1308 let panel = workspace.panel::<AssistantPanel>(cx)?;
1309 panel.read(cx).active_context_editor(cx)
1310 }
1311
1312 fn open_saved_context(
1313 &self,
1314 workspace: &mut Workspace,
1315 path: PathBuf,
1316 cx: &mut ViewContext<Workspace>,
1317 ) -> Task<Result<()>> {
1318 let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
1319 return Task::ready(Err(anyhow!("no Assistant panel found")));
1320 };
1321
1322 panel.update(cx, |panel, cx| panel.open_saved_context(path, cx))
1323 }
1324
1325 fn open_remote_context(
1326 &self,
1327 workspace: &mut Workspace,
1328 context_id: ContextId,
1329 cx: &mut ViewContext<Workspace>,
1330 ) -> Task<Result<View<ContextEditor>>> {
1331 let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
1332 return Task::ready(Err(anyhow!("no Assistant panel found")));
1333 };
1334
1335 panel.update(cx, |panel, cx| panel.open_remote_context(context_id, cx))
1336 }
1337
1338 fn quote_selection(
1339 &self,
1340 workspace: &mut Workspace,
1341 creases: Vec<(String, String)>,
1342 cx: &mut ViewContext<Workspace>,
1343 ) {
1344 let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
1345 return;
1346 };
1347
1348 if !panel.focus_handle(cx).contains_focused(cx) {
1349 workspace.toggle_panel_focus::<AssistantPanel>(cx);
1350 }
1351
1352 panel.update(cx, |_, cx| {
1353 // Wait to create a new context until the workspace is no longer
1354 // being updated.
1355 cx.defer(move |panel, cx| {
1356 if let Some(context) = panel
1357 .active_context_editor(cx)
1358 .or_else(|| panel.new_context(cx))
1359 {
1360 context.update(cx, |context, cx| context.quote_creases(creases, cx));
1361 };
1362 });
1363 });
1364 }
1365}
1366
1367#[derive(Debug, PartialEq, Eq, Clone, Copy)]
1368pub enum WorkflowAssistStatus {
1369 Pending,
1370 Confirmed,
1371 Done,
1372 Idle,
1373}
1374
1375pub struct ConfigurationView {
1376 focus_handle: FocusHandle,
1377 configuration_views: HashMap<LanguageModelProviderId, AnyView>,
1378 _registry_subscription: Subscription,
1379}
1380
1381impl ConfigurationView {
1382 fn new(cx: &mut ViewContext<Self>) -> Self {
1383 let focus_handle = cx.focus_handle();
1384
1385 let registry_subscription = cx.subscribe(
1386 &LanguageModelRegistry::global(cx),
1387 |this, _, event: &language_model::Event, cx| match event {
1388 language_model::Event::AddedProvider(provider_id) => {
1389 let provider = LanguageModelRegistry::read_global(cx).provider(provider_id);
1390 if let Some(provider) = provider {
1391 this.add_configuration_view(&provider, cx);
1392 }
1393 }
1394 language_model::Event::RemovedProvider(provider_id) => {
1395 this.remove_configuration_view(provider_id);
1396 }
1397 _ => {}
1398 },
1399 );
1400
1401 let mut this = Self {
1402 focus_handle,
1403 configuration_views: HashMap::default(),
1404 _registry_subscription: registry_subscription,
1405 };
1406 this.build_configuration_views(cx);
1407 this
1408 }
1409
1410 fn build_configuration_views(&mut self, cx: &mut ViewContext<Self>) {
1411 let providers = LanguageModelRegistry::read_global(cx).providers();
1412 for provider in providers {
1413 self.add_configuration_view(&provider, cx);
1414 }
1415 }
1416
1417 fn remove_configuration_view(&mut self, provider_id: &LanguageModelProviderId) {
1418 self.configuration_views.remove(provider_id);
1419 }
1420
1421 fn add_configuration_view(
1422 &mut self,
1423 provider: &Arc<dyn LanguageModelProvider>,
1424 cx: &mut ViewContext<Self>,
1425 ) {
1426 let configuration_view = provider.configuration_view(cx);
1427 self.configuration_views
1428 .insert(provider.id(), configuration_view);
1429 }
1430
1431 fn render_provider_view(
1432 &mut self,
1433 provider: &Arc<dyn LanguageModelProvider>,
1434 cx: &mut ViewContext<Self>,
1435 ) -> Div {
1436 let provider_id = provider.id().0.clone();
1437 let provider_name = provider.name().0.clone();
1438 let configuration_view = self.configuration_views.get(&provider.id()).cloned();
1439
1440 let open_new_context = cx.listener({
1441 let provider = provider.clone();
1442 move |_, _, cx| {
1443 cx.emit(ConfigurationViewEvent::NewProviderContextEditor(
1444 provider.clone(),
1445 ))
1446 }
1447 });
1448
1449 v_flex()
1450 .gap_2()
1451 .child(
1452 h_flex()
1453 .justify_between()
1454 .child(Headline::new(provider_name.clone()).size(HeadlineSize::Small))
1455 .when(provider.is_authenticated(cx), move |this| {
1456 this.child(
1457 h_flex().justify_end().child(
1458 Button::new(
1459 SharedString::from(format!("new-context-{provider_id}")),
1460 "Open New Chat",
1461 )
1462 .icon_position(IconPosition::Start)
1463 .icon(IconName::Plus)
1464 .style(ButtonStyle::Filled)
1465 .layer(ElevationIndex::ModalSurface)
1466 .on_click(open_new_context),
1467 ),
1468 )
1469 }),
1470 )
1471 .child(
1472 div()
1473 .p(DynamicSpacing::Base08.rems(cx))
1474 .bg(cx.theme().colors().surface_background)
1475 .border_1()
1476 .border_color(cx.theme().colors().border_variant)
1477 .rounded_md()
1478 .when(configuration_view.is_none(), |this| {
1479 this.child(div().child(Label::new(format!(
1480 "No configuration view for {}",
1481 provider_name
1482 ))))
1483 })
1484 .when_some(configuration_view, |this, configuration_view| {
1485 this.child(configuration_view)
1486 }),
1487 )
1488 }
1489}
1490
1491impl Render for ConfigurationView {
1492 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
1493 let providers = LanguageModelRegistry::read_global(cx).providers();
1494 let provider_views = providers
1495 .into_iter()
1496 .map(|provider| self.render_provider_view(&provider, cx))
1497 .collect::<Vec<_>>();
1498
1499 let mut element = v_flex()
1500 .id("assistant-configuration-view")
1501 .track_focus(&self.focus_handle(cx))
1502 .bg(cx.theme().colors().editor_background)
1503 .size_full()
1504 .overflow_y_scroll()
1505 .child(
1506 v_flex()
1507 .p(DynamicSpacing::Base16.rems(cx))
1508 .border_b_1()
1509 .border_color(cx.theme().colors().border)
1510 .gap_1()
1511 .child(Headline::new("Configure your Assistant").size(HeadlineSize::Medium))
1512 .child(
1513 Label::new(
1514 "At least one LLM provider must be configured to use the Assistant.",
1515 )
1516 .color(Color::Muted),
1517 ),
1518 )
1519 .child(
1520 v_flex()
1521 .p(DynamicSpacing::Base16.rems(cx))
1522 .mt_1()
1523 .gap_6()
1524 .flex_1()
1525 .children(provider_views),
1526 )
1527 .into_any();
1528
1529 // We use a canvas here to get scrolling to work in the ConfigurationView. It's a workaround
1530 // because we couldn't the element to take up the size of the parent.
1531 canvas(
1532 move |bounds, cx| {
1533 element.prepaint_as_root(bounds.origin, bounds.size.into(), cx);
1534 element
1535 },
1536 |_, mut element, cx| {
1537 element.paint(cx);
1538 },
1539 )
1540 .flex_1()
1541 .w_full()
1542 }
1543}
1544
1545pub enum ConfigurationViewEvent {
1546 NewProviderContextEditor(Arc<dyn LanguageModelProvider>),
1547}
1548
1549impl EventEmitter<ConfigurationViewEvent> for ConfigurationView {}
1550
1551impl FocusableView for ConfigurationView {
1552 fn focus_handle(&self, _: &AppContext) -> FocusHandle {
1553 self.focus_handle.clone()
1554 }
1555}
1556
1557impl Item for ConfigurationView {
1558 type Event = ConfigurationViewEvent;
1559
1560 fn tab_content_text(&self, _cx: &WindowContext) -> Option<SharedString> {
1561 Some("Configuration".into())
1562 }
1563}