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