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