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