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