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