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