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