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