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