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