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