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