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