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