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