1use crate::{
2 assistant_settings::{AssistantDockPosition, AssistantSettings},
3 humanize_token_count,
4 prompt_library::open_prompt_library,
5 prompts::PromptBuilder,
6 slash_command::{
7 default_command::DefaultSlashCommand,
8 docs_command::{DocsSlashCommand, DocsSlashCommandArgs},
9 SlashCommandCompletionProvider, SlashCommandRegistry,
10 },
11 terminal_inline_assistant::TerminalInlineAssistant,
12 Assist, CodegenStatus, ConfirmCommand, Context, ContextEvent, ContextId, ContextStore,
13 CycleMessageRole, DebugEditSteps, DeployHistory, DeployPromptLibrary, InlineAssist,
14 InlineAssistId, InlineAssistant, InsertIntoEditor, MessageStatus, ModelSelector,
15 PendingSlashCommand, PendingSlashCommandStatus, QuoteSelection, RemoteContextMetadata,
16 ResolvedWorkflowStep, SavedContextMetadata, Split, ToggleFocus, ToggleModelSelector,
17 WorkflowStepStatus,
18};
19use crate::{ContextStoreEvent, ShowConfiguration};
20use anyhow::{anyhow, Context as _, Result};
21use assistant_slash_command::{SlashCommand, SlashCommandOutputSection};
22use client::{proto, Client, Status};
23use collections::{BTreeSet, HashMap, HashSet};
24use editor::{
25 actions::{FoldAt, MoveToEndOfLine, Newline, ShowCompletions, UnfoldAt},
26 display_map::{
27 BlockDisposition, BlockProperties, BlockStyle, Crease, CustomBlockId, RenderBlock,
28 ToDisplayPoint,
29 },
30 scroll::{Autoscroll, AutoscrollStrategy, ScrollAnchor},
31 Anchor, Editor, EditorEvent, ExcerptRange, MultiBuffer, RowExt, ToOffset as _, ToPoint,
32};
33use editor::{display_map::CreaseId, FoldPlaceholder};
34use fs::Fs;
35use gpui::{
36 div, percentage, point, Action, Animation, AnimationExt, AnyElement, AnyView, AppContext,
37 AsyncWindowContext, ClipboardItem, Context as _, DismissEvent, Empty, Entity, EventEmitter,
38 FocusHandle, FocusableView, FontWeight, InteractiveElement, IntoElement, Model, ParentElement,
39 Pixels, ReadGlobal, Render, SharedString, StatefulInteractiveElement, Styled, Subscription,
40 Task, Transformation, UpdateGlobal, View, ViewContext, VisualContext, WeakView, WindowContext,
41};
42use indexed_docs::IndexedDocsStore;
43use language::{
44 language_settings::SoftWrap, Capability, LanguageRegistry, LspAdapterDelegate, Point, ToOffset,
45};
46use language_model::{
47 provider::cloud::PROVIDER_ID, LanguageModelProvider, LanguageModelProviderId,
48 LanguageModelRegistry, Role,
49};
50use multi_buffer::MultiBufferRow;
51use picker::{Picker, PickerDelegate};
52use project::{Project, ProjectLspAdapterDelegate};
53use search::{buffer_search::DivRegistrar, BufferSearchBar};
54use settings::{update_settings_file, Settings};
55use smol::stream::StreamExt;
56use std::{
57 borrow::Cow,
58 cmp::{self, Ordering},
59 fmt::Write,
60 ops::Range,
61 path::PathBuf,
62 sync::Arc,
63 time::Duration,
64};
65use terminal_view::{terminal_panel::TerminalPanel, TerminalView};
66use ui::TintColor;
67use ui::{
68 prelude::*,
69 utils::{format_distance_from_now, DateTimeType},
70 Avatar, AvatarShape, ButtonLike, ContextMenu, Disclosure, ElevationIndex, KeyBinding, ListItem,
71 ListItemSpacing, PopoverMenu, PopoverMenuHandle, Tooltip,
72};
73use util::ResultExt;
74use workspace::{
75 dock::{DockPosition, Panel, PanelEvent},
76 item::{self, FollowableItem, Item, ItemHandle},
77 notifications::NotifyTaskExt,
78 pane::{self, SaveIntent},
79 searchable::{SearchEvent, SearchableItem},
80 Pane, Save, ToggleZoom, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace,
81};
82use workspace::{searchable::SearchableItemHandle, NewFile};
83
84pub fn init(cx: &mut AppContext) {
85 workspace::FollowableViewRegistry::register::<ContextEditor>(cx);
86 cx.observe_new_views(
87 |workspace: &mut Workspace, _cx: &mut ViewContext<Workspace>| {
88 workspace
89 .register_action(|workspace, _: &ToggleFocus, cx| {
90 let settings = AssistantSettings::get_global(cx);
91 if !settings.enabled {
92 return;
93 }
94
95 workspace.toggle_panel_focus::<AssistantPanel>(cx);
96 })
97 .register_action(AssistantPanel::inline_assist)
98 .register_action(ContextEditor::quote_selection)
99 .register_action(ContextEditor::insert_selection)
100 .register_action(AssistantPanel::show_configuration);
101 },
102 )
103 .detach();
104
105 cx.observe_new_views(
106 |terminal_panel: &mut TerminalPanel, cx: &mut ViewContext<TerminalPanel>| {
107 let settings = AssistantSettings::get_global(cx);
108 if !settings.enabled {
109 return;
110 }
111
112 terminal_panel.register_tab_bar_button(cx.new_view(|_| InlineAssistTabBarButton), cx);
113 },
114 )
115 .detach();
116}
117
118struct InlineAssistTabBarButton;
119
120impl Render for InlineAssistTabBarButton {
121 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
122 IconButton::new("terminal_inline_assistant", IconName::MagicWand)
123 .icon_size(IconSize::Small)
124 .on_click(cx.listener(|_, _, cx| {
125 cx.dispatch_action(InlineAssist::default().boxed_clone());
126 }))
127 .tooltip(move |cx| Tooltip::for_action("Inline Assist", &InlineAssist::default(), cx))
128 }
129}
130
131pub enum AssistantPanelEvent {
132 ContextEdited,
133}
134
135pub struct AssistantPanel {
136 pane: View<Pane>,
137 workspace: WeakView<Workspace>,
138 width: Option<Pixels>,
139 height: Option<Pixels>,
140 project: Model<Project>,
141 context_store: Model<ContextStore>,
142 languages: Arc<LanguageRegistry>,
143 fs: Arc<dyn Fs>,
144 subscriptions: Vec<Subscription>,
145 model_selector_menu_handle: PopoverMenuHandle<ContextMenu>,
146 model_summary_editor: View<Editor>,
147 authenticate_provider_task: Option<(LanguageModelProviderId, Task<()>)>,
148 configuration_subscription: Option<Subscription>,
149 client_status: Option<client::Status>,
150 watch_client_status: Option<Task<()>>,
151 show_zed_ai_notice: bool,
152}
153
154#[derive(Clone)]
155enum ContextMetadata {
156 Remote(RemoteContextMetadata),
157 Saved(SavedContextMetadata),
158}
159
160struct SavedContextPickerDelegate {
161 store: Model<ContextStore>,
162 project: Model<Project>,
163 matches: Vec<ContextMetadata>,
164 selected_index: usize,
165}
166
167enum SavedContextPickerEvent {
168 Confirmed(ContextMetadata),
169}
170
171enum InlineAssistTarget {
172 Editor(View<Editor>, bool),
173 Terminal(View<TerminalView>),
174}
175
176impl EventEmitter<SavedContextPickerEvent> for Picker<SavedContextPickerDelegate> {}
177
178impl SavedContextPickerDelegate {
179 fn new(project: Model<Project>, store: Model<ContextStore>) -> Self {
180 Self {
181 project,
182 store,
183 matches: Vec::new(),
184 selected_index: 0,
185 }
186 }
187}
188
189impl PickerDelegate for SavedContextPickerDelegate {
190 type ListItem = ListItem;
191
192 fn match_count(&self) -> usize {
193 self.matches.len()
194 }
195
196 fn selected_index(&self) -> usize {
197 self.selected_index
198 }
199
200 fn set_selected_index(&mut self, ix: usize, _cx: &mut ViewContext<Picker<Self>>) {
201 self.selected_index = ix;
202 }
203
204 fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str> {
205 "Search...".into()
206 }
207
208 fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
209 let search = self.store.read(cx).search(query, cx);
210 cx.spawn(|this, mut cx| async move {
211 let matches = search.await;
212 this.update(&mut cx, |this, cx| {
213 let host_contexts = this.delegate.store.read(cx).host_contexts();
214 this.delegate.matches = host_contexts
215 .iter()
216 .cloned()
217 .map(ContextMetadata::Remote)
218 .chain(matches.into_iter().map(ContextMetadata::Saved))
219 .collect();
220 this.delegate.selected_index = 0;
221 cx.notify();
222 })
223 .ok();
224 })
225 }
226
227 fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
228 if let Some(metadata) = self.matches.get(self.selected_index) {
229 cx.emit(SavedContextPickerEvent::Confirmed(metadata.clone()));
230 }
231 }
232
233 fn dismissed(&mut self, _cx: &mut ViewContext<Picker<Self>>) {}
234
235 fn render_match(
236 &self,
237 ix: usize,
238 selected: bool,
239 cx: &mut ViewContext<Picker<Self>>,
240 ) -> Option<Self::ListItem> {
241 let context = self.matches.get(ix)?;
242 let item = match context {
243 ContextMetadata::Remote(context) => {
244 let host_user = self.project.read(cx).host().and_then(|collaborator| {
245 self.project
246 .read(cx)
247 .user_store()
248 .read(cx)
249 .get_cached_user(collaborator.user_id)
250 });
251 div()
252 .flex()
253 .w_full()
254 .justify_between()
255 .gap_2()
256 .child(
257 h_flex().flex_1().overflow_x_hidden().child(
258 Label::new(context.summary.clone().unwrap_or(DEFAULT_TAB_TITLE.into()))
259 .size(LabelSize::Small),
260 ),
261 )
262 .child(
263 h_flex()
264 .gap_2()
265 .children(if let Some(host_user) = host_user {
266 vec![
267 Avatar::new(host_user.avatar_uri.clone())
268 .shape(AvatarShape::Circle)
269 .into_any_element(),
270 Label::new(format!("Shared by @{}", host_user.github_login))
271 .color(Color::Muted)
272 .size(LabelSize::Small)
273 .into_any_element(),
274 ]
275 } else {
276 vec![Label::new("Shared by host")
277 .color(Color::Muted)
278 .size(LabelSize::Small)
279 .into_any_element()]
280 }),
281 )
282 }
283 ContextMetadata::Saved(context) => div()
284 .flex()
285 .w_full()
286 .justify_between()
287 .gap_2()
288 .child(
289 h_flex()
290 .flex_1()
291 .child(Label::new(context.title.clone()).size(LabelSize::Small))
292 .overflow_x_hidden(),
293 )
294 .child(
295 Label::new(format_distance_from_now(
296 DateTimeType::Local(context.mtime),
297 false,
298 true,
299 true,
300 ))
301 .color(Color::Muted)
302 .size(LabelSize::Small),
303 ),
304 };
305 Some(
306 ListItem::new(ix)
307 .inset(true)
308 .spacing(ListItemSpacing::Sparse)
309 .selected(selected)
310 .child(item),
311 )
312 }
313}
314
315impl AssistantPanel {
316 pub fn load(
317 workspace: WeakView<Workspace>,
318 prompt_builder: Arc<PromptBuilder>,
319 cx: AsyncWindowContext,
320 ) -> Task<Result<View<Self>>> {
321 cx.spawn(|mut cx| async move {
322 let context_store = workspace
323 .update(&mut cx, |workspace, cx| {
324 let project = workspace.project().clone();
325 ContextStore::new(project, prompt_builder.clone(), cx)
326 })?
327 .await?;
328
329 workspace.update(&mut cx, |workspace, cx| {
330 // TODO: deserialize state.
331 cx.new_view(|cx| Self::new(workspace, context_store, cx))
332 })
333 })
334 }
335
336 fn new(
337 workspace: &Workspace,
338 context_store: Model<ContextStore>,
339 cx: &mut ViewContext<Self>,
340 ) -> Self {
341 let model_selector_menu_handle = PopoverMenuHandle::default();
342 let model_summary_editor = cx.new_view(|cx| Editor::single_line(cx));
343 let context_editor_toolbar = cx.new_view(|_| {
344 ContextEditorToolbarItem::new(
345 workspace,
346 model_selector_menu_handle.clone(),
347 model_summary_editor.clone(),
348 )
349 });
350 let pane = cx.new_view(|cx| {
351 let mut pane = Pane::new(
352 workspace.weak_handle(),
353 workspace.project().clone(),
354 Default::default(),
355 None,
356 NewFile.boxed_clone(),
357 cx,
358 );
359 pane.set_can_split(false, cx);
360 pane.set_can_navigate(true, cx);
361 pane.display_nav_history_buttons(None);
362 pane.set_should_display_tab_bar(|_| true);
363 pane.set_render_tab_bar_buttons(cx, move |pane, cx| {
364 let focus_handle = pane.focus_handle(cx);
365 let left_children = IconButton::new("history", IconName::TextSearch)
366 .on_click(
367 cx.listener(|_, _, cx| cx.dispatch_action(DeployHistory.boxed_clone())),
368 )
369 .tooltip(move |cx| {
370 cx.new_view(|cx| {
371 let keybind =
372 KeyBinding::for_action_in(&DeployHistory, &focus_handle, cx);
373 Tooltip::new("History").key_binding(keybind)
374 })
375 .into()
376 })
377 .selected(
378 pane.active_item()
379 .map_or(false, |item| item.downcast::<ContextHistory>().is_some()),
380 );
381 let right_children = h_flex()
382 .gap(Spacing::Small.rems(cx))
383 .child(
384 IconButton::new("new-context", IconName::Plus)
385 .on_click(
386 cx.listener(|_, _, cx| cx.dispatch_action(NewFile.boxed_clone())),
387 )
388 .tooltip(|cx| Tooltip::for_action("New Context", &NewFile, cx)),
389 )
390 .child(
391 IconButton::new("menu", IconName::Menu)
392 .icon_size(IconSize::Small)
393 .on_click(cx.listener(|pane, _, cx| {
394 let zoom_label = if pane.is_zoomed() {
395 "Zoom Out"
396 } else {
397 "Zoom In"
398 };
399 let menu = ContextMenu::build(cx, |menu, cx| {
400 menu.context(pane.focus_handle(cx))
401 .action("New Context", Box::new(NewFile))
402 .action("History", Box::new(DeployHistory))
403 .action("Prompt Library", Box::new(DeployPromptLibrary))
404 .action("Configure", Box::new(ShowConfiguration))
405 .action(zoom_label, Box::new(ToggleZoom))
406 });
407 cx.subscribe(&menu, |pane, _, _: &DismissEvent, _| {
408 pane.new_item_menu = None;
409 })
410 .detach();
411 pane.new_item_menu = Some(menu);
412 })),
413 )
414 .when_some(pane.new_item_menu.as_ref(), |el, new_item_menu| {
415 el.child(Pane::render_menu_overlay(new_item_menu))
416 })
417 .into_any_element()
418 .into();
419
420 (Some(left_children.into_any_element()), right_children)
421 });
422 pane.toolbar().update(cx, |toolbar, cx| {
423 toolbar.add_item(context_editor_toolbar.clone(), cx);
424 toolbar.add_item(cx.new_view(BufferSearchBar::new), cx)
425 });
426 pane
427 });
428
429 let subscriptions = vec![
430 cx.observe(&pane, |_, _, cx| cx.notify()),
431 cx.subscribe(&pane, Self::handle_pane_event),
432 cx.subscribe(&context_editor_toolbar, Self::handle_toolbar_event),
433 cx.subscribe(&model_summary_editor, Self::handle_summary_editor_event),
434 cx.subscribe(&context_store, Self::handle_context_store_event),
435 cx.subscribe(
436 &LanguageModelRegistry::global(cx),
437 |this, _, event: &language_model::Event, cx| match event {
438 language_model::Event::ActiveModelChanged => {
439 this.completion_provider_changed(cx);
440 }
441 language_model::Event::ProviderStateChanged => {
442 this.ensure_authenticated(cx);
443 }
444 language_model::Event::AddedProvider(_)
445 | language_model::Event::RemovedProvider(_) => {
446 this.ensure_authenticated(cx);
447 }
448 },
449 ),
450 ];
451
452 let watch_client_status = Self::watch_client_status(workspace.client().clone(), cx);
453
454 let mut this = Self {
455 pane,
456 workspace: workspace.weak_handle(),
457 width: None,
458 height: None,
459 project: workspace.project().clone(),
460 context_store,
461 languages: workspace.app_state().languages.clone(),
462 fs: workspace.app_state().fs.clone(),
463 subscriptions,
464 model_selector_menu_handle,
465 model_summary_editor,
466 authenticate_provider_task: None,
467 configuration_subscription: None,
468 client_status: None,
469 watch_client_status: Some(watch_client_status),
470 show_zed_ai_notice: false,
471 };
472 this.new_context(cx);
473 this
474 }
475
476 fn watch_client_status(client: Arc<Client>, cx: &mut ViewContext<Self>) -> Task<()> {
477 let mut status_rx = client.status();
478
479 cx.spawn(|this, mut cx| async move {
480 while let Some(status) = status_rx.next().await {
481 this.update(&mut cx, |this, cx| {
482 if this.client_status.is_none()
483 || this
484 .client_status
485 .map_or(false, |old_status| old_status != status)
486 {
487 this.update_zed_ai_notice_visibility(status, cx);
488 }
489 this.client_status = Some(status);
490 })
491 .log_err();
492 }
493 this.update(&mut cx, |this, _cx| this.watch_client_status = None)
494 .log_err();
495 })
496 }
497
498 fn handle_pane_event(
499 &mut self,
500 pane: View<Pane>,
501 event: &pane::Event,
502 cx: &mut ViewContext<Self>,
503 ) {
504 let update_model_summary = match event {
505 pane::Event::Remove => {
506 cx.emit(PanelEvent::Close);
507 false
508 }
509 pane::Event::ZoomIn => {
510 cx.emit(PanelEvent::ZoomIn);
511 false
512 }
513 pane::Event::ZoomOut => {
514 cx.emit(PanelEvent::ZoomOut);
515 false
516 }
517
518 pane::Event::AddItem { item } => {
519 self.workspace
520 .update(cx, |workspace, cx| {
521 item.added_to_pane(workspace, self.pane.clone(), cx)
522 })
523 .ok();
524 true
525 }
526
527 pane::Event::ActivateItem { local } => {
528 if *local {
529 self.workspace
530 .update(cx, |workspace, cx| {
531 workspace.unfollow_in_pane(&pane, cx);
532 })
533 .ok();
534 }
535 cx.emit(AssistantPanelEvent::ContextEdited);
536 true
537 }
538
539 pane::Event::RemoveItem { idx } => {
540 if self
541 .pane
542 .read(cx)
543 .item_for_index(*idx)
544 .map_or(false, |item| item.downcast::<ConfigurationView>().is_some())
545 {
546 self.configuration_subscription = None;
547 }
548 false
549 }
550 pane::Event::RemovedItem { .. } => {
551 cx.emit(AssistantPanelEvent::ContextEdited);
552 true
553 }
554
555 _ => false,
556 };
557
558 if update_model_summary {
559 if let Some(editor) = self.active_context_editor(cx) {
560 self.show_updated_summary(&editor, cx)
561 }
562 }
563 }
564
565 fn handle_summary_editor_event(
566 &mut self,
567 model_summary_editor: View<Editor>,
568 event: &EditorEvent,
569 cx: &mut ViewContext<Self>,
570 ) {
571 if matches!(event, EditorEvent::Edited { .. }) {
572 if let Some(context_editor) = self.active_context_editor(cx) {
573 let new_summary = model_summary_editor.read(cx).text(cx);
574 context_editor.update(cx, |context_editor, cx| {
575 context_editor.context.update(cx, |context, cx| {
576 if context.summary().is_none()
577 && (new_summary == DEFAULT_TAB_TITLE || new_summary.trim().is_empty())
578 {
579 return;
580 }
581 context.custom_summary(new_summary, cx)
582 });
583 });
584 }
585 }
586 }
587
588 fn update_zed_ai_notice_visibility(
589 &mut self,
590 client_status: Status,
591 cx: &mut ViewContext<Self>,
592 ) {
593 let active_provider = LanguageModelRegistry::read_global(cx).active_provider();
594
595 // If we're signed out and don't have a provider configured, or we're signed-out AND Zed.dev is
596 // the provider, we want to show a nudge to sign in.
597 let show_zed_ai_notice = client_status.is_signed_out()
598 && active_provider.map_or(true, |provider| provider.id().0 == PROVIDER_ID);
599
600 self.show_zed_ai_notice = show_zed_ai_notice;
601 cx.notify();
602 }
603
604 fn handle_toolbar_event(
605 &mut self,
606 _: View<ContextEditorToolbarItem>,
607 _: &ContextEditorToolbarItemEvent,
608 cx: &mut ViewContext<Self>,
609 ) {
610 if let Some(context_editor) = self.active_context_editor(cx) {
611 context_editor.update(cx, |context_editor, cx| {
612 context_editor.context.update(cx, |context, cx| {
613 context.summarize(true, cx);
614 })
615 })
616 }
617 }
618
619 fn handle_context_store_event(
620 &mut self,
621 _context_store: Model<ContextStore>,
622 event: &ContextStoreEvent,
623 cx: &mut ViewContext<Self>,
624 ) {
625 let ContextStoreEvent::ContextCreated(context_id) = event;
626 let Some(context) = self
627 .context_store
628 .read(cx)
629 .loaded_context_for_id(&context_id, cx)
630 else {
631 log::error!("no context found with ID: {}", context_id.to_proto());
632 return;
633 };
634 let lsp_adapter_delegate = make_lsp_adapter_delegate(&self.project, cx).log_err();
635
636 let assistant_panel = cx.view().downgrade();
637 let editor = cx.new_view(|cx| {
638 let mut editor = ContextEditor::for_context(
639 context,
640 self.fs.clone(),
641 self.workspace.clone(),
642 self.project.clone(),
643 lsp_adapter_delegate,
644 assistant_panel,
645 cx,
646 );
647 editor.insert_default_prompt(cx);
648 editor
649 });
650
651 self.show_context(editor.clone(), cx);
652 }
653
654 fn completion_provider_changed(&mut self, cx: &mut ViewContext<Self>) {
655 if let Some(editor) = self.active_context_editor(cx) {
656 editor.update(cx, |active_context, cx| {
657 active_context
658 .context
659 .update(cx, |context, cx| context.completion_provider_changed(cx))
660 })
661 }
662
663 let Some(new_provider_id) = LanguageModelRegistry::read_global(cx)
664 .active_provider()
665 .map(|p| p.id())
666 else {
667 return;
668 };
669
670 if self
671 .authenticate_provider_task
672 .as_ref()
673 .map_or(true, |(old_provider_id, _)| {
674 *old_provider_id != new_provider_id
675 })
676 {
677 self.authenticate_provider_task = None;
678 self.ensure_authenticated(cx);
679 }
680
681 if let Some(status) = self.client_status {
682 self.update_zed_ai_notice_visibility(status, cx);
683 }
684 }
685
686 fn ensure_authenticated(&mut self, cx: &mut ViewContext<Self>) {
687 if self.is_authenticated(cx) {
688 return;
689 }
690
691 let Some(provider) = LanguageModelRegistry::read_global(cx).active_provider() else {
692 return;
693 };
694
695 let load_credentials = self.authenticate(cx);
696
697 if self.authenticate_provider_task.is_none() {
698 self.authenticate_provider_task = Some((
699 provider.id(),
700 cx.spawn(|this, mut cx| async move {
701 let _ = load_credentials.await;
702 this.update(&mut cx, |this, _cx| {
703 this.authenticate_provider_task = None;
704 })
705 .log_err();
706 }),
707 ));
708 }
709 }
710
711 pub fn inline_assist(
712 workspace: &mut Workspace,
713 action: &InlineAssist,
714 cx: &mut ViewContext<Workspace>,
715 ) {
716 let settings = AssistantSettings::get_global(cx);
717 if !settings.enabled {
718 return;
719 }
720
721 let Some(assistant_panel) = workspace.panel::<AssistantPanel>(cx) else {
722 return;
723 };
724
725 let Some(inline_assist_target) =
726 Self::resolve_inline_assist_target(workspace, &assistant_panel, cx)
727 else {
728 return;
729 };
730
731 let initial_prompt = action.prompt.clone();
732 if assistant_panel.update(cx, |assistant, cx| assistant.is_authenticated(cx)) {
733 match inline_assist_target {
734 InlineAssistTarget::Editor(active_editor, include_context) => {
735 InlineAssistant::update_global(cx, |assistant, cx| {
736 assistant.assist(
737 &active_editor,
738 Some(cx.view().downgrade()),
739 include_context.then_some(&assistant_panel),
740 initial_prompt,
741 cx,
742 )
743 })
744 }
745 InlineAssistTarget::Terminal(active_terminal) => {
746 TerminalInlineAssistant::update_global(cx, |assistant, cx| {
747 assistant.assist(
748 &active_terminal,
749 Some(cx.view().downgrade()),
750 Some(&assistant_panel),
751 initial_prompt,
752 cx,
753 )
754 })
755 }
756 }
757 } else {
758 let assistant_panel = assistant_panel.downgrade();
759 cx.spawn(|workspace, mut cx| async move {
760 assistant_panel
761 .update(&mut cx, |assistant, cx| assistant.authenticate(cx))?
762 .await?;
763 if assistant_panel.update(&mut cx, |panel, cx| panel.is_authenticated(cx))? {
764 cx.update(|cx| match inline_assist_target {
765 InlineAssistTarget::Editor(active_editor, include_context) => {
766 let assistant_panel = if include_context {
767 assistant_panel.upgrade()
768 } else {
769 None
770 };
771 InlineAssistant::update_global(cx, |assistant, cx| {
772 assistant.assist(
773 &active_editor,
774 Some(workspace),
775 assistant_panel.as_ref(),
776 initial_prompt,
777 cx,
778 )
779 })
780 }
781 InlineAssistTarget::Terminal(active_terminal) => {
782 TerminalInlineAssistant::update_global(cx, |assistant, cx| {
783 assistant.assist(
784 &active_terminal,
785 Some(workspace),
786 assistant_panel.upgrade().as_ref(),
787 initial_prompt,
788 cx,
789 )
790 })
791 }
792 })?
793 } else {
794 workspace.update(&mut cx, |workspace, cx| {
795 workspace.focus_panel::<AssistantPanel>(cx)
796 })?;
797 }
798
799 anyhow::Ok(())
800 })
801 .detach_and_log_err(cx)
802 }
803 }
804
805 fn resolve_inline_assist_target(
806 workspace: &mut Workspace,
807 assistant_panel: &View<AssistantPanel>,
808 cx: &mut WindowContext,
809 ) -> Option<InlineAssistTarget> {
810 if let Some(terminal_panel) = workspace.panel::<TerminalPanel>(cx) {
811 if terminal_panel
812 .read(cx)
813 .focus_handle(cx)
814 .contains_focused(cx)
815 {
816 if let Some(terminal_view) = terminal_panel.read(cx).pane().and_then(|pane| {
817 pane.read(cx)
818 .active_item()
819 .and_then(|t| t.downcast::<TerminalView>())
820 }) {
821 return Some(InlineAssistTarget::Terminal(terminal_view));
822 }
823 }
824 }
825 let context_editor =
826 assistant_panel
827 .read(cx)
828 .active_context_editor(cx)
829 .and_then(|editor| {
830 let editor = &editor.read(cx).editor;
831 if editor.read(cx).is_focused(cx) {
832 Some(editor.clone())
833 } else {
834 None
835 }
836 });
837
838 if let Some(context_editor) = context_editor {
839 Some(InlineAssistTarget::Editor(context_editor, false))
840 } else if let Some(workspace_editor) = workspace
841 .active_item(cx)
842 .and_then(|item| item.act_as::<Editor>(cx))
843 {
844 Some(InlineAssistTarget::Editor(workspace_editor, true))
845 } else if let Some(terminal_view) = workspace
846 .active_item(cx)
847 .and_then(|item| item.act_as::<TerminalView>(cx))
848 {
849 Some(InlineAssistTarget::Terminal(terminal_view))
850 } else {
851 None
852 }
853 }
854
855 fn new_context(&mut self, cx: &mut ViewContext<Self>) -> Option<View<ContextEditor>> {
856 if self.project.read(cx).is_remote() {
857 let task = self
858 .context_store
859 .update(cx, |store, cx| store.create_remote_context(cx));
860
861 cx.spawn(|this, mut cx| async move {
862 let context = task.await?;
863
864 this.update(&mut cx, |this, cx| {
865 let workspace = this.workspace.clone();
866 let project = this.project.clone();
867 let lsp_adapter_delegate = make_lsp_adapter_delegate(&project, cx).log_err();
868
869 let fs = this.fs.clone();
870 let project = this.project.clone();
871 let weak_assistant_panel = cx.view().downgrade();
872
873 let editor = cx.new_view(|cx| {
874 ContextEditor::for_context(
875 context,
876 fs,
877 workspace,
878 project,
879 lsp_adapter_delegate,
880 weak_assistant_panel,
881 cx,
882 )
883 });
884
885 this.show_context(editor, cx);
886
887 anyhow::Ok(())
888 })??;
889
890 anyhow::Ok(())
891 })
892 .detach_and_log_err(cx);
893
894 None
895 } else {
896 let context = self.context_store.update(cx, |store, cx| store.create(cx));
897 let lsp_adapter_delegate = make_lsp_adapter_delegate(&self.project, cx).log_err();
898
899 let assistant_panel = cx.view().downgrade();
900 let editor = cx.new_view(|cx| {
901 let mut editor = ContextEditor::for_context(
902 context,
903 self.fs.clone(),
904 self.workspace.clone(),
905 self.project.clone(),
906 lsp_adapter_delegate,
907 assistant_panel,
908 cx,
909 );
910 editor.insert_default_prompt(cx);
911 editor
912 });
913
914 self.show_context(editor.clone(), cx);
915 Some(editor)
916 }
917 }
918
919 fn show_context(&mut self, context_editor: View<ContextEditor>, cx: &mut ViewContext<Self>) {
920 let focus = self.focus_handle(cx).contains_focused(cx);
921 let prev_len = self.pane.read(cx).items_len();
922 self.pane.update(cx, |pane, cx| {
923 pane.add_item(Box::new(context_editor.clone()), focus, focus, None, cx)
924 });
925
926 if prev_len != self.pane.read(cx).items_len() {
927 self.subscriptions
928 .push(cx.subscribe(&context_editor, Self::handle_context_editor_event));
929 }
930
931 self.show_updated_summary(&context_editor, cx);
932
933 cx.emit(AssistantPanelEvent::ContextEdited);
934 cx.notify();
935 }
936
937 fn show_updated_summary(
938 &self,
939 context_editor: &View<ContextEditor>,
940 cx: &mut ViewContext<Self>,
941 ) {
942 context_editor.update(cx, |context_editor, cx| {
943 let new_summary = context_editor.title(cx).to_string();
944 self.model_summary_editor.update(cx, |summary_editor, cx| {
945 if summary_editor.text(cx) != new_summary {
946 summary_editor.set_text(new_summary, cx);
947 }
948 });
949 });
950 }
951
952 fn handle_context_editor_event(
953 &mut self,
954 context_editor: View<ContextEditor>,
955 event: &EditorEvent,
956 cx: &mut ViewContext<Self>,
957 ) {
958 match event {
959 EditorEvent::TitleChanged => {
960 self.show_updated_summary(&context_editor, cx);
961 cx.notify()
962 }
963 EditorEvent::Edited { .. } => cx.emit(AssistantPanelEvent::ContextEdited),
964 _ => {}
965 }
966 }
967
968 fn show_configuration(
969 workspace: &mut Workspace,
970 _: &ShowConfiguration,
971 cx: &mut ViewContext<Workspace>,
972 ) {
973 let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
974 return;
975 };
976
977 if !panel.focus_handle(cx).contains_focused(cx) {
978 workspace.toggle_panel_focus::<AssistantPanel>(cx);
979 }
980
981 panel.update(cx, |this, cx| {
982 this.show_configuration_tab(cx);
983 })
984 }
985
986 fn show_configuration_tab(&mut self, cx: &mut ViewContext<Self>) {
987 let configuration_item_ix = self
988 .pane
989 .read(cx)
990 .items()
991 .position(|item| item.downcast::<ConfigurationView>().is_some());
992
993 if let Some(configuration_item_ix) = configuration_item_ix {
994 self.pane.update(cx, |pane, cx| {
995 pane.activate_item(configuration_item_ix, true, true, cx);
996 });
997 } else {
998 let configuration = cx.new_view(|cx| ConfigurationView::new(cx));
999 self.configuration_subscription = Some(cx.subscribe(
1000 &configuration,
1001 |this, _, event: &ConfigurationViewEvent, cx| match event {
1002 ConfigurationViewEvent::NewProviderContextEditor(provider) => {
1003 if LanguageModelRegistry::read_global(cx)
1004 .active_provider()
1005 .map_or(true, |p| p.id() != provider.id())
1006 {
1007 if let Some(model) = provider.provided_models(cx).first().cloned() {
1008 update_settings_file::<AssistantSettings>(
1009 this.fs.clone(),
1010 cx,
1011 move |settings, _| settings.set_model(model),
1012 );
1013 }
1014 }
1015
1016 this.new_context(cx);
1017 }
1018 },
1019 ));
1020 self.pane.update(cx, |pane, cx| {
1021 pane.add_item(Box::new(configuration), true, true, None, cx);
1022 });
1023 }
1024 }
1025
1026 fn deploy_history(&mut self, _: &DeployHistory, cx: &mut ViewContext<Self>) {
1027 let history_item_ix = self
1028 .pane
1029 .read(cx)
1030 .items()
1031 .position(|item| item.downcast::<ContextHistory>().is_some());
1032
1033 if let Some(history_item_ix) = history_item_ix {
1034 self.pane.update(cx, |pane, cx| {
1035 pane.activate_item(history_item_ix, true, true, cx);
1036 });
1037 } else {
1038 let assistant_panel = cx.view().downgrade();
1039 let history = cx.new_view(|cx| {
1040 ContextHistory::new(
1041 self.project.clone(),
1042 self.context_store.clone(),
1043 assistant_panel,
1044 cx,
1045 )
1046 });
1047 self.pane.update(cx, |pane, cx| {
1048 pane.add_item(Box::new(history), true, true, None, cx);
1049 });
1050 }
1051 }
1052
1053 fn deploy_prompt_library(&mut self, _: &DeployPromptLibrary, cx: &mut ViewContext<Self>) {
1054 open_prompt_library(self.languages.clone(), cx).detach_and_log_err(cx);
1055 }
1056
1057 fn toggle_model_selector(&mut self, _: &ToggleModelSelector, cx: &mut ViewContext<Self>) {
1058 self.model_selector_menu_handle.toggle(cx);
1059 }
1060
1061 fn active_context_editor(&self, cx: &AppContext) -> Option<View<ContextEditor>> {
1062 self.pane
1063 .read(cx)
1064 .active_item()?
1065 .downcast::<ContextEditor>()
1066 }
1067
1068 pub fn active_context(&self, cx: &AppContext) -> Option<Model<Context>> {
1069 Some(self.active_context_editor(cx)?.read(cx).context.clone())
1070 }
1071
1072 fn open_saved_context(
1073 &mut self,
1074 path: PathBuf,
1075 cx: &mut ViewContext<Self>,
1076 ) -> Task<Result<()>> {
1077 let existing_context = self.pane.read(cx).items().find_map(|item| {
1078 item.downcast::<ContextEditor>()
1079 .filter(|editor| editor.read(cx).context.read(cx).path() == Some(&path))
1080 });
1081 if let Some(existing_context) = existing_context {
1082 return cx.spawn(|this, mut cx| async move {
1083 this.update(&mut cx, |this, cx| this.show_context(existing_context, cx))
1084 });
1085 }
1086
1087 let context = self
1088 .context_store
1089 .update(cx, |store, cx| store.open_local_context(path.clone(), cx));
1090 let fs = self.fs.clone();
1091 let project = self.project.clone();
1092 let workspace = self.workspace.clone();
1093
1094 let lsp_adapter_delegate = make_lsp_adapter_delegate(&project, cx).log_err();
1095
1096 cx.spawn(|this, mut cx| async move {
1097 let context = context.await?;
1098 let assistant_panel = this.clone();
1099 this.update(&mut cx, |this, cx| {
1100 let editor = cx.new_view(|cx| {
1101 ContextEditor::for_context(
1102 context,
1103 fs,
1104 workspace,
1105 project,
1106 lsp_adapter_delegate,
1107 assistant_panel,
1108 cx,
1109 )
1110 });
1111 this.show_context(editor, cx);
1112 anyhow::Ok(())
1113 })??;
1114 Ok(())
1115 })
1116 }
1117
1118 fn open_remote_context(
1119 &mut self,
1120 id: ContextId,
1121 cx: &mut ViewContext<Self>,
1122 ) -> Task<Result<View<ContextEditor>>> {
1123 let existing_context = self.pane.read(cx).items().find_map(|item| {
1124 item.downcast::<ContextEditor>()
1125 .filter(|editor| *editor.read(cx).context.read(cx).id() == id)
1126 });
1127 if let Some(existing_context) = existing_context {
1128 return cx.spawn(|this, mut cx| async move {
1129 this.update(&mut cx, |this, cx| {
1130 this.show_context(existing_context.clone(), cx)
1131 })?;
1132 Ok(existing_context)
1133 });
1134 }
1135
1136 let context = self
1137 .context_store
1138 .update(cx, |store, cx| store.open_remote_context(id, cx));
1139 let fs = self.fs.clone();
1140 let workspace = self.workspace.clone();
1141 let lsp_adapter_delegate = make_lsp_adapter_delegate(&self.project, cx).log_err();
1142
1143 cx.spawn(|this, mut cx| async move {
1144 let context = context.await?;
1145 let assistant_panel = this.clone();
1146 this.update(&mut cx, |this, cx| {
1147 let editor = cx.new_view(|cx| {
1148 ContextEditor::for_context(
1149 context,
1150 fs,
1151 workspace,
1152 this.project.clone(),
1153 lsp_adapter_delegate,
1154 assistant_panel,
1155 cx,
1156 )
1157 });
1158 this.show_context(editor.clone(), cx);
1159 anyhow::Ok(editor)
1160 })?
1161 })
1162 }
1163
1164 fn is_authenticated(&mut self, cx: &mut ViewContext<Self>) -> bool {
1165 LanguageModelRegistry::read_global(cx)
1166 .active_provider()
1167 .map_or(false, |provider| provider.is_authenticated(cx))
1168 }
1169
1170 fn authenticate(&mut self, cx: &mut ViewContext<Self>) -> Task<Result<()>> {
1171 LanguageModelRegistry::read_global(cx)
1172 .active_provider()
1173 .map_or(
1174 Task::ready(Err(anyhow!("no active language model provider"))),
1175 |provider| provider.authenticate(cx),
1176 )
1177 }
1178}
1179
1180impl Render for AssistantPanel {
1181 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
1182 let mut registrar = DivRegistrar::new(
1183 |panel, cx| {
1184 panel
1185 .pane
1186 .read(cx)
1187 .toolbar()
1188 .read(cx)
1189 .item_of_type::<BufferSearchBar>()
1190 },
1191 cx,
1192 );
1193 BufferSearchBar::register(&mut registrar);
1194 let registrar = registrar.into_div();
1195
1196 v_flex()
1197 .key_context("AssistantPanel")
1198 .size_full()
1199 .on_action(cx.listener(|this, _: &workspace::NewFile, cx| {
1200 this.new_context(cx);
1201 }))
1202 .on_action(
1203 cx.listener(|this, _: &ShowConfiguration, cx| this.show_configuration_tab(cx)),
1204 )
1205 .on_action(cx.listener(AssistantPanel::deploy_history))
1206 .on_action(cx.listener(AssistantPanel::deploy_prompt_library))
1207 .on_action(cx.listener(AssistantPanel::toggle_model_selector))
1208 .child(registrar.size_full().child(self.pane.clone()))
1209 .into_any_element()
1210 }
1211}
1212
1213impl Panel for AssistantPanel {
1214 fn persistent_name() -> &'static str {
1215 "AssistantPanel"
1216 }
1217
1218 fn position(&self, cx: &WindowContext) -> DockPosition {
1219 match AssistantSettings::get_global(cx).dock {
1220 AssistantDockPosition::Left => DockPosition::Left,
1221 AssistantDockPosition::Bottom => DockPosition::Bottom,
1222 AssistantDockPosition::Right => DockPosition::Right,
1223 }
1224 }
1225
1226 fn position_is_valid(&self, _: DockPosition) -> bool {
1227 true
1228 }
1229
1230 fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
1231 settings::update_settings_file::<AssistantSettings>(
1232 self.fs.clone(),
1233 cx,
1234 move |settings, _| {
1235 let dock = match position {
1236 DockPosition::Left => AssistantDockPosition::Left,
1237 DockPosition::Bottom => AssistantDockPosition::Bottom,
1238 DockPosition::Right => AssistantDockPosition::Right,
1239 };
1240 settings.set_dock(dock);
1241 },
1242 );
1243 }
1244
1245 fn size(&self, cx: &WindowContext) -> Pixels {
1246 let settings = AssistantSettings::get_global(cx);
1247 match self.position(cx) {
1248 DockPosition::Left | DockPosition::Right => {
1249 self.width.unwrap_or(settings.default_width)
1250 }
1251 DockPosition::Bottom => self.height.unwrap_or(settings.default_height),
1252 }
1253 }
1254
1255 fn set_size(&mut self, size: Option<Pixels>, cx: &mut ViewContext<Self>) {
1256 match self.position(cx) {
1257 DockPosition::Left | DockPosition::Right => self.width = size,
1258 DockPosition::Bottom => self.height = size,
1259 }
1260 cx.notify();
1261 }
1262
1263 fn is_zoomed(&self, cx: &WindowContext) -> bool {
1264 self.pane.read(cx).is_zoomed()
1265 }
1266
1267 fn set_zoomed(&mut self, zoomed: bool, cx: &mut ViewContext<Self>) {
1268 self.pane.update(cx, |pane, cx| pane.set_zoomed(zoomed, cx));
1269 }
1270
1271 fn set_active(&mut self, active: bool, cx: &mut ViewContext<Self>) {
1272 if active {
1273 if self.pane.read(cx).items_len() == 0 {
1274 self.new_context(cx);
1275 }
1276
1277 self.ensure_authenticated(cx);
1278 }
1279 }
1280
1281 fn pane(&self) -> Option<View<Pane>> {
1282 Some(self.pane.clone())
1283 }
1284
1285 fn remote_id() -> Option<proto::PanelId> {
1286 Some(proto::PanelId::AssistantPanel)
1287 }
1288
1289 fn icon(&self, cx: &WindowContext) -> Option<IconName> {
1290 let settings = AssistantSettings::get_global(cx);
1291 if !settings.enabled || !settings.button {
1292 return None;
1293 }
1294
1295 Some(IconName::ZedAssistant)
1296 }
1297
1298 fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {
1299 Some("Assistant Panel")
1300 }
1301
1302 fn toggle_action(&self) -> Box<dyn Action> {
1303 Box::new(ToggleFocus)
1304 }
1305}
1306
1307impl EventEmitter<PanelEvent> for AssistantPanel {}
1308impl EventEmitter<AssistantPanelEvent> for AssistantPanel {}
1309
1310impl FocusableView for AssistantPanel {
1311 fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
1312 self.pane.focus_handle(cx)
1313 }
1314}
1315
1316pub enum ContextEditorEvent {
1317 Edited,
1318 TabContentChanged,
1319}
1320
1321#[derive(Copy, Clone, Debug, PartialEq)]
1322struct ScrollPosition {
1323 offset_before_cursor: gpui::Point<f32>,
1324 cursor: Anchor,
1325}
1326
1327struct WorkflowAssist {
1328 editor: WeakView<Editor>,
1329 editor_was_open: bool,
1330 assist_ids: Vec<InlineAssistId>,
1331}
1332
1333pub struct ContextEditor {
1334 context: Model<Context>,
1335 fs: Arc<dyn Fs>,
1336 workspace: WeakView<Workspace>,
1337 project: Model<Project>,
1338 lsp_adapter_delegate: Option<Arc<dyn LspAdapterDelegate>>,
1339 editor: View<Editor>,
1340 blocks: HashSet<CustomBlockId>,
1341 scroll_position: Option<ScrollPosition>,
1342 remote_id: Option<workspace::ViewId>,
1343 pending_slash_command_creases: HashMap<Range<language::Anchor>, CreaseId>,
1344 pending_slash_command_blocks: HashMap<Range<language::Anchor>, CustomBlockId>,
1345 workflow_assists: HashMap<Range<language::Anchor>, WorkflowAssist>,
1346 active_workflow_step_range: Option<Range<language::Anchor>>,
1347 _subscriptions: Vec<Subscription>,
1348 assistant_panel: WeakView<AssistantPanel>,
1349 error_message: Option<SharedString>,
1350}
1351
1352const DEFAULT_TAB_TITLE: &str = "New Context";
1353const MAX_TAB_TITLE_LEN: usize = 16;
1354
1355impl ContextEditor {
1356 fn for_context(
1357 context: Model<Context>,
1358 fs: Arc<dyn Fs>,
1359 workspace: WeakView<Workspace>,
1360 project: Model<Project>,
1361 lsp_adapter_delegate: Option<Arc<dyn LspAdapterDelegate>>,
1362 assistant_panel: WeakView<AssistantPanel>,
1363 cx: &mut ViewContext<Self>,
1364 ) -> Self {
1365 let completion_provider = SlashCommandCompletionProvider::new(
1366 Some(cx.view().downgrade()),
1367 Some(workspace.clone()),
1368 );
1369
1370 let editor = cx.new_view(|cx| {
1371 let mut editor = Editor::for_buffer(context.read(cx).buffer().clone(), None, cx);
1372 editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
1373 editor.set_show_line_numbers(false, cx);
1374 editor.set_show_git_diff_gutter(false, cx);
1375 editor.set_show_code_actions(false, cx);
1376 editor.set_show_runnables(false, cx);
1377 editor.set_show_wrap_guides(false, cx);
1378 editor.set_show_indent_guides(false, cx);
1379 editor.set_completion_provider(Box::new(completion_provider));
1380 editor.set_collaboration_hub(Box::new(project.clone()));
1381 editor
1382 });
1383
1384 let _subscriptions = vec![
1385 cx.observe(&context, |_, _, cx| cx.notify()),
1386 cx.subscribe(&context, Self::handle_context_event),
1387 cx.subscribe(&editor, Self::handle_editor_event),
1388 cx.subscribe(&editor, Self::handle_editor_search_event),
1389 ];
1390
1391 let sections = context.read(cx).slash_command_output_sections().to_vec();
1392 let mut this = Self {
1393 context,
1394 editor,
1395 lsp_adapter_delegate,
1396 blocks: Default::default(),
1397 scroll_position: None,
1398 remote_id: None,
1399 fs,
1400 workspace,
1401 project,
1402 pending_slash_command_creases: HashMap::default(),
1403 pending_slash_command_blocks: HashMap::default(),
1404 _subscriptions,
1405 workflow_assists: HashMap::default(),
1406 active_workflow_step_range: None,
1407 assistant_panel,
1408 error_message: None,
1409 };
1410 this.update_message_headers(cx);
1411 this.insert_slash_command_output_sections(sections, cx);
1412 this
1413 }
1414
1415 fn insert_default_prompt(&mut self, cx: &mut ViewContext<Self>) {
1416 let command_name = DefaultSlashCommand.name();
1417 self.editor.update(cx, |editor, cx| {
1418 editor.insert(&format!("/{command_name}"), cx)
1419 });
1420 self.split(&Split, cx);
1421 let command = self.context.update(cx, |context, cx| {
1422 let first_message_id = context.messages(cx).next().unwrap().id;
1423 context.update_metadata(first_message_id, cx, |metadata| {
1424 metadata.role = Role::System;
1425 });
1426 context.reparse_slash_commands(cx);
1427 context.pending_slash_commands()[0].clone()
1428 });
1429
1430 self.run_command(
1431 command.source_range,
1432 &command.name,
1433 command.argument.as_deref(),
1434 false,
1435 self.workspace.clone(),
1436 cx,
1437 );
1438 }
1439
1440 fn assist(&mut self, _: &Assist, cx: &mut ViewContext<Self>) {
1441 if !self.apply_workflow_step(cx) {
1442 self.error_message = None;
1443 self.send_to_model(cx);
1444 cx.notify();
1445 }
1446 }
1447
1448 fn apply_workflow_step(&mut self, cx: &mut ViewContext<Self>) -> bool {
1449 if let Some(step_range) = self.active_workflow_step_range.as_ref() {
1450 if let Some(assists) = self.workflow_assists.get(&step_range) {
1451 let assist_ids = assists.assist_ids.clone();
1452 cx.window_context().defer(|cx| {
1453 InlineAssistant::update_global(cx, |assistant, cx| {
1454 for assist_id in assist_ids {
1455 assistant.start_assist(assist_id, cx);
1456 }
1457 })
1458 });
1459
1460 !assists.assist_ids.is_empty()
1461 } else {
1462 false
1463 }
1464 } else {
1465 false
1466 }
1467 }
1468
1469 fn send_to_model(&mut self, cx: &mut ViewContext<Self>) {
1470 if let Some(user_message) = self.context.update(cx, |context, cx| context.assist(cx)) {
1471 let new_selection = {
1472 let cursor = user_message
1473 .start
1474 .to_offset(self.context.read(cx).buffer().read(cx));
1475 cursor..cursor
1476 };
1477 self.editor.update(cx, |editor, cx| {
1478 editor.change_selections(
1479 Some(Autoscroll::Strategy(AutoscrollStrategy::Fit)),
1480 cx,
1481 |selections| selections.select_ranges([new_selection]),
1482 );
1483 });
1484 // Avoid scrolling to the new cursor position so the assistant's output is stable.
1485 cx.defer(|this, _| this.scroll_position = None);
1486 }
1487 }
1488
1489 fn cancel_last_assist(&mut self, _: &editor::actions::Cancel, cx: &mut ViewContext<Self>) {
1490 if !self
1491 .context
1492 .update(cx, |context, _| context.cancel_last_assist())
1493 {
1494 cx.propagate();
1495 }
1496 }
1497
1498 fn debug_edit_steps(&mut self, _: &DebugEditSteps, cx: &mut ViewContext<Self>) {
1499 let mut output = String::new();
1500 for (i, step) in self.context.read(cx).workflow_steps().iter().enumerate() {
1501 output.push_str(&format!("Step {}:\n", i + 1));
1502 output.push_str(&format!(
1503 "Content: {}\n",
1504 self.context
1505 .read(cx)
1506 .buffer()
1507 .read(cx)
1508 .text_for_range(step.tagged_range.clone())
1509 .collect::<String>()
1510 ));
1511 match &step.status {
1512 WorkflowStepStatus::Resolved(ResolvedWorkflowStep { title, suggestions }) => {
1513 output.push_str("Resolution:\n");
1514 output.push_str(&format!(" {:?}\n", title));
1515 output.push_str(&format!(" {:?}\n", suggestions));
1516 }
1517 WorkflowStepStatus::Pending(_) => {
1518 output.push_str("Resolution: Pending\n");
1519 }
1520 }
1521 output.push('\n');
1522 }
1523
1524 let editor = self
1525 .workspace
1526 .update(cx, |workspace, cx| Editor::new_in_workspace(workspace, cx));
1527
1528 if let Ok(editor) = editor {
1529 cx.spawn(|_, mut cx| async move {
1530 let editor = editor.await?;
1531 editor.update(&mut cx, |editor, cx| editor.set_text(output, cx))
1532 })
1533 .detach_and_notify_err(cx);
1534 }
1535 }
1536
1537 fn cycle_message_role(&mut self, _: &CycleMessageRole, cx: &mut ViewContext<Self>) {
1538 let cursors = self.cursors(cx);
1539 self.context.update(cx, |context, cx| {
1540 let messages = context
1541 .messages_for_offsets(cursors, cx)
1542 .into_iter()
1543 .map(|message| message.id)
1544 .collect();
1545 context.cycle_message_roles(messages, cx)
1546 });
1547 }
1548
1549 fn cursors(&self, cx: &AppContext) -> Vec<usize> {
1550 let selections = self.editor.read(cx).selections.all::<usize>(cx);
1551 selections
1552 .into_iter()
1553 .map(|selection| selection.head())
1554 .collect()
1555 }
1556
1557 fn insert_command(&mut self, name: &str, cx: &mut ViewContext<Self>) {
1558 if let Some(command) = SlashCommandRegistry::global(cx).command(name) {
1559 self.editor.update(cx, |editor, cx| {
1560 editor.transact(cx, |editor, cx| {
1561 editor.change_selections(Some(Autoscroll::fit()), cx, |s| s.try_cancel());
1562 let snapshot = editor.buffer().read(cx).snapshot(cx);
1563 let newest_cursor = editor.selections.newest::<Point>(cx).head();
1564 if newest_cursor.column > 0
1565 || snapshot
1566 .chars_at(newest_cursor)
1567 .next()
1568 .map_or(false, |ch| ch != '\n')
1569 {
1570 editor.move_to_end_of_line(
1571 &MoveToEndOfLine {
1572 stop_at_soft_wraps: false,
1573 },
1574 cx,
1575 );
1576 editor.newline(&Newline, cx);
1577 }
1578
1579 editor.insert(&format!("/{name}"), cx);
1580 if command.requires_argument() {
1581 editor.insert(" ", cx);
1582 editor.show_completions(&ShowCompletions::default(), cx);
1583 }
1584 });
1585 });
1586 if !command.requires_argument() {
1587 self.confirm_command(&ConfirmCommand, cx);
1588 }
1589 }
1590 }
1591
1592 pub fn confirm_command(&mut self, _: &ConfirmCommand, cx: &mut ViewContext<Self>) {
1593 let selections = self.editor.read(cx).selections.disjoint_anchors();
1594 let mut commands_by_range = HashMap::default();
1595 let workspace = self.workspace.clone();
1596 self.context.update(cx, |context, cx| {
1597 context.reparse_slash_commands(cx);
1598 for selection in selections.iter() {
1599 if let Some(command) =
1600 context.pending_command_for_position(selection.head().text_anchor, cx)
1601 {
1602 commands_by_range
1603 .entry(command.source_range.clone())
1604 .or_insert_with(|| command.clone());
1605 }
1606 }
1607 });
1608
1609 if commands_by_range.is_empty() {
1610 cx.propagate();
1611 } else {
1612 for command in commands_by_range.into_values() {
1613 self.run_command(
1614 command.source_range,
1615 &command.name,
1616 command.argument.as_deref(),
1617 true,
1618 workspace.clone(),
1619 cx,
1620 );
1621 }
1622 cx.stop_propagation();
1623 }
1624 }
1625
1626 pub fn run_command(
1627 &mut self,
1628 command_range: Range<language::Anchor>,
1629 name: &str,
1630 argument: Option<&str>,
1631 insert_trailing_newline: bool,
1632 workspace: WeakView<Workspace>,
1633 cx: &mut ViewContext<Self>,
1634 ) {
1635 if let Some(command) = SlashCommandRegistry::global(cx).command(name) {
1636 let argument = argument.map(ToString::to_string);
1637 let output = command.run(
1638 argument.as_deref(),
1639 workspace,
1640 self.lsp_adapter_delegate.clone(),
1641 cx,
1642 );
1643 self.context.update(cx, |context, cx| {
1644 context.insert_command_output(command_range, output, insert_trailing_newline, cx)
1645 });
1646 }
1647 }
1648
1649 fn handle_context_event(
1650 &mut self,
1651 _: Model<Context>,
1652 event: &ContextEvent,
1653 cx: &mut ViewContext<Self>,
1654 ) {
1655 let context_editor = cx.view().downgrade();
1656
1657 match event {
1658 ContextEvent::MessagesEdited => {
1659 self.update_message_headers(cx);
1660 self.context.update(cx, |context, cx| {
1661 context.save(Some(Duration::from_millis(500)), self.fs.clone(), cx);
1662 });
1663 }
1664 ContextEvent::WorkflowStepsChanged => {
1665 self.update_active_workflow_step_from_cursor(cx);
1666 cx.notify();
1667 }
1668 ContextEvent::SummaryChanged => {
1669 cx.emit(EditorEvent::TitleChanged);
1670 self.context.update(cx, |context, cx| {
1671 context.save(Some(Duration::from_millis(500)), self.fs.clone(), cx);
1672 });
1673 }
1674 ContextEvent::StreamedCompletion => {
1675 self.editor.update(cx, |editor, cx| {
1676 if let Some(scroll_position) = self.scroll_position {
1677 let snapshot = editor.snapshot(cx);
1678 let cursor_point = scroll_position.cursor.to_display_point(&snapshot);
1679 let scroll_top =
1680 cursor_point.row().as_f32() - scroll_position.offset_before_cursor.y;
1681 editor.set_scroll_position(
1682 point(scroll_position.offset_before_cursor.x, scroll_top),
1683 cx,
1684 );
1685 }
1686 });
1687 }
1688 ContextEvent::PendingSlashCommandsUpdated { removed, updated } => {
1689 self.editor.update(cx, |editor, cx| {
1690 let buffer = editor.buffer().read(cx).snapshot(cx);
1691 let (excerpt_id, buffer_id, _) = buffer.as_singleton().unwrap();
1692 let excerpt_id = *excerpt_id;
1693
1694 editor.remove_creases(
1695 removed
1696 .iter()
1697 .filter_map(|range| self.pending_slash_command_creases.remove(range)),
1698 cx,
1699 );
1700
1701 editor.remove_blocks(
1702 HashSet::from_iter(
1703 removed.iter().filter_map(|range| {
1704 self.pending_slash_command_blocks.remove(range)
1705 }),
1706 ),
1707 None,
1708 cx,
1709 );
1710
1711 let crease_ids = editor.insert_creases(
1712 updated.iter().map(|command| {
1713 let workspace = self.workspace.clone();
1714 let confirm_command = Arc::new({
1715 let context_editor = context_editor.clone();
1716 let command = command.clone();
1717 move |cx: &mut WindowContext| {
1718 context_editor
1719 .update(cx, |context_editor, cx| {
1720 context_editor.run_command(
1721 command.source_range.clone(),
1722 &command.name,
1723 command.argument.as_deref(),
1724 false,
1725 workspace.clone(),
1726 cx,
1727 );
1728 })
1729 .ok();
1730 }
1731 });
1732 let placeholder = FoldPlaceholder {
1733 render: Arc::new(move |_, _, _| Empty.into_any()),
1734 constrain_width: false,
1735 merge_adjacent: false,
1736 };
1737 let render_toggle = {
1738 let confirm_command = confirm_command.clone();
1739 let command = command.clone();
1740 move |row, _, _, _cx: &mut WindowContext| {
1741 render_pending_slash_command_gutter_decoration(
1742 row,
1743 &command.status,
1744 confirm_command.clone(),
1745 )
1746 }
1747 };
1748 let render_trailer = {
1749 let command = command.clone();
1750 move |row, _unfold, cx: &mut WindowContext| {
1751 // TODO: In the future we should investigate how we can expose
1752 // this as a hook on the `SlashCommand` trait so that we don't
1753 // need to special-case it here.
1754 if command.name == DocsSlashCommand::NAME {
1755 return render_docs_slash_command_trailer(
1756 row,
1757 command.clone(),
1758 cx,
1759 );
1760 }
1761
1762 Empty.into_any()
1763 }
1764 };
1765
1766 let start = buffer
1767 .anchor_in_excerpt(excerpt_id, command.source_range.start)
1768 .unwrap();
1769 let end = buffer
1770 .anchor_in_excerpt(excerpt_id, command.source_range.end)
1771 .unwrap();
1772 Crease::new(start..end, placeholder, render_toggle, render_trailer)
1773 }),
1774 cx,
1775 );
1776
1777 let block_ids = editor.insert_blocks(
1778 updated
1779 .iter()
1780 .filter_map(|command| match &command.status {
1781 PendingSlashCommandStatus::Error(error) => {
1782 Some((command, error.clone()))
1783 }
1784 _ => None,
1785 })
1786 .map(|(command, error_message)| BlockProperties {
1787 style: BlockStyle::Fixed,
1788 position: Anchor {
1789 buffer_id: Some(buffer_id),
1790 excerpt_id,
1791 text_anchor: command.source_range.start,
1792 },
1793 height: 1,
1794 disposition: BlockDisposition::Below,
1795 render: slash_command_error_block_renderer(error_message),
1796 }),
1797 None,
1798 cx,
1799 );
1800
1801 self.pending_slash_command_creases.extend(
1802 updated
1803 .iter()
1804 .map(|command| command.source_range.clone())
1805 .zip(crease_ids),
1806 );
1807
1808 self.pending_slash_command_blocks.extend(
1809 updated
1810 .iter()
1811 .map(|command| command.source_range.clone())
1812 .zip(block_ids),
1813 );
1814 })
1815 }
1816 ContextEvent::SlashCommandFinished {
1817 output_range,
1818 sections,
1819 run_commands_in_output,
1820 } => {
1821 self.insert_slash_command_output_sections(sections.iter().cloned(), cx);
1822
1823 if *run_commands_in_output {
1824 let commands = self.context.update(cx, |context, cx| {
1825 context.reparse_slash_commands(cx);
1826 context
1827 .pending_commands_for_range(output_range.clone(), cx)
1828 .to_vec()
1829 });
1830
1831 for command in commands {
1832 self.run_command(
1833 command.source_range,
1834 &command.name,
1835 command.argument.as_deref(),
1836 false,
1837 self.workspace.clone(),
1838 cx,
1839 );
1840 }
1841 }
1842 }
1843 ContextEvent::Operation(_) => {}
1844 ContextEvent::AssistError(error_message) => {
1845 self.error_message = Some(SharedString::from(error_message.clone()));
1846 }
1847 }
1848 }
1849
1850 fn insert_slash_command_output_sections(
1851 &mut self,
1852 sections: impl IntoIterator<Item = SlashCommandOutputSection<language::Anchor>>,
1853 cx: &mut ViewContext<Self>,
1854 ) {
1855 self.editor.update(cx, |editor, cx| {
1856 let buffer = editor.buffer().read(cx).snapshot(cx);
1857 let excerpt_id = *buffer.as_singleton().unwrap().0;
1858 let mut buffer_rows_to_fold = BTreeSet::new();
1859 let mut creases = Vec::new();
1860 for section in sections {
1861 let start = buffer
1862 .anchor_in_excerpt(excerpt_id, section.range.start)
1863 .unwrap();
1864 let end = buffer
1865 .anchor_in_excerpt(excerpt_id, section.range.end)
1866 .unwrap();
1867 let buffer_row = MultiBufferRow(start.to_point(&buffer).row);
1868 buffer_rows_to_fold.insert(buffer_row);
1869 creases.push(Crease::new(
1870 start..end,
1871 FoldPlaceholder {
1872 render: Arc::new({
1873 let editor = cx.view().downgrade();
1874 let icon = section.icon;
1875 let label = section.label.clone();
1876 move |fold_id, fold_range, _cx| {
1877 let editor = editor.clone();
1878 ButtonLike::new(fold_id)
1879 .style(ButtonStyle::Filled)
1880 .layer(ElevationIndex::ElevatedSurface)
1881 .child(Icon::new(icon))
1882 .child(Label::new(label.clone()).single_line())
1883 .on_click(move |_, cx| {
1884 editor
1885 .update(cx, |editor, cx| {
1886 let buffer_start = fold_range
1887 .start
1888 .to_point(&editor.buffer().read(cx).read(cx));
1889 let buffer_row = MultiBufferRow(buffer_start.row);
1890 editor.unfold_at(&UnfoldAt { buffer_row }, cx);
1891 })
1892 .ok();
1893 })
1894 .into_any_element()
1895 }
1896 }),
1897 constrain_width: false,
1898 merge_adjacent: false,
1899 },
1900 render_slash_command_output_toggle,
1901 |_, _, _| Empty.into_any_element(),
1902 ));
1903 }
1904
1905 editor.insert_creases(creases, cx);
1906
1907 for buffer_row in buffer_rows_to_fold.into_iter().rev() {
1908 editor.fold_at(&FoldAt { buffer_row }, cx);
1909 }
1910 });
1911 }
1912
1913 fn handle_editor_event(
1914 &mut self,
1915 _: View<Editor>,
1916 event: &EditorEvent,
1917 cx: &mut ViewContext<Self>,
1918 ) {
1919 match event {
1920 EditorEvent::ScrollPositionChanged { autoscroll, .. } => {
1921 let cursor_scroll_position = self.cursor_scroll_position(cx);
1922 if *autoscroll {
1923 self.scroll_position = cursor_scroll_position;
1924 } else if self.scroll_position != cursor_scroll_position {
1925 self.scroll_position = None;
1926 }
1927 }
1928 EditorEvent::SelectionsChanged { .. } => {
1929 self.scroll_position = self.cursor_scroll_position(cx);
1930 self.update_active_workflow_step_from_cursor(cx);
1931 }
1932 _ => {}
1933 }
1934 cx.emit(event.clone());
1935 }
1936
1937 fn update_active_workflow_step_from_cursor(&mut self, cx: &mut ViewContext<Self>) {
1938 let new_step = self
1939 .workflow_step_range_for_cursor(cx)
1940 .as_ref()
1941 .and_then(|step_range| {
1942 let workflow_step = self
1943 .context
1944 .read(cx)
1945 .workflow_step_for_range(step_range.clone())?;
1946 Some(workflow_step.tagged_range.clone())
1947 });
1948 if new_step.as_ref() != self.active_workflow_step_range.as_ref() {
1949 if let Some(old_step_range) = self.active_workflow_step_range.take() {
1950 self.hide_workflow_step(old_step_range, cx);
1951 }
1952
1953 if let Some(new_step) = new_step {
1954 self.activate_workflow_step(new_step, cx);
1955 }
1956 }
1957 }
1958
1959 fn hide_workflow_step(
1960 &mut self,
1961 step_range: Range<language::Anchor>,
1962 cx: &mut ViewContext<Self>,
1963 ) {
1964 let Some(step_assist) = self.workflow_assists.get_mut(&step_range) else {
1965 return;
1966 };
1967 let Some(editor) = step_assist.editor.upgrade() else {
1968 self.workflow_assists.remove(&step_range);
1969 return;
1970 };
1971
1972 InlineAssistant::update_global(cx, |assistant, cx| {
1973 step_assist.assist_ids.retain(|assist_id| {
1974 match assistant.status_for_assist(*assist_id, cx) {
1975 Some(CodegenStatus::Idle) | None => {
1976 assistant.finish_assist(*assist_id, true, cx);
1977 false
1978 }
1979 _ => true,
1980 }
1981 });
1982 });
1983
1984 if step_assist.assist_ids.is_empty() {
1985 let editor_was_open = step_assist.editor_was_open;
1986 self.workflow_assists.remove(&step_range);
1987 self.workspace
1988 .update(cx, |workspace, cx| {
1989 if let Some(pane) = workspace.pane_for(&editor) {
1990 pane.update(cx, |pane, cx| {
1991 let item_id = editor.entity_id();
1992 if !editor_was_open && pane.is_active_preview_item(item_id) {
1993 pane.close_item_by_id(item_id, SaveIntent::Skip, cx)
1994 .detach_and_log_err(cx);
1995 }
1996 });
1997 }
1998 })
1999 .ok();
2000 }
2001 }
2002
2003 fn activate_workflow_step(
2004 &mut self,
2005 step_range: Range<language::Anchor>,
2006 cx: &mut ViewContext<Self>,
2007 ) -> Option<()> {
2008 if self.scroll_to_existing_workflow_assist(&step_range, cx) {
2009 return None;
2010 }
2011
2012 let step = self
2013 .workflow_step(&step_range, cx)
2014 .with_context(|| format!("could not find workflow step for range {:?}", step_range))
2015 .log_err()?;
2016 let Some(resolved) = step.status.as_resolved() else {
2017 return None;
2018 };
2019
2020 let title = resolved.title.clone();
2021 let suggestions = resolved.suggestions.clone();
2022
2023 if let Some((editor, assist_ids, editor_was_open)) = {
2024 let assistant_panel = self.assistant_panel.upgrade()?;
2025 if suggestions.is_empty() {
2026 return None;
2027 }
2028
2029 let editor;
2030 let mut editor_was_open = false;
2031 let mut suggestion_groups = Vec::new();
2032 if suggestions.len() == 1 && suggestions.values().next().unwrap().len() == 1 {
2033 // If there's only one buffer and one suggestion group, open it directly
2034 let (buffer, groups) = suggestions.into_iter().next().unwrap();
2035 let group = groups.into_iter().next().unwrap();
2036 editor = self
2037 .workspace
2038 .update(cx, |workspace, cx| {
2039 let active_pane = workspace.active_pane().clone();
2040 editor_was_open =
2041 workspace.is_project_item_open::<Editor>(&active_pane, &buffer, cx);
2042 workspace.open_project_item::<Editor>(active_pane, buffer, false, false, cx)
2043 })
2044 .log_err()?;
2045
2046 let (&excerpt_id, _, _) = editor
2047 .read(cx)
2048 .buffer()
2049 .read(cx)
2050 .read(cx)
2051 .as_singleton()
2052 .unwrap();
2053
2054 // Scroll the editor to the suggested assist
2055 editor.update(cx, |editor, cx| {
2056 let multibuffer = editor.buffer().read(cx).snapshot(cx);
2057 let (&excerpt_id, _, buffer) = multibuffer.as_singleton().unwrap();
2058 let anchor = if group.context_range.start.to_offset(buffer) == 0 {
2059 Anchor::min()
2060 } else {
2061 multibuffer
2062 .anchor_in_excerpt(excerpt_id, group.context_range.start)
2063 .unwrap()
2064 };
2065
2066 editor.set_scroll_anchor(
2067 ScrollAnchor {
2068 offset: gpui::Point::default(),
2069 anchor,
2070 },
2071 cx,
2072 );
2073 });
2074
2075 suggestion_groups.push((excerpt_id, group));
2076 } else {
2077 // If there are multiple buffers or suggestion groups, create a multibuffer
2078 let multibuffer = cx.new_model(|cx| {
2079 let replica_id = self.project.read(cx).replica_id();
2080 let mut multibuffer =
2081 MultiBuffer::new(replica_id, Capability::ReadWrite).with_title(title);
2082 for (buffer, groups) in suggestions {
2083 let excerpt_ids = multibuffer.push_excerpts(
2084 buffer,
2085 groups.iter().map(|suggestion_group| ExcerptRange {
2086 context: suggestion_group.context_range.clone(),
2087 primary: None,
2088 }),
2089 cx,
2090 );
2091 suggestion_groups.extend(excerpt_ids.into_iter().zip(groups));
2092 }
2093 multibuffer
2094 });
2095
2096 editor = cx.new_view(|cx| {
2097 Editor::for_multibuffer(multibuffer, Some(self.project.clone()), true, cx)
2098 });
2099 self.workspace
2100 .update(cx, |workspace, cx| {
2101 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, false, cx)
2102 })
2103 .log_err()?;
2104 }
2105
2106 let mut assist_ids = Vec::new();
2107 for (excerpt_id, suggestion_group) in suggestion_groups {
2108 for suggestion in suggestion_group.suggestions {
2109 assist_ids.extend(suggestion.show(
2110 &editor,
2111 excerpt_id,
2112 &self.workspace,
2113 &assistant_panel,
2114 cx,
2115 ));
2116 }
2117 }
2118
2119 if let Some(range) = self.active_workflow_step_range.clone() {
2120 self.workflow_assists.insert(
2121 range,
2122 WorkflowAssist {
2123 assist_ids: assist_ids.clone(),
2124 editor: editor.downgrade(),
2125 editor_was_open,
2126 },
2127 );
2128 }
2129
2130 Some((editor, assist_ids, editor_was_open))
2131 } {
2132 self.workflow_assists.insert(
2133 step_range.clone(),
2134 WorkflowAssist {
2135 assist_ids,
2136 editor_was_open,
2137 editor: editor.downgrade(),
2138 },
2139 );
2140 }
2141
2142 self.active_workflow_step_range = Some(step_range);
2143
2144 Some(())
2145 }
2146
2147 fn active_workflow_step<'a>(&'a self, cx: &'a AppContext) -> Option<&'a crate::WorkflowStep> {
2148 self.active_workflow_step_range
2149 .as_ref()
2150 .and_then(|step_range| {
2151 self.context
2152 .read(cx)
2153 .workflow_step_for_range(step_range.clone())
2154 })
2155 }
2156
2157 fn workflow_step<'a>(
2158 &'a mut self,
2159 step_range: &Range<text::Anchor>,
2160 cx: &'a mut ViewContext<ContextEditor>,
2161 ) -> Option<&'a crate::WorkflowStep> {
2162 self.context
2163 .read(cx)
2164 .workflow_step_for_range(step_range.clone())
2165 }
2166
2167 fn scroll_to_existing_workflow_assist(
2168 &self,
2169 step_range: &Range<language::Anchor>,
2170 cx: &mut ViewContext<Self>,
2171 ) -> bool {
2172 let step_assists = match self.workflow_assists.get(step_range) {
2173 Some(assists) => assists,
2174 None => return false,
2175 };
2176 let editor = match step_assists.editor.upgrade() {
2177 Some(editor) => editor,
2178 None => return false,
2179 };
2180 for assist_id in &step_assists.assist_ids {
2181 match InlineAssistant::global(cx).status_for_assist(*assist_id, cx) {
2182 Some(CodegenStatus::Idle) | None => {}
2183 _ => {
2184 self.workspace
2185 .update(cx, |workspace, cx| {
2186 workspace.activate_item(&editor, false, false, cx);
2187 })
2188 .ok();
2189 InlineAssistant::update_global(cx, |assistant, cx| {
2190 assistant.scroll_to_assist(*assist_id, cx)
2191 });
2192 return true;
2193 }
2194 }
2195 }
2196 false
2197 }
2198
2199 fn handle_editor_search_event(
2200 &mut self,
2201 _: View<Editor>,
2202 event: &SearchEvent,
2203 cx: &mut ViewContext<Self>,
2204 ) {
2205 cx.emit(event.clone());
2206 }
2207
2208 fn cursor_scroll_position(&self, cx: &mut ViewContext<Self>) -> Option<ScrollPosition> {
2209 self.editor.update(cx, |editor, cx| {
2210 let snapshot = editor.snapshot(cx);
2211 let cursor = editor.selections.newest_anchor().head();
2212 let cursor_row = cursor
2213 .to_display_point(&snapshot.display_snapshot)
2214 .row()
2215 .as_f32();
2216 let scroll_position = editor
2217 .scroll_manager
2218 .anchor()
2219 .scroll_position(&snapshot.display_snapshot);
2220
2221 let scroll_bottom = scroll_position.y + editor.visible_line_count().unwrap_or(0.);
2222 if (scroll_position.y..scroll_bottom).contains(&cursor_row) {
2223 Some(ScrollPosition {
2224 cursor,
2225 offset_before_cursor: point(scroll_position.x, cursor_row - scroll_position.y),
2226 })
2227 } else {
2228 None
2229 }
2230 })
2231 }
2232
2233 fn update_message_headers(&mut self, cx: &mut ViewContext<Self>) {
2234 self.editor.update(cx, |editor, cx| {
2235 let buffer = editor.buffer().read(cx).snapshot(cx);
2236 let excerpt_id = *buffer.as_singleton().unwrap().0;
2237 let old_blocks = std::mem::take(&mut self.blocks);
2238 let new_blocks = self
2239 .context
2240 .read(cx)
2241 .messages(cx)
2242 .map(|message| BlockProperties {
2243 position: buffer
2244 .anchor_in_excerpt(excerpt_id, message.anchor)
2245 .unwrap(),
2246 height: 2,
2247 style: BlockStyle::Sticky,
2248 render: Box::new({
2249 let context = self.context.clone();
2250 move |cx| {
2251 let message_id = message.id;
2252 let sender = ButtonLike::new("role")
2253 .style(ButtonStyle::Filled)
2254 .child(match message.role {
2255 Role::User => Label::new("You").color(Color::Default),
2256 Role::Assistant => Label::new("Assistant").color(Color::Info),
2257 Role::System => Label::new("System").color(Color::Warning),
2258 })
2259 .tooltip(|cx| {
2260 Tooltip::with_meta(
2261 "Toggle message role",
2262 None,
2263 "Available roles: You (User), Assistant, System",
2264 cx,
2265 )
2266 })
2267 .on_click({
2268 let context = context.clone();
2269 move |_, cx| {
2270 context.update(cx, |context, cx| {
2271 context.cycle_message_roles(
2272 HashSet::from_iter(Some(message_id)),
2273 cx,
2274 )
2275 })
2276 }
2277 });
2278
2279 let trigger = Button::new("show-error", "Error")
2280 .color(Color::Error)
2281 .selected_label_color(Color::Error)
2282 .selected_icon_color(Color::Error)
2283 .icon(IconName::XCircle)
2284 .icon_color(Color::Error)
2285 .icon_size(IconSize::Small)
2286 .icon_position(IconPosition::Start)
2287 .tooltip(move |cx| {
2288 Tooltip::with_meta(
2289 "Error interacting with language model",
2290 None,
2291 "Click for more details",
2292 cx,
2293 )
2294 });
2295 h_flex()
2296 .id(("message_header", message_id.as_u64()))
2297 .pl(cx.gutter_dimensions.full_width())
2298 .h_11()
2299 .w_full()
2300 .relative()
2301 .gap_1()
2302 .child(sender)
2303 .children(
2304 if let MessageStatus::Error(error) = message.status.clone() {
2305 Some(
2306 PopoverMenu::new("show-error-popover")
2307 .menu(move |cx| {
2308 Some(cx.new_view(|cx| ErrorPopover {
2309 error: error.clone(),
2310 focus_handle: cx.focus_handle(),
2311 }))
2312 })
2313 .trigger(trigger),
2314 )
2315 } else {
2316 None
2317 },
2318 )
2319 .into_any_element()
2320 }
2321 }),
2322 disposition: BlockDisposition::Above,
2323 })
2324 .collect::<Vec<_>>();
2325
2326 editor.remove_blocks(old_blocks, None, cx);
2327 let ids = editor.insert_blocks(new_blocks, None, cx);
2328 self.blocks = HashSet::from_iter(ids);
2329 });
2330 }
2331
2332 fn insert_selection(
2333 workspace: &mut Workspace,
2334 _: &InsertIntoEditor,
2335 cx: &mut ViewContext<Workspace>,
2336 ) {
2337 let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
2338 return;
2339 };
2340 let Some(context_editor_view) = panel.read(cx).active_context_editor(cx) else {
2341 return;
2342 };
2343 let Some(active_editor_view) = workspace
2344 .active_item(cx)
2345 .and_then(|item| item.act_as::<Editor>(cx))
2346 else {
2347 return;
2348 };
2349
2350 let context_editor = context_editor_view.read(cx).editor.read(cx);
2351 let anchor = context_editor.selections.newest_anchor();
2352 let text = context_editor
2353 .buffer()
2354 .read(cx)
2355 .read(cx)
2356 .text_for_range(anchor.range())
2357 .collect::<String>();
2358
2359 // If nothing is selected, don't delete the current selection; instead, be a no-op.
2360 if !text.is_empty() {
2361 active_editor_view.update(cx, |editor, cx| {
2362 editor.insert(&text, cx);
2363 editor.focus(cx);
2364 })
2365 }
2366 }
2367
2368 fn quote_selection(
2369 workspace: &mut Workspace,
2370 _: &QuoteSelection,
2371 cx: &mut ViewContext<Workspace>,
2372 ) {
2373 let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
2374 return;
2375 };
2376 let Some(editor) = workspace
2377 .active_item(cx)
2378 .and_then(|item| item.act_as::<Editor>(cx))
2379 else {
2380 return;
2381 };
2382
2383 let selection = editor.update(cx, |editor, cx| editor.selections.newest_adjusted(cx));
2384 let editor = editor.read(cx);
2385 let buffer = editor.buffer().read(cx).snapshot(cx);
2386 let range = editor::ToOffset::to_offset(&selection.start, &buffer)
2387 ..editor::ToOffset::to_offset(&selection.end, &buffer);
2388 let start_language = buffer.language_at(range.start);
2389 let end_language = buffer.language_at(range.end);
2390 let language_name = if start_language == end_language {
2391 start_language.map(|language| language.code_fence_block_name())
2392 } else {
2393 None
2394 };
2395 let language_name = language_name.as_deref().unwrap_or("");
2396
2397 let selected_text = buffer.text_for_range(range).collect::<String>();
2398 let text = if selected_text.is_empty() {
2399 None
2400 } else {
2401 Some(if language_name == "markdown" {
2402 selected_text
2403 .lines()
2404 .map(|line| format!("> {}", line))
2405 .collect::<Vec<_>>()
2406 .join("\n")
2407 } else {
2408 format!("```{language_name}\n{selected_text}\n```")
2409 })
2410 };
2411
2412 // Activate the panel
2413 if !panel.focus_handle(cx).contains_focused(cx) {
2414 workspace.toggle_panel_focus::<AssistantPanel>(cx);
2415 }
2416
2417 if let Some(text) = text {
2418 panel.update(cx, |_, cx| {
2419 // Wait to create a new context until the workspace is no longer
2420 // being updated.
2421 cx.defer(move |panel, cx| {
2422 if let Some(context) = panel
2423 .active_context_editor(cx)
2424 .or_else(|| panel.new_context(cx))
2425 {
2426 context.update(cx, |context, cx| {
2427 context
2428 .editor
2429 .update(cx, |editor, cx| editor.insert(&text, cx))
2430 });
2431 };
2432 });
2433 });
2434 }
2435 }
2436
2437 fn copy(&mut self, _: &editor::actions::Copy, cx: &mut ViewContext<Self>) {
2438 let editor = self.editor.read(cx);
2439 let context = self.context.read(cx);
2440 if editor.selections.count() == 1 {
2441 let selection = editor.selections.newest::<usize>(cx);
2442 let mut copied_text = String::new();
2443 let mut spanned_messages = 0;
2444 for message in context.messages(cx) {
2445 if message.offset_range.start >= selection.range().end {
2446 break;
2447 } else if message.offset_range.end >= selection.range().start {
2448 let range = cmp::max(message.offset_range.start, selection.range().start)
2449 ..cmp::min(message.offset_range.end, selection.range().end);
2450 if !range.is_empty() {
2451 spanned_messages += 1;
2452 write!(&mut copied_text, "## {}\n\n", message.role).unwrap();
2453 for chunk in context.buffer().read(cx).text_for_range(range) {
2454 copied_text.push_str(chunk);
2455 }
2456 copied_text.push('\n');
2457 }
2458 }
2459 }
2460
2461 if spanned_messages > 1 {
2462 cx.write_to_clipboard(ClipboardItem::new(copied_text));
2463 return;
2464 }
2465 }
2466
2467 cx.propagate();
2468 }
2469
2470 fn split(&mut self, _: &Split, cx: &mut ViewContext<Self>) {
2471 self.context.update(cx, |context, cx| {
2472 let selections = self.editor.read(cx).selections.disjoint_anchors();
2473 for selection in selections.as_ref() {
2474 let buffer = self.editor.read(cx).buffer().read(cx).snapshot(cx);
2475 let range = selection
2476 .map(|endpoint| endpoint.to_offset(&buffer))
2477 .range();
2478 context.split_message(range, cx);
2479 }
2480 });
2481 }
2482
2483 fn save(&mut self, _: &Save, cx: &mut ViewContext<Self>) {
2484 self.context.update(cx, |context, cx| {
2485 context.save(Some(Duration::from_millis(500)), self.fs.clone(), cx)
2486 });
2487 }
2488
2489 fn title(&self, cx: &AppContext) -> Cow<str> {
2490 self.context
2491 .read(cx)
2492 .summary()
2493 .map(|summary| summary.text.clone())
2494 .map(Cow::Owned)
2495 .unwrap_or_else(|| Cow::Borrowed(DEFAULT_TAB_TITLE))
2496 }
2497
2498 fn render_notice(&self, cx: &mut ViewContext<Self>) -> Option<AnyElement> {
2499 use feature_flags::FeatureFlagAppExt;
2500 let nudge = self.assistant_panel.upgrade().map(|assistant_panel| {
2501 assistant_panel.read(cx).show_zed_ai_notice && cx.has_flag::<feature_flags::ZedPro>()
2502 });
2503
2504 if nudge.map_or(false, |value| value) {
2505 Some(
2506 h_flex()
2507 .p_3()
2508 .border_b_1()
2509 .border_color(cx.theme().colors().border_variant)
2510 .bg(cx.theme().colors().editor_background)
2511 .justify_between()
2512 .child(
2513 h_flex()
2514 .gap_3()
2515 .child(Icon::new(IconName::ZedAssistant).color(Color::Accent))
2516 .child(Label::new("Zed AI is here! Get started by signing in →")),
2517 )
2518 .child(
2519 Button::new("sign-in", "Sign in")
2520 .size(ButtonSize::Compact)
2521 .style(ButtonStyle::Filled)
2522 .on_click(cx.listener(|this, _event, cx| {
2523 let client = this
2524 .workspace
2525 .update(cx, |workspace, _| workspace.client().clone())
2526 .log_err();
2527
2528 if let Some(client) = client {
2529 cx.spawn(|this, mut cx| async move {
2530 client.authenticate_and_connect(true, &mut cx).await?;
2531 this.update(&mut cx, |_, cx| cx.notify())
2532 })
2533 .detach_and_log_err(cx)
2534 }
2535 })),
2536 )
2537 .into_any_element(),
2538 )
2539 } else if let Some(configuration_error) = configuration_error(cx) {
2540 let label = match configuration_error {
2541 ConfigurationError::NoProvider => "No LLM provider selected.",
2542 ConfigurationError::ProviderNotAuthenticated => "LLM provider is not configured.",
2543 };
2544 Some(
2545 h_flex()
2546 .p_3()
2547 .border_b_1()
2548 .border_color(cx.theme().colors().border_variant)
2549 .bg(cx.theme().colors().editor_background)
2550 .justify_between()
2551 .child(
2552 h_flex()
2553 .gap_3()
2554 .child(
2555 Icon::new(IconName::ExclamationTriangle)
2556 .size(IconSize::Small)
2557 .color(Color::Warning),
2558 )
2559 .child(Label::new(label)),
2560 )
2561 .child(
2562 Button::new("open-configuration", "Open configuration")
2563 .size(ButtonSize::Compact)
2564 .icon_size(IconSize::Small)
2565 .style(ButtonStyle::Filled)
2566 .on_click({
2567 let focus_handle = self.focus_handle(cx).clone();
2568 move |_event, cx| {
2569 focus_handle.dispatch_action(&ShowConfiguration, cx);
2570 }
2571 }),
2572 )
2573 .into_any_element(),
2574 )
2575 } else {
2576 None
2577 }
2578 }
2579
2580 fn render_send_button(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
2581 let focus_handle = self.focus_handle(cx).clone();
2582 let button_text = match self.active_workflow_step(cx) {
2583 Some(step) => {
2584 if step.status.is_resolved() {
2585 "Apply Changes"
2586 } else {
2587 "Computing Changes..."
2588 }
2589 }
2590 None => "Send",
2591 };
2592
2593 let (style, tooltip) = match token_state(&self.context, cx) {
2594 Some(TokenState::NoTokensLeft { .. }) => (
2595 ButtonStyle::Tinted(TintColor::Negative),
2596 Some(Tooltip::text("Token limit reached", cx)),
2597 ),
2598 Some(TokenState::HasMoreTokens {
2599 over_warn_threshold,
2600 ..
2601 }) => {
2602 let (style, tooltip) = if over_warn_threshold {
2603 (
2604 ButtonStyle::Tinted(TintColor::Warning),
2605 Some(Tooltip::text("Token limit is close to exhaustion", cx)),
2606 )
2607 } else {
2608 (ButtonStyle::Filled, None)
2609 };
2610 (style, tooltip)
2611 }
2612 None => (ButtonStyle::Filled, None),
2613 };
2614
2615 ButtonLike::new("send_button")
2616 .style(style)
2617 .when_some(tooltip, |button, tooltip| {
2618 button.tooltip(move |_| tooltip.clone())
2619 })
2620 .layer(ElevationIndex::ModalSurface)
2621 .children(
2622 KeyBinding::for_action_in(&Assist, &focus_handle, cx)
2623 .map(|binding| binding.into_any_element()),
2624 )
2625 .child(Label::new(button_text))
2626 .on_click(move |_event, cx| {
2627 focus_handle.dispatch_action(&Assist, cx);
2628 })
2629 }
2630
2631 fn workflow_step_range_for_cursor(&self, cx: &AppContext) -> Option<Range<language::Anchor>> {
2632 let newest_cursor = self
2633 .editor
2634 .read(cx)
2635 .selections
2636 .newest_anchor()
2637 .head()
2638 .text_anchor;
2639 let context = self.context.read(cx);
2640 let buffer = context.buffer().read(cx);
2641
2642 let edit_steps = context.workflow_steps();
2643 edit_steps
2644 .binary_search_by(|step| {
2645 let step_range = step.tagged_range.clone();
2646 if newest_cursor.cmp(&step_range.start, buffer).is_lt() {
2647 Ordering::Greater
2648 } else if newest_cursor.cmp(&step_range.end, buffer).is_gt() {
2649 Ordering::Less
2650 } else {
2651 Ordering::Equal
2652 }
2653 })
2654 .ok()
2655 .map(|index| edit_steps[index].tagged_range.clone())
2656 }
2657}
2658
2659impl EventEmitter<EditorEvent> for ContextEditor {}
2660impl EventEmitter<SearchEvent> for ContextEditor {}
2661
2662impl Render for ContextEditor {
2663 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
2664 v_flex()
2665 .key_context("ContextEditor")
2666 .capture_action(cx.listener(ContextEditor::cancel_last_assist))
2667 .capture_action(cx.listener(ContextEditor::save))
2668 .capture_action(cx.listener(ContextEditor::copy))
2669 .capture_action(cx.listener(ContextEditor::cycle_message_role))
2670 .capture_action(cx.listener(ContextEditor::confirm_command))
2671 .on_action(cx.listener(ContextEditor::assist))
2672 .on_action(cx.listener(ContextEditor::split))
2673 .on_action(cx.listener(ContextEditor::debug_edit_steps))
2674 .size_full()
2675 .children(self.render_notice(cx))
2676 .child(
2677 div()
2678 .flex_grow()
2679 .bg(cx.theme().colors().editor_background)
2680 .child(self.editor.clone()),
2681 )
2682 .child(
2683 h_flex().flex_none().relative().child(
2684 h_flex()
2685 .w_full()
2686 .absolute()
2687 .right_4()
2688 .bottom_2()
2689 .justify_end()
2690 .child(self.render_send_button(cx)),
2691 ),
2692 )
2693 }
2694}
2695
2696struct ErrorPopover {
2697 error: SharedString,
2698 focus_handle: FocusHandle,
2699}
2700
2701impl EventEmitter<DismissEvent> for ErrorPopover {}
2702
2703impl FocusableView for ErrorPopover {
2704 fn focus_handle(&self, _: &AppContext) -> FocusHandle {
2705 self.focus_handle.clone()
2706 }
2707}
2708
2709impl Render for ErrorPopover {
2710 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
2711 v_flex()
2712 .mt_2()
2713 .max_w_96()
2714 .py_2()
2715 .px_3()
2716 .gap_0p5()
2717 .elevation_2(cx)
2718 .bg(cx.theme().colors().surface_background)
2719 .occlude()
2720 .child(Label::new("Error interacting with language model").weight(FontWeight::SEMIBOLD))
2721 .child(Label::new(self.error.clone()))
2722 .child(
2723 h_flex().justify_end().mt_1().child(
2724 Button::new("dismiss", "Dismiss")
2725 .on_click(cx.listener(|_, _, cx| cx.emit(DismissEvent))),
2726 ),
2727 )
2728 }
2729}
2730
2731impl FocusableView for ContextEditor {
2732 fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
2733 self.editor.focus_handle(cx)
2734 }
2735}
2736
2737impl Item for ContextEditor {
2738 type Event = editor::EditorEvent;
2739
2740 fn tab_content_text(&self, cx: &WindowContext) -> Option<SharedString> {
2741 Some(util::truncate_and_trailoff(&self.title(cx), MAX_TAB_TITLE_LEN).into())
2742 }
2743
2744 fn to_item_events(event: &Self::Event, mut f: impl FnMut(item::ItemEvent)) {
2745 match event {
2746 EditorEvent::Edited { .. } => {
2747 f(item::ItemEvent::Edit);
2748 }
2749 EditorEvent::TitleChanged => {
2750 f(item::ItemEvent::UpdateTab);
2751 }
2752 _ => {}
2753 }
2754 }
2755
2756 fn tab_tooltip_text(&self, cx: &AppContext) -> Option<SharedString> {
2757 Some(self.title(cx).to_string().into())
2758 }
2759
2760 fn as_searchable(&self, handle: &View<Self>) -> Option<Box<dyn SearchableItemHandle>> {
2761 Some(Box::new(handle.clone()))
2762 }
2763
2764 fn set_nav_history(&mut self, nav_history: pane::ItemNavHistory, cx: &mut ViewContext<Self>) {
2765 self.editor.update(cx, |editor, cx| {
2766 Item::set_nav_history(editor, nav_history, cx)
2767 })
2768 }
2769
2770 fn navigate(&mut self, data: Box<dyn std::any::Any>, cx: &mut ViewContext<Self>) -> bool {
2771 self.editor
2772 .update(cx, |editor, cx| Item::navigate(editor, data, cx))
2773 }
2774
2775 fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
2776 self.editor
2777 .update(cx, |editor, cx| Item::deactivated(editor, cx))
2778 }
2779}
2780
2781impl SearchableItem for ContextEditor {
2782 type Match = <Editor as SearchableItem>::Match;
2783
2784 fn clear_matches(&mut self, cx: &mut ViewContext<Self>) {
2785 self.editor.update(cx, |editor, cx| {
2786 editor.clear_matches(cx);
2787 });
2788 }
2789
2790 fn update_matches(&mut self, matches: &[Self::Match], cx: &mut ViewContext<Self>) {
2791 self.editor
2792 .update(cx, |editor, cx| editor.update_matches(matches, cx));
2793 }
2794
2795 fn query_suggestion(&mut self, cx: &mut ViewContext<Self>) -> String {
2796 self.editor
2797 .update(cx, |editor, cx| editor.query_suggestion(cx))
2798 }
2799
2800 fn activate_match(
2801 &mut self,
2802 index: usize,
2803 matches: &[Self::Match],
2804 cx: &mut ViewContext<Self>,
2805 ) {
2806 self.editor.update(cx, |editor, cx| {
2807 editor.activate_match(index, matches, cx);
2808 });
2809 }
2810
2811 fn select_matches(&mut self, matches: &[Self::Match], cx: &mut ViewContext<Self>) {
2812 self.editor
2813 .update(cx, |editor, cx| editor.select_matches(matches, cx));
2814 }
2815
2816 fn replace(
2817 &mut self,
2818 identifier: &Self::Match,
2819 query: &project::search::SearchQuery,
2820 cx: &mut ViewContext<Self>,
2821 ) {
2822 self.editor
2823 .update(cx, |editor, cx| editor.replace(identifier, query, cx));
2824 }
2825
2826 fn find_matches(
2827 &mut self,
2828 query: Arc<project::search::SearchQuery>,
2829 cx: &mut ViewContext<Self>,
2830 ) -> Task<Vec<Self::Match>> {
2831 self.editor
2832 .update(cx, |editor, cx| editor.find_matches(query, cx))
2833 }
2834
2835 fn active_match_index(
2836 &mut self,
2837 matches: &[Self::Match],
2838 cx: &mut ViewContext<Self>,
2839 ) -> Option<usize> {
2840 self.editor
2841 .update(cx, |editor, cx| editor.active_match_index(matches, cx))
2842 }
2843}
2844
2845impl FollowableItem for ContextEditor {
2846 fn remote_id(&self) -> Option<workspace::ViewId> {
2847 self.remote_id
2848 }
2849
2850 fn to_state_proto(&self, cx: &WindowContext) -> Option<proto::view::Variant> {
2851 let context = self.context.read(cx);
2852 Some(proto::view::Variant::ContextEditor(
2853 proto::view::ContextEditor {
2854 context_id: context.id().to_proto(),
2855 editor: if let Some(proto::view::Variant::Editor(proto)) =
2856 self.editor.read(cx).to_state_proto(cx)
2857 {
2858 Some(proto)
2859 } else {
2860 None
2861 },
2862 },
2863 ))
2864 }
2865
2866 fn from_state_proto(
2867 workspace: View<Workspace>,
2868 id: workspace::ViewId,
2869 state: &mut Option<proto::view::Variant>,
2870 cx: &mut WindowContext,
2871 ) -> Option<Task<Result<View<Self>>>> {
2872 let proto::view::Variant::ContextEditor(_) = state.as_ref()? else {
2873 return None;
2874 };
2875 let Some(proto::view::Variant::ContextEditor(state)) = state.take() else {
2876 unreachable!()
2877 };
2878
2879 let context_id = ContextId::from_proto(state.context_id);
2880 let editor_state = state.editor?;
2881
2882 let (project, panel) = workspace.update(cx, |workspace, cx| {
2883 Some((
2884 workspace.project().clone(),
2885 workspace.panel::<AssistantPanel>(cx)?,
2886 ))
2887 })?;
2888
2889 let context_editor =
2890 panel.update(cx, |panel, cx| panel.open_remote_context(context_id, cx));
2891
2892 Some(cx.spawn(|mut cx| async move {
2893 let context_editor = context_editor.await?;
2894 context_editor
2895 .update(&mut cx, |context_editor, cx| {
2896 context_editor.remote_id = Some(id);
2897 context_editor.editor.update(cx, |editor, cx| {
2898 editor.apply_update_proto(
2899 &project,
2900 proto::update_view::Variant::Editor(proto::update_view::Editor {
2901 selections: editor_state.selections,
2902 pending_selection: editor_state.pending_selection,
2903 scroll_top_anchor: editor_state.scroll_top_anchor,
2904 scroll_x: editor_state.scroll_y,
2905 scroll_y: editor_state.scroll_y,
2906 ..Default::default()
2907 }),
2908 cx,
2909 )
2910 })
2911 })?
2912 .await?;
2913 Ok(context_editor)
2914 }))
2915 }
2916
2917 fn to_follow_event(event: &Self::Event) -> Option<item::FollowEvent> {
2918 Editor::to_follow_event(event)
2919 }
2920
2921 fn add_event_to_update_proto(
2922 &self,
2923 event: &Self::Event,
2924 update: &mut Option<proto::update_view::Variant>,
2925 cx: &WindowContext,
2926 ) -> bool {
2927 self.editor
2928 .read(cx)
2929 .add_event_to_update_proto(event, update, cx)
2930 }
2931
2932 fn apply_update_proto(
2933 &mut self,
2934 project: &Model<Project>,
2935 message: proto::update_view::Variant,
2936 cx: &mut ViewContext<Self>,
2937 ) -> Task<Result<()>> {
2938 self.editor.update(cx, |editor, cx| {
2939 editor.apply_update_proto(project, message, cx)
2940 })
2941 }
2942
2943 fn is_project_item(&self, _cx: &WindowContext) -> bool {
2944 true
2945 }
2946
2947 fn set_leader_peer_id(
2948 &mut self,
2949 leader_peer_id: Option<proto::PeerId>,
2950 cx: &mut ViewContext<Self>,
2951 ) {
2952 self.editor.update(cx, |editor, cx| {
2953 editor.set_leader_peer_id(leader_peer_id, cx)
2954 })
2955 }
2956
2957 fn dedup(&self, existing: &Self, cx: &WindowContext) -> Option<item::Dedup> {
2958 if existing.context.read(cx).id() == self.context.read(cx).id() {
2959 Some(item::Dedup::KeepExisting)
2960 } else {
2961 None
2962 }
2963 }
2964}
2965
2966pub struct ContextEditorToolbarItem {
2967 fs: Arc<dyn Fs>,
2968 workspace: WeakView<Workspace>,
2969 active_context_editor: Option<WeakView<ContextEditor>>,
2970 model_summary_editor: View<Editor>,
2971}
2972
2973impl ContextEditorToolbarItem {
2974 pub fn new(
2975 workspace: &Workspace,
2976 _model_selector_menu_handle: PopoverMenuHandle<ContextMenu>,
2977 model_summary_editor: View<Editor>,
2978 ) -> Self {
2979 Self {
2980 fs: workspace.app_state().fs.clone(),
2981 workspace: workspace.weak_handle(),
2982 active_context_editor: None,
2983 model_summary_editor,
2984 }
2985 }
2986
2987 fn render_inject_context_menu(&self, cx: &mut ViewContext<Self>) -> impl Element {
2988 let commands = SlashCommandRegistry::global(cx);
2989 let active_editor_focus_handle = self.workspace.upgrade().and_then(|workspace| {
2990 Some(
2991 workspace
2992 .read(cx)
2993 .active_item_as::<Editor>(cx)?
2994 .focus_handle(cx),
2995 )
2996 });
2997 let active_context_editor = self.active_context_editor.clone();
2998
2999 PopoverMenu::new("inject-context-menu")
3000 .trigger(IconButton::new("trigger", IconName::Quote).tooltip(|cx| {
3001 Tooltip::with_meta("Insert Context", None, "Type / to insert via keyboard", cx)
3002 }))
3003 .menu(move |cx| {
3004 let active_context_editor = active_context_editor.clone()?;
3005 ContextMenu::build(cx, |mut menu, _cx| {
3006 for command_name in commands.featured_command_names() {
3007 if let Some(command) = commands.command(&command_name) {
3008 let menu_text = SharedString::from(Arc::from(command.menu_text()));
3009 menu = menu.custom_entry(
3010 {
3011 let command_name = command_name.clone();
3012 move |_cx| {
3013 h_flex()
3014 .gap_4()
3015 .w_full()
3016 .justify_between()
3017 .child(Label::new(menu_text.clone()))
3018 .child(
3019 Label::new(format!("/{command_name}"))
3020 .color(Color::Muted),
3021 )
3022 .into_any()
3023 }
3024 },
3025 {
3026 let active_context_editor = active_context_editor.clone();
3027 move |cx| {
3028 active_context_editor
3029 .update(cx, |context_editor, cx| {
3030 context_editor.insert_command(&command_name, cx)
3031 })
3032 .ok();
3033 }
3034 },
3035 )
3036 }
3037 }
3038
3039 if let Some(active_editor_focus_handle) = active_editor_focus_handle.clone() {
3040 menu = menu
3041 .context(active_editor_focus_handle)
3042 .action("Quote Selection", Box::new(QuoteSelection));
3043 }
3044
3045 menu
3046 })
3047 .into()
3048 })
3049 }
3050
3051 fn render_remaining_tokens(&self, cx: &mut ViewContext<Self>) -> Option<impl IntoElement> {
3052 let context = &self
3053 .active_context_editor
3054 .as_ref()?
3055 .upgrade()?
3056 .read(cx)
3057 .context;
3058 let (token_count_color, token_count, max_token_count) = match token_state(context, cx)? {
3059 TokenState::NoTokensLeft {
3060 max_token_count,
3061 token_count,
3062 } => (Color::Error, token_count, max_token_count),
3063 TokenState::HasMoreTokens {
3064 max_token_count,
3065 token_count,
3066 over_warn_threshold,
3067 } => {
3068 let color = if over_warn_threshold {
3069 Color::Warning
3070 } else {
3071 Color::Muted
3072 };
3073 (color, token_count, max_token_count)
3074 }
3075 };
3076 Some(
3077 h_flex()
3078 .gap_0p5()
3079 .child(
3080 Label::new(humanize_token_count(token_count))
3081 .size(LabelSize::Small)
3082 .color(token_count_color),
3083 )
3084 .child(Label::new("/").size(LabelSize::Small).color(Color::Muted))
3085 .child(
3086 Label::new(humanize_token_count(max_token_count))
3087 .size(LabelSize::Small)
3088 .color(Color::Muted),
3089 ),
3090 )
3091 }
3092}
3093
3094impl Render for ContextEditorToolbarItem {
3095 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
3096 let left_side = h_flex()
3097 .gap_2()
3098 .flex_1()
3099 .min_w(rems(DEFAULT_TAB_TITLE.len() as f32))
3100 .when(self.active_context_editor.is_some(), |left_side| {
3101 left_side
3102 .child(
3103 IconButton::new("regenerate-context", IconName::ArrowCircle)
3104 .visible_on_hover("toolbar")
3105 .tooltip(|cx| Tooltip::text("Regenerate Summary", cx))
3106 .on_click(cx.listener(move |_, _, cx| {
3107 cx.emit(ContextEditorToolbarItemEvent::RegenerateSummary)
3108 })),
3109 )
3110 .child(self.model_summary_editor.clone())
3111 });
3112
3113 let active_provider = LanguageModelRegistry::read_global(cx).active_provider();
3114 let active_model = LanguageModelRegistry::read_global(cx).active_model();
3115
3116 let right_side = h_flex()
3117 .gap_2()
3118 .child(ModelSelector::new(
3119 self.fs.clone(),
3120 ButtonLike::new("active-model")
3121 .style(ButtonStyle::Subtle)
3122 .child(
3123 h_flex()
3124 .w_full()
3125 .gap_0p5()
3126 .child(
3127 div()
3128 .overflow_x_hidden()
3129 .flex_grow()
3130 .whitespace_nowrap()
3131 .child(match (active_provider, active_model) {
3132 (Some(provider), Some(model)) => h_flex()
3133 .gap_1()
3134 .child(
3135 Icon::new(provider.icon())
3136 .color(Color::Muted)
3137 .size(IconSize::XSmall),
3138 )
3139 .child(
3140 Label::new(model.name().0)
3141 .size(LabelSize::Small)
3142 .color(Color::Muted),
3143 )
3144 .into_any_element(),
3145 _ => Label::new("No model selected")
3146 .size(LabelSize::Small)
3147 .color(Color::Muted)
3148 .into_any_element(),
3149 }),
3150 )
3151 .child(
3152 Icon::new(IconName::ChevronDown)
3153 .color(Color::Muted)
3154 .size(IconSize::XSmall),
3155 ),
3156 )
3157 .tooltip(move |cx| {
3158 Tooltip::for_action("Change Model", &ToggleModelSelector, cx)
3159 }),
3160 ))
3161 .children(self.render_remaining_tokens(cx))
3162 .child(self.render_inject_context_menu(cx));
3163
3164 h_flex()
3165 .size_full()
3166 .justify_between()
3167 .child(left_side)
3168 .child(right_side)
3169 }
3170}
3171
3172impl ToolbarItemView for ContextEditorToolbarItem {
3173 fn set_active_pane_item(
3174 &mut self,
3175 active_pane_item: Option<&dyn ItemHandle>,
3176 cx: &mut ViewContext<Self>,
3177 ) -> ToolbarItemLocation {
3178 self.active_context_editor = active_pane_item
3179 .and_then(|item| item.act_as::<ContextEditor>(cx))
3180 .map(|editor| editor.downgrade());
3181 cx.notify();
3182 if self.active_context_editor.is_none() {
3183 ToolbarItemLocation::Hidden
3184 } else {
3185 ToolbarItemLocation::PrimaryRight
3186 }
3187 }
3188
3189 fn pane_focus_update(&mut self, _pane_focused: bool, cx: &mut ViewContext<Self>) {
3190 cx.notify();
3191 }
3192}
3193
3194impl EventEmitter<ToolbarItemEvent> for ContextEditorToolbarItem {}
3195
3196enum ContextEditorToolbarItemEvent {
3197 RegenerateSummary,
3198}
3199impl EventEmitter<ContextEditorToolbarItemEvent> for ContextEditorToolbarItem {}
3200
3201pub struct ContextHistory {
3202 picker: View<Picker<SavedContextPickerDelegate>>,
3203 _subscriptions: Vec<Subscription>,
3204 assistant_panel: WeakView<AssistantPanel>,
3205}
3206
3207impl ContextHistory {
3208 fn new(
3209 project: Model<Project>,
3210 context_store: Model<ContextStore>,
3211 assistant_panel: WeakView<AssistantPanel>,
3212 cx: &mut ViewContext<Self>,
3213 ) -> Self {
3214 let picker = cx.new_view(|cx| {
3215 Picker::uniform_list(
3216 SavedContextPickerDelegate::new(project, context_store.clone()),
3217 cx,
3218 )
3219 .modal(false)
3220 .max_height(None)
3221 });
3222
3223 let _subscriptions = vec![
3224 cx.observe(&context_store, |this, _, cx| {
3225 this.picker.update(cx, |picker, cx| picker.refresh(cx));
3226 }),
3227 cx.subscribe(&picker, Self::handle_picker_event),
3228 ];
3229
3230 Self {
3231 picker,
3232 _subscriptions,
3233 assistant_panel,
3234 }
3235 }
3236
3237 fn handle_picker_event(
3238 &mut self,
3239 _: View<Picker<SavedContextPickerDelegate>>,
3240 event: &SavedContextPickerEvent,
3241 cx: &mut ViewContext<Self>,
3242 ) {
3243 let SavedContextPickerEvent::Confirmed(context) = event;
3244 self.assistant_panel
3245 .update(cx, |assistant_panel, cx| match context {
3246 ContextMetadata::Remote(metadata) => {
3247 assistant_panel
3248 .open_remote_context(metadata.id.clone(), cx)
3249 .detach_and_log_err(cx);
3250 }
3251 ContextMetadata::Saved(metadata) => {
3252 assistant_panel
3253 .open_saved_context(metadata.path.clone(), cx)
3254 .detach_and_log_err(cx);
3255 }
3256 })
3257 .ok();
3258 }
3259}
3260
3261impl Render for ContextHistory {
3262 fn render(&mut self, _: &mut ViewContext<Self>) -> impl IntoElement {
3263 div().size_full().child(self.picker.clone())
3264 }
3265}
3266
3267impl FocusableView for ContextHistory {
3268 fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
3269 self.picker.focus_handle(cx)
3270 }
3271}
3272
3273impl EventEmitter<()> for ContextHistory {}
3274
3275impl Item for ContextHistory {
3276 type Event = ();
3277
3278 fn tab_content_text(&self, _cx: &WindowContext) -> Option<SharedString> {
3279 Some("History".into())
3280 }
3281}
3282
3283pub struct ConfigurationView {
3284 focus_handle: FocusHandle,
3285 configuration_views: HashMap<LanguageModelProviderId, AnyView>,
3286 _registry_subscription: Subscription,
3287}
3288
3289impl ConfigurationView {
3290 fn new(cx: &mut ViewContext<Self>) -> Self {
3291 let focus_handle = cx.focus_handle();
3292
3293 let registry_subscription = cx.subscribe(
3294 &LanguageModelRegistry::global(cx),
3295 |this, _, event: &language_model::Event, cx| match event {
3296 language_model::Event::AddedProvider(provider_id) => {
3297 let provider = LanguageModelRegistry::read_global(cx).provider(provider_id);
3298 if let Some(provider) = provider {
3299 this.add_configuration_view(&provider, cx);
3300 }
3301 }
3302 language_model::Event::RemovedProvider(provider_id) => {
3303 this.remove_configuration_view(provider_id);
3304 }
3305 _ => {}
3306 },
3307 );
3308
3309 let mut this = Self {
3310 focus_handle,
3311 configuration_views: HashMap::default(),
3312 _registry_subscription: registry_subscription,
3313 };
3314 this.build_configuration_views(cx);
3315 this
3316 }
3317
3318 fn build_configuration_views(&mut self, cx: &mut ViewContext<Self>) {
3319 let providers = LanguageModelRegistry::read_global(cx).providers();
3320 for provider in providers {
3321 self.add_configuration_view(&provider, cx);
3322 }
3323 }
3324
3325 fn remove_configuration_view(&mut self, provider_id: &LanguageModelProviderId) {
3326 self.configuration_views.remove(provider_id);
3327 }
3328
3329 fn add_configuration_view(
3330 &mut self,
3331 provider: &Arc<dyn LanguageModelProvider>,
3332 cx: &mut ViewContext<Self>,
3333 ) {
3334 let configuration_view = provider.configuration_view(cx);
3335 self.configuration_views
3336 .insert(provider.id(), configuration_view);
3337 }
3338
3339 fn render_provider_view(
3340 &mut self,
3341 provider: &Arc<dyn LanguageModelProvider>,
3342 cx: &mut ViewContext<Self>,
3343 ) -> Div {
3344 let provider_name = provider.name().0.clone();
3345 let configuration_view = self.configuration_views.get(&provider.id()).cloned();
3346
3347 let open_new_context = cx.listener({
3348 let provider = provider.clone();
3349 move |_, _, cx| {
3350 cx.emit(ConfigurationViewEvent::NewProviderContextEditor(
3351 provider.clone(),
3352 ))
3353 }
3354 });
3355
3356 v_flex()
3357 .gap_2()
3358 .child(
3359 h_flex()
3360 .justify_between()
3361 .child(Headline::new(provider_name.clone()).size(HeadlineSize::Small))
3362 .when(provider.is_authenticated(cx), move |this| {
3363 this.child(
3364 h_flex().justify_end().child(
3365 Button::new("new-context", "Open new context")
3366 .icon_position(IconPosition::Start)
3367 .icon(IconName::Plus)
3368 .style(ButtonStyle::Filled)
3369 .layer(ElevationIndex::ModalSurface)
3370 .on_click(open_new_context),
3371 ),
3372 )
3373 }),
3374 )
3375 .child(
3376 div()
3377 .p(Spacing::Large.rems(cx))
3378 .bg(cx.theme().colors().surface_background)
3379 .border_1()
3380 .border_color(cx.theme().colors().border_variant)
3381 .rounded_md()
3382 .when(configuration_view.is_none(), |this| {
3383 this.child(div().child(Label::new(format!(
3384 "No configuration view for {}",
3385 provider_name
3386 ))))
3387 })
3388 .when_some(configuration_view, |this, configuration_view| {
3389 this.child(configuration_view)
3390 }),
3391 )
3392 }
3393}
3394
3395impl Render for ConfigurationView {
3396 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
3397 let providers = LanguageModelRegistry::read_global(cx).providers();
3398 let provider_views = providers
3399 .into_iter()
3400 .map(|provider| self.render_provider_view(&provider, cx))
3401 .collect::<Vec<_>>();
3402
3403 v_flex()
3404 .id("assistant-configuration-view")
3405 .track_focus(&self.focus_handle)
3406 .bg(cx.theme().colors().editor_background)
3407 .size_full()
3408 .overflow_y_scroll()
3409 .child(
3410 v_flex()
3411 .p(Spacing::XXLarge.rems(cx))
3412 .border_b_1()
3413 .border_color(cx.theme().colors().border)
3414 .gap_1()
3415 .child(Headline::new("Configure your Assistant").size(HeadlineSize::Medium))
3416 .child(
3417 Label::new(
3418 "At least one LLM provider must be configured to use the Assistant.",
3419 )
3420 .color(Color::Muted),
3421 ),
3422 )
3423 .child(
3424 v_flex()
3425 .p(Spacing::XXLarge.rems(cx))
3426 .mt_1()
3427 .gap_6()
3428 .size_full()
3429 .children(provider_views),
3430 )
3431 }
3432}
3433
3434pub enum ConfigurationViewEvent {
3435 NewProviderContextEditor(Arc<dyn LanguageModelProvider>),
3436}
3437
3438impl EventEmitter<ConfigurationViewEvent> for ConfigurationView {}
3439
3440impl FocusableView for ConfigurationView {
3441 fn focus_handle(&self, _: &AppContext) -> FocusHandle {
3442 self.focus_handle.clone()
3443 }
3444}
3445
3446impl Item for ConfigurationView {
3447 type Event = ConfigurationViewEvent;
3448
3449 fn tab_content_text(&self, _cx: &WindowContext) -> Option<SharedString> {
3450 Some("Configuration".into())
3451 }
3452}
3453
3454type ToggleFold = Arc<dyn Fn(bool, &mut WindowContext) + Send + Sync>;
3455
3456fn render_slash_command_output_toggle(
3457 row: MultiBufferRow,
3458 is_folded: bool,
3459 fold: ToggleFold,
3460 _cx: &mut WindowContext,
3461) -> AnyElement {
3462 Disclosure::new(
3463 ("slash-command-output-fold-indicator", row.0 as u64),
3464 !is_folded,
3465 )
3466 .selected(is_folded)
3467 .on_click(move |_e, cx| fold(!is_folded, cx))
3468 .into_any_element()
3469}
3470
3471fn render_pending_slash_command_gutter_decoration(
3472 row: MultiBufferRow,
3473 status: &PendingSlashCommandStatus,
3474 confirm_command: Arc<dyn Fn(&mut WindowContext)>,
3475) -> AnyElement {
3476 let mut icon = IconButton::new(
3477 ("slash-command-gutter-decoration", row.0),
3478 ui::IconName::TriangleRight,
3479 )
3480 .on_click(move |_e, cx| confirm_command(cx))
3481 .icon_size(ui::IconSize::Small)
3482 .size(ui::ButtonSize::None);
3483
3484 match status {
3485 PendingSlashCommandStatus::Idle => {
3486 icon = icon.icon_color(Color::Muted);
3487 }
3488 PendingSlashCommandStatus::Running { .. } => {
3489 icon = icon.selected(true);
3490 }
3491 PendingSlashCommandStatus::Error(_) => icon = icon.icon_color(Color::Error),
3492 }
3493
3494 icon.into_any_element()
3495}
3496
3497fn render_docs_slash_command_trailer(
3498 row: MultiBufferRow,
3499 command: PendingSlashCommand,
3500 cx: &mut WindowContext,
3501) -> AnyElement {
3502 let Some(argument) = command.argument else {
3503 return Empty.into_any();
3504 };
3505
3506 let args = DocsSlashCommandArgs::parse(&argument);
3507
3508 let Some(store) = args
3509 .provider()
3510 .and_then(|provider| IndexedDocsStore::try_global(provider, cx).ok())
3511 else {
3512 return Empty.into_any();
3513 };
3514
3515 let Some(package) = args.package() else {
3516 return Empty.into_any();
3517 };
3518
3519 let mut children = Vec::new();
3520
3521 if store.is_indexing(&package) {
3522 children.push(
3523 div()
3524 .id(("crates-being-indexed", row.0))
3525 .child(Icon::new(IconName::ArrowCircle).with_animation(
3526 "arrow-circle",
3527 Animation::new(Duration::from_secs(4)).repeat(),
3528 |icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
3529 ))
3530 .tooltip({
3531 let package = package.clone();
3532 move |cx| Tooltip::text(format!("Indexing {package}…"), cx)
3533 })
3534 .into_any_element(),
3535 );
3536 }
3537
3538 if let Some(latest_error) = store.latest_error_for_package(&package) {
3539 children.push(
3540 div()
3541 .id(("latest-error", row.0))
3542 .child(
3543 Icon::new(IconName::ExclamationTriangle)
3544 .size(IconSize::Small)
3545 .color(Color::Warning),
3546 )
3547 .tooltip(move |cx| Tooltip::text(format!("Failed to index: {latest_error}"), cx))
3548 .into_any_element(),
3549 )
3550 }
3551
3552 let is_indexing = store.is_indexing(&package);
3553 let latest_error = store.latest_error_for_package(&package);
3554
3555 if !is_indexing && latest_error.is_none() {
3556 return Empty.into_any();
3557 }
3558
3559 h_flex().gap_2().children(children).into_any_element()
3560}
3561
3562fn make_lsp_adapter_delegate(
3563 project: &Model<Project>,
3564 cx: &mut AppContext,
3565) -> Result<Arc<dyn LspAdapterDelegate>> {
3566 project.update(cx, |project, cx| {
3567 // TODO: Find the right worktree.
3568 let worktree = project
3569 .worktrees(cx)
3570 .next()
3571 .ok_or_else(|| anyhow!("no worktrees when constructing ProjectLspAdapterDelegate"))?;
3572 Ok(ProjectLspAdapterDelegate::new(project, &worktree, cx) as Arc<dyn LspAdapterDelegate>)
3573 })
3574}
3575
3576fn slash_command_error_block_renderer(message: String) -> RenderBlock {
3577 Box::new(move |_| {
3578 div()
3579 .pl_6()
3580 .child(
3581 Label::new(format!("error: {}", message))
3582 .single_line()
3583 .color(Color::Error),
3584 )
3585 .into_any()
3586 })
3587}
3588
3589enum TokenState {
3590 NoTokensLeft {
3591 max_token_count: usize,
3592 token_count: usize,
3593 },
3594 HasMoreTokens {
3595 max_token_count: usize,
3596 token_count: usize,
3597 over_warn_threshold: bool,
3598 },
3599}
3600
3601fn token_state(context: &Model<Context>, cx: &AppContext) -> Option<TokenState> {
3602 const WARNING_TOKEN_THRESHOLD: f32 = 0.8;
3603
3604 let model = LanguageModelRegistry::read_global(cx).active_model()?;
3605 let token_count = context.read(cx).token_count()?;
3606 let max_token_count = model.max_token_count();
3607
3608 let remaining_tokens = max_token_count as isize - token_count as isize;
3609 let token_state = if remaining_tokens <= 0 {
3610 TokenState::NoTokensLeft {
3611 max_token_count,
3612 token_count,
3613 }
3614 } else {
3615 let over_warn_threshold =
3616 token_count as f32 / max_token_count as f32 >= WARNING_TOKEN_THRESHOLD;
3617 TokenState::HasMoreTokens {
3618 max_token_count,
3619 token_count,
3620 over_warn_threshold,
3621 }
3622 };
3623 Some(token_state)
3624}
3625
3626enum ConfigurationError {
3627 NoProvider,
3628 ProviderNotAuthenticated,
3629}
3630
3631fn configuration_error(cx: &AppContext) -> Option<ConfigurationError> {
3632 let provider = LanguageModelRegistry::read_global(cx).active_provider();
3633 let is_authenticated = provider
3634 .as_ref()
3635 .map_or(false, |provider| provider.is_authenticated(cx));
3636
3637 if provider.is_some() && is_authenticated {
3638 return None;
3639 }
3640
3641 if provider.is_none() {
3642 return Some(ConfigurationError::NoProvider);
3643 }
3644
3645 if !is_authenticated {
3646 return Some(ConfigurationError::ProviderNotAuthenticated);
3647 }
3648
3649 None
3650}