Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T05:01:34.255Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

edit_prediction_button.rs

1734 lines · 72.5 KB · rust
1use anyhow::Result;
2use client::{Client, UserStore, zed_urls};
3use cloud_llm_client::UsageLimit;
4use codestral::{self, CodestralEditPredictionDelegate};
5use copilot::Status;
6use edit_prediction::EditPredictionStore;
7use edit_prediction_types::EditPredictionDelegateHandle;
8use editor::{
9    Editor, MultiBufferOffset, SelectionEffects, actions::ShowEditPrediction, scroll::Autoscroll,
10};
11use feature_flags::FeatureFlagAppExt;
12use fs::Fs;
13use gpui::{
14    Action, Anchor, Animation, AnimationExt, App, AsyncWindowContext, Entity, FocusHandle,
15    Focusable, IntoElement, ParentElement, Render, Subscription, TaskExt, WeakEntity, actions, div,
16    ease_in_out, pulsating_between,
17};
18use indoc::indoc;
19use language::{
20    EditPredictionsMode, File, Language,
21    language_settings::{
22        AllLanguageSettings, EditPredictionProvider, LanguageSettings, all_language_settings,
23    },
24};
25use project::{DisableAiSettings, Project};
26use regex::Regex;
27use settings::{Settings, SettingsStore, update_settings_file};
28use std::{
29    rc::Rc,
30    sync::{Arc, LazyLock},
31    time::Duration,
32};
33use ui::{
34    Clickable, ContextMenu, ContextMenuEntry, DocumentationSide, IconButton, IconButtonShape,
35    Indicator, PopoverMenu, PopoverMenuHandle, ProgressBar, Tooltip, prelude::*,
36};
37use util::ResultExt as _;
38
39use workspace::{
40    HideStatusItem, StatusItemView, Toast, Workspace, create_and_open_local_file, item::ItemHandle,
41    notifications::NotificationId,
42};
43use zed_actions::{OpenBrowser, OpenSettingsAt};
44
45use crate::{RatePredictions, rate_prediction_modal::PredictEditsRatePredictionsFeatureFlag};
46
47actions!(
48    edit_prediction,
49    [
50        /// Toggles the edit prediction menu.
51        ToggleMenu
52    ]
53);
54
55const COPILOT_SETTINGS_PATH: &str = "/settings/copilot";
56const COPILOT_SETTINGS_URL: &str = concat!("https://github.com", "/settings/copilot");
57const PRIVACY_DOCS: &str = "https://zed.dev/docs/ai/privacy-and-security";
58
59struct CopilotErrorToast;
60
61pub struct EditPredictionButton {
62    editor_subscription: Option<(Subscription, usize)>,
63    editor_enabled: Option<bool>,
64    editor_show_predictions: bool,
65    editor_focus_handle: Option<FocusHandle>,
66    language: Option<Arc<Language>>,
67    file: Option<Arc<dyn File>>,
68    edit_prediction_provider: Option<Arc<dyn EditPredictionDelegateHandle>>,
69    fs: Arc<dyn Fs>,
70    user_store: Entity<UserStore>,
71    popover_menu_handle: PopoverMenuHandle<ContextMenu>,
72    project: WeakEntity<Project>,
73}
74
75impl Render for EditPredictionButton {
76    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
77        // Return empty div if AI is disabled
78        if DisableAiSettings::get_global(cx).disable_ai {
79            return div().hidden();
80        }
81
82        let language_settings = all_language_settings(None, cx);
83
84        match language_settings.edit_predictions.provider {
85            EditPredictionProvider::Copilot => {
86                let Some(copilot) = EditPredictionStore::try_global(cx)
87                    .and_then(|store| store.read(cx).copilot_for_project(&self.project.upgrade()?))
88                else {
89                    return div().hidden();
90                };
91                let status = copilot.read(cx).status();
92
93                let enabled = self.editor_enabled.unwrap_or(false);
94
95                let icon = match status {
96                    Status::Error(_) => IconName::CopilotError,
97                    Status::Authorized => {
98                        if enabled {
99                            IconName::Copilot
100                        } else {
101                            IconName::CopilotDisabled
102                        }
103                    }
104                    _ => IconName::CopilotInit,
105                };
106
107                if let Status::Error(e) = status {
108                    return div().child(
109                        IconButton::new("copilot-error", icon)
110                            .icon_size(IconSize::Small)
111                            .tab_index(0isize)
112                            .aria_label("GitHub Copilot")
113                            .on_click(cx.listener(move |_, _, window, cx| {
114                                if let Some(workspace) = Workspace::for_window(window, cx) {
115                                    workspace.update(cx, |workspace, cx| {
116                                        let copilot = copilot.clone();
117                                        workspace.show_toast(
118                                            Toast::new(
119                                                NotificationId::unique::<CopilotErrorToast>(),
120                                                format!("Copilot can't be started: {}", e),
121                                            )
122                                            .on_click(
123                                                "Reinstall Copilot",
124                                                move |window, cx| {
125                                                    copilot_ui::reinstall_and_sign_in(
126                                                        copilot.clone(),
127                                                        window,
128                                                        cx,
129                                                    )
130                                                },
131                                            ),
132                                            cx,
133                                        );
134                                    });
135                                }
136                            }))
137                            .tooltip(|_window, cx| {
138                                Tooltip::for_action("GitHub Copilot", &ToggleMenu, cx)
139                            }),
140                    );
141                }
142                let this = cx.weak_entity();
143                let project = self.project.clone();
144                let file = self.file.clone();
145                let language = self.language.clone();
146                div().child(
147                    PopoverMenu::new("copilot")
148                        .on_open({
149                            let file = file.clone();
150                            let language = language;
151                            let project = project.clone();
152                            Rc::new(move |_window, cx| {
153                                emit_edit_prediction_menu_opened(
154                                    "copilot", &file, &language, &project, cx,
155                                );
156                            })
157                        })
158                        .menu(move |window, cx| {
159                            let current_status = EditPredictionStore::try_global(cx)
160                                .and_then(|store| {
161                                    store.read(cx).copilot_for_project(&project.upgrade()?)
162                                })?
163                                .read(cx)
164                                .status();
165                            match current_status {
166                                Status::Authorized => this.update(cx, |this, cx| {
167                                    this.build_copilot_context_menu(window, cx)
168                                }),
169                                _ => this.update(cx, |this, cx| {
170                                    this.build_copilot_start_menu(window, cx)
171                                }),
172                            }
173                            .ok()
174                        })
175                        .anchor(Anchor::BottomRight)
176                        .trigger_with_tooltip(
177                            IconButton::new("copilot-icon", icon)
178                                .tab_index(0isize)
179                                .aria_label("GitHub Copilot"),
180                            |_window, cx| Tooltip::for_action("GitHub Copilot", &ToggleMenu, cx),
181                        )
182                        .with_handle(self.popover_menu_handle.clone()),
183                )
184            }
185            EditPredictionProvider::Codestral => {
186                let enabled = self.editor_enabled.unwrap_or(true);
187                let has_api_key = codestral::codestral_api_key(cx).is_some();
188                let this = cx.weak_entity();
189                let file = self.file.clone();
190                let language = self.language.clone();
191                let project = self.project.clone();
192
193                let tooltip_meta = if has_api_key {
194                    "Powered by Codestral"
195                } else {
196                    "Missing API key for Codestral"
197                };
198
199                div().child(
200                    PopoverMenu::new("codestral")
201                        .on_open({
202                            let file = file.clone();
203                            let language = language;
204                            let project = project;
205                            Rc::new(move |_window, cx| {
206                                emit_edit_prediction_menu_opened(
207                                    "codestral",
208                                    &file,
209                                    &language,
210                                    &project,
211                                    cx,
212                                );
213                            })
214                        })
215                        .menu(move |window, cx| {
216                            this.update(cx, |this, cx| {
217                                this.build_codestral_context_menu(window, cx)
218                            })
219                            .ok()
220                        })
221                        .anchor(Anchor::BottomRight)
222                        .trigger_with_tooltip(
223                            IconButton::new("codestral-icon", IconName::AiMistral)
224                                .shape(IconButtonShape::Square)
225                                .tab_index(0isize)
226                                .aria_label("Edit Prediction")
227                                .when(!has_api_key, |this| {
228                                    this.indicator(Indicator::dot().color(Color::Error))
229                                        .indicator_border_color(Some(
230                                            cx.theme().colors().status_bar_background,
231                                        ))
232                                })
233                                .when(has_api_key && !enabled, |this| {
234                                    this.indicator(Indicator::dot().color(Color::Ignored))
235                                        .indicator_border_color(Some(
236                                            cx.theme().colors().status_bar_background,
237                                        ))
238                                }),
239                            move |_window, cx| {
240                                Tooltip::with_meta(
241                                    "Edit Prediction",
242                                    Some(&ToggleMenu),
243                                    tooltip_meta,
244                                    cx,
245                                )
246                            },
247                        )
248                        .with_handle(self.popover_menu_handle.clone()),
249                )
250            }
251            EditPredictionProvider::OpenAiCompatibleApi => {
252                let enabled = self.editor_enabled.unwrap_or(true);
253                let this = cx.weak_entity();
254
255                div().child(
256                    PopoverMenu::new("openai-compatible-api")
257                        .menu(move |window, cx| {
258                            this.update(cx, |this, cx| {
259                                this.build_edit_prediction_context_menu(
260                                    EditPredictionProvider::OpenAiCompatibleApi,
261                                    window,
262                                    cx,
263                                )
264                            })
265                            .ok()
266                        })
267                        .anchor(Anchor::BottomRight)
268                        .trigger(
269                            IconButton::new("openai-compatible-api-icon", IconName::AiOpenAiCompat)
270                                .shape(IconButtonShape::Square)
271                                .tab_index(0isize)
272                                .aria_label("Edit Prediction")
273                                .when(!enabled, |this| {
274                                    this.indicator(Indicator::dot().color(Color::Ignored))
275                                        .indicator_border_color(Some(
276                                            cx.theme().colors().status_bar_background,
277                                        ))
278                                }),
279                        )
280                        .with_handle(self.popover_menu_handle.clone()),
281                )
282            }
283            EditPredictionProvider::Ollama => {
284                let enabled = self.editor_enabled.unwrap_or(true);
285                let this = cx.weak_entity();
286
287                div().child(
288                    PopoverMenu::new("ollama")
289                        .menu(move |window, cx| {
290                            this.update(cx, |this, cx| {
291                                this.build_edit_prediction_context_menu(
292                                    EditPredictionProvider::Ollama,
293                                    window,
294                                    cx,
295                                )
296                            })
297                            .ok()
298                        })
299                        .anchor(Anchor::BottomRight)
300                        .trigger_with_tooltip(
301                            IconButton::new("ollama-icon", IconName::AiOllama)
302                                .shape(IconButtonShape::Square)
303                                .tab_index(0isize)
304                                .aria_label("Edit Prediction")
305                                .when(!enabled, |this| {
306                                    this.indicator(Indicator::dot().color(Color::Ignored))
307                                        .indicator_border_color(Some(
308                                            cx.theme().colors().status_bar_background,
309                                        ))
310                                }),
311                            move |_window, cx| {
312                                let settings = all_language_settings(None, cx);
313                                let tooltip_meta = match settings.edit_predictions.ollama.as_ref() {
314                                    Some(settings) if !settings.model.trim().is_empty() => {
315                                        format!("Powered by Ollama ({})", settings.model)
316                                    }
317                                    _ => {
318                                        "Ollama model not configured — configure a model before use"
319                                            .to_string()
320                                    }
321                                };
322
323                                Tooltip::with_meta(
324                                    "Edit Prediction",
325                                    Some(&ToggleMenu),
326                                    tooltip_meta,
327                                    cx,
328                                )
329                            },
330                        )
331                        .with_handle(self.popover_menu_handle.clone()),
332                )
333            }
334            provider @ (EditPredictionProvider::Zed | EditPredictionProvider::Mercury) => {
335                let enabled = self.editor_enabled.unwrap_or(true);
336                let file = self.file.clone();
337                let language = self.language.clone();
338                let project = self.project.clone();
339                let provider_name: &'static str = match provider {
340                    EditPredictionProvider::Zed => "zed",
341                    _ => "unknown",
342                };
343                let icons = self
344                    .edit_prediction_provider
345                    .as_ref()
346                    .map(|p| p.icons(cx))
347                    .unwrap_or_else(|| {
348                        edit_prediction_types::EditPredictionIconSet::new(IconName::OmegaPredict)
349                    });
350
351                let ep_icon;
352                let tooltip_meta;
353                let mut missing_token = false;
354
355                match provider {
356                    EditPredictionProvider::Mercury => {
357                        ep_icon = if enabled { icons.base } else { icons.disabled };
358                        let mercury_has_error =
359                            edit_prediction::EditPredictionStore::try_global(cx).is_some_and(
360                                |ep_store| ep_store.read(cx).mercury_has_payment_required_error(),
361                            );
362                        missing_token = edit_prediction::EditPredictionStore::try_global(cx)
363                            .is_some_and(|ep_store| !ep_store.read(cx).has_mercury_api_token(cx));
364                        tooltip_meta = if missing_token {
365                            "Missing API key for Mercury"
366                        } else if mercury_has_error {
367                            "Mercury free tier limit reached"
368                        } else {
369                            "Powered by Mercury"
370                        };
371                    }
372                    _ => {
373                        ep_icon = if enabled { icons.base } else { icons.disabled };
374                        tooltip_meta = "Powered by Zeta"
375                    }
376                };
377
378                if edit_prediction::should_show_upsell_modal(cx) {
379                    let tooltip_meta = if self.user_store.read(cx).current_user().is_some() {
380                        "Choose a Plan"
381                    } else {
382                        "Configure a Provider"
383                    };
384
385                    return div().child(
386                        IconButton::new("zed-predict-pending-button", ep_icon)
387                            .shape(IconButtonShape::Square)
388                            .tab_index(0isize)
389                            .aria_label("Edit Predictions")
390                            .indicator(Indicator::dot().color(Color::Muted))
391                            .indicator_border_color(Some(cx.theme().colors().status_bar_background))
392                            .tooltip(move |_window, cx| {
393                                Tooltip::with_meta("Edit Predictions", None, tooltip_meta, cx)
394                            })
395                            .on_click(cx.listener(move |_, _, window, cx| {
396                                telemetry::event!(
397                                    "Pending ToS Clicked",
398                                    source = "Edit Prediction Status Button"
399                                );
400                                window.dispatch_action(
401                                    zed_actions::OpenOmegaPredictOnboarding.boxed_clone(),
402                                    cx,
403                                );
404                            })),
405                    );
406                }
407
408                let mut over_limit = false;
409
410                if let Some(usage) = self
411                    .edit_prediction_provider
412                    .as_ref()
413                    .and_then(|provider| provider.usage(cx))
414                {
415                    over_limit = usage.over_limit()
416                }
417
418                let show_editor_predictions = self.editor_show_predictions;
419                let user = self.user_store.read(cx).current_user();
420
421                let mercury_has_error = matches!(provider, EditPredictionProvider::Mercury)
422                    && edit_prediction::EditPredictionStore::try_global(cx).is_some_and(
423                        |ep_store| ep_store.read(cx).mercury_has_payment_required_error(),
424                    );
425
426                let indicator_color = if missing_token || mercury_has_error {
427                    Some(Color::Error)
428                } else if enabled && (!show_editor_predictions || over_limit) {
429                    Some(if over_limit {
430                        Color::Error
431                    } else {
432                        Color::Muted
433                    })
434                } else {
435                    None
436                };
437
438                let zed_cloud_needs_sign_in =
439                    matches!(provider, EditPredictionProvider::Zed) && user.is_none();
440                let provider_unavailable =
441                    missing_token || mercury_has_error || zed_cloud_needs_sign_in;
442
443                let icon_button = IconButton::new("zed-predict-pending-button", ep_icon)
444                    .shape(IconButtonShape::Square)
445                    .tab_index(0isize)
446                    .aria_label("Edit Prediction")
447                    .when_some(indicator_color, |this, color| {
448                        this.indicator(Indicator::dot().color(color))
449                            .indicator_border_color(Some(cx.theme().colors().status_bar_background))
450                    })
451                    .when(!self.popover_menu_handle.is_deployed(), |element| {
452                        element.tooltip(move |_window, cx| {
453                            let description = if !enabled {
454                                "Disabled For This File"
455                            } else if zed_cloud_needs_sign_in {
456                                "Sign In Or Configure a Provider"
457                            } else if provider_unavailable || show_editor_predictions {
458                                tooltip_meta
459                            } else {
460                                "Enable to Use"
461                            };
462
463                            Tooltip::with_meta(
464                                "Edit Prediction",
465                                Some(&ToggleMenu),
466                                description,
467                                cx,
468                            )
469                        })
470                    });
471
472                let this = cx.weak_entity();
473
474                let mut popover_menu = PopoverMenu::new("edit-prediction")
475                    .on_open({
476                        let file = file.clone();
477                        let language = language;
478                        let project = project;
479                        Rc::new(move |_window, cx| {
480                            emit_edit_prediction_menu_opened(
481                                provider_name,
482                                &file,
483                                &language,
484                                &project,
485                                cx,
486                            );
487                        })
488                    })
489                    .map(|popover_menu| {
490                        let this = this.clone();
491                        popover_menu.menu(move |window, cx| {
492                            this.update(cx, |this, cx| {
493                                this.build_edit_prediction_context_menu(provider, window, cx)
494                            })
495                            .ok()
496                        })
497                    })
498                    .anchor(Anchor::BottomRight)
499                    .with_handle(self.popover_menu_handle.clone());
500
501                let is_refreshing = self
502                    .edit_prediction_provider
503                    .as_ref()
504                    .is_some_and(|provider| provider.is_refreshing(cx));
505
506                if is_refreshing {
507                    popover_menu = popover_menu.trigger(
508                        icon_button.with_animation(
509                            "pulsating-label",
510                            Animation::new(Duration::from_secs(2))
511                                .repeat()
512                                .with_easing(pulsating_between(0.2, 1.0)),
513                            |icon_button, delta| icon_button.alpha(delta),
514                        ),
515                    );
516                } else {
517                    popover_menu = popover_menu.trigger(icon_button);
518                }
519
520                div().child(popover_menu.into_any_element())
521            }
522
523            EditPredictionProvider::None => div().hidden(),
524        }
525    }
526}
527
528impl EditPredictionButton {
529    pub fn new(
530        fs: Arc<dyn Fs>,
531        user_store: Entity<UserStore>,
532        popover_menu_handle: PopoverMenuHandle<ContextMenu>,
533        project: Entity<Project>,
534        cx: &mut Context<Self>,
535    ) -> Self {
536        let copilot = EditPredictionStore::try_global(cx).and_then(|store| {
537            store.update(cx, |this, cx| this.start_copilot_for_project(&project, cx))
538        });
539        if let Some(copilot) = copilot {
540            cx.observe(&copilot, |_, _, cx| cx.notify()).detach()
541        }
542
543        cx.observe_global::<SettingsStore>(move |_, cx| cx.notify())
544            .detach();
545
546        cx.observe_global::<EditPredictionStore>(move |_, cx| cx.notify())
547            .detach();
548
549        edit_prediction::ollama::ensure_authenticated(cx);
550        let mercury_api_token_task = edit_prediction::mercury::load_mercury_api_token(cx);
551        let open_ai_compatible_api_token_task =
552            edit_prediction::open_ai_compatible::load_open_ai_compatible_api_token(cx);
553
554        cx.spawn(async move |this, cx| {
555            _ = futures::join!(mercury_api_token_task, open_ai_compatible_api_token_task);
556            this.update(cx, |_, cx| {
557                cx.notify();
558            })
559            .ok();
560        })
561        .detach();
562
563        CodestralEditPredictionDelegate::ensure_api_key_loaded(cx);
564
565        Self {
566            editor_subscription: None,
567            editor_enabled: None,
568            editor_show_predictions: true,
569            editor_focus_handle: None,
570            language: None,
571            file: None,
572            edit_prediction_provider: None,
573            user_store,
574            popover_menu_handle,
575            project: project.downgrade(),
576            fs,
577        }
578    }
579
580    fn add_provider_switching_section(
581        &self,
582        mut menu: ContextMenu,
583        current_provider: EditPredictionProvider,
584        cx: &mut App,
585    ) -> ContextMenu {
586        let organization_configuration = self
587            .user_store
588            .read(cx)
589            .current_organization_configuration();
590
591        let is_zed_provider_disabled = organization_configuration
592            .is_some_and(|configuration| !configuration.edit_prediction.is_enabled);
593
594        let available_providers = get_available_providers(cx);
595
596        let providers: Vec<_> = available_providers
597            .into_iter()
598            .filter(|p| *p != EditPredictionProvider::None)
599            .collect();
600
601        if !providers.is_empty() {
602            menu = menu.separator().header("Providers");
603
604            for provider in providers {
605                let Some(name) = provider.display_name() else {
606                    continue;
607                };
608                let is_current = provider == current_provider;
609                let is_disabled_zed_provider =
610                    provider == EditPredictionProvider::Zed && is_zed_provider_disabled;
611                let fs = self.fs.clone();
612
613                menu = menu.item(
614                    ContextMenuEntry::new(name)
615                        .toggleable(IconPosition::Start, is_current && !is_disabled_zed_provider)
616                        .disabled(is_disabled_zed_provider)
617                        .when(is_disabled_zed_provider, |item| {
618                            item.documentation_aside(DocumentationSide::Left, move |_cx| {
619                                Label::new("Edit predictions are disabled for this organization.")
620                                    .into_any_element()
621                            })
622                        })
623                        .handler(move |_, cx| {
624                            set_completion_provider(fs.clone(), cx, provider);
625                        }),
626                )
627            }
628        }
629
630        menu
631    }
632
633    fn add_configure_providers_item(&self, menu: ContextMenu) -> ContextMenu {
634        menu.separator().item(
635            ContextMenuEntry::new("Configure Providers")
636                .icon(IconName::Settings)
637                .icon_position(IconPosition::Start)
638                .icon_color(Color::Muted)
639                .handler(move |window, cx| {
640                    telemetry::event!(
641                        "Edit Prediction Menu Action",
642                        action = "configure_providers",
643                    );
644                    window.dispatch_action(
645                        OpenSettingsAt {
646                            path: "edit_predictions.providers".to_string(),
647                            target: None,
648                        }
649                        .boxed_clone(),
650                        cx,
651                    );
652                }),
653        )
654    }
655
656    pub fn build_copilot_start_menu(
657        &mut self,
658        window: &mut Window,
659        cx: &mut Context<Self>,
660    ) -> Entity<ContextMenu> {
661        let fs = self.fs.clone();
662        let project = self.project.clone();
663        ContextMenu::build(window, cx, |menu, _, cx| {
664            let menu = menu
665                .entry("Sign In to Copilot", None, move |window, cx| {
666                    telemetry::event!(
667                        "Edit Prediction Menu Action",
668                        action = "sign_in",
669                        provider = "copilot",
670                    );
671                    if let Some(copilot) = EditPredictionStore::try_global(cx).and_then(|store| {
672                        store.update(cx, |this, cx| {
673                            this.start_copilot_for_project(&project.upgrade()?, cx)
674                        })
675                    }) {
676                        copilot_ui::initiate_sign_in(copilot, window, cx);
677                    }
678                })
679                .entry("Disable Copilot", None, {
680                    let fs = fs.clone();
681                    move |_window, cx| {
682                        telemetry::event!(
683                            "Edit Prediction Menu Action",
684                            action = "disable_provider",
685                            provider = "copilot",
686                        );
687                        hide_copilot(fs.clone(), cx)
688                    }
689                });
690
691            let menu =
692                self.add_provider_switching_section(menu, EditPredictionProvider::Copilot, cx);
693            let menu = self.add_configure_providers_item(menu);
694            menu
695        })
696    }
697
698    pub fn build_language_settings_menu(
699        &self,
700        mut menu: ContextMenu,
701        window: &Window,
702        cx: &mut App,
703    ) -> ContextMenu {
704        let fs = self.fs.clone();
705        let line_height = window.line_height();
706
707        menu = menu.header("Show Edit Predictions For");
708
709        let language_state = self.language.as_ref().map(|language| {
710            (
711                language.clone(),
712                LanguageSettings::resolve(None, Some(&language.name()), cx).show_edit_predictions,
713            )
714        });
715
716        if let Some(editor_focus_handle) = self.editor_focus_handle.clone() {
717            let entry = ContextMenuEntry::new("This Buffer")
718                .toggleable(IconPosition::Start, self.editor_show_predictions)
719                .action(Box::new(editor::actions::ToggleEditPrediction))
720                .handler(move |window, cx| {
721                    editor_focus_handle.dispatch_action(
722                        &editor::actions::ToggleEditPrediction,
723                        window,
724                        cx,
725                    );
726                });
727
728            match language_state.clone() {
729                Some((language, false)) => {
730                    menu = menu.item(entry.disabled(true).documentation_aside(
731                        DocumentationSide::Left,
732                        move |_cx| {
733                            Label::new(format!(
734                                "Edit predictions are disabled for {}",
735                                language.name()
736                            ))
737                            .into_any_element()
738                        },
739                    ));
740                }
741                Some(_) | None => menu = menu.item(entry),
742            }
743        }
744
745        if let Some((language, language_enabled)) = language_state {
746            let fs = fs.clone();
747            let language_name = language.name();
748
749            menu = menu.toggleable_entry(
750                language_name.clone(),
751                language_enabled,
752                IconPosition::Start,
753                None,
754                move |_, cx| {
755                    telemetry::event!(
756                        "Edit Prediction Setting Changed",
757                        setting = "language",
758                        language = language_name.to_string(),
759                        enabled = !language_enabled,
760                    );
761                    toggle_show_edit_predictions_for_language(language.clone(), fs.clone(), cx)
762                },
763            );
764        }
765
766        let settings = AllLanguageSettings::get_global(cx);
767
768        let globally_enabled = settings.show_edit_predictions(None, cx);
769        let entry = ContextMenuEntry::new("All Files")
770            .toggleable(IconPosition::Start, globally_enabled)
771            .action(workspace::ToggleEditPrediction.boxed_clone())
772            .handler(|window, cx| {
773                window.dispatch_action(workspace::ToggleEditPrediction.boxed_clone(), cx)
774            });
775        menu = menu.item(entry);
776
777        let provider = settings.edit_predictions.provider;
778        let current_mode = settings.edit_predictions_mode();
779        let subtle_mode = matches!(current_mode, EditPredictionsMode::Subtle);
780        let eager_mode = matches!(current_mode, EditPredictionsMode::Eager);
781
782        menu = menu
783                .separator()
784                .header("Display Modes")
785                .item(
786                    ContextMenuEntry::new("Eager")
787                        .toggleable(IconPosition::Start, eager_mode)
788                        .documentation_aside(DocumentationSide::Left, move |_| {
789                            Label::new("Display predictions inline when there are no language server completions available.").into_any_element()
790                        })
791                        .handler({
792                            let fs = fs.clone();
793                            move |_, cx| {
794                                telemetry::event!(
795                                    "Edit Prediction Setting Changed",
796                                    setting = "mode",
797                                    value = "eager",
798                                );
799                                toggle_edit_prediction_mode(fs.clone(), EditPredictionsMode::Eager, cx)
800                            }
801                        }),
802                )
803                .item(
804                    ContextMenuEntry::new("Subtle")
805                        .toggleable(IconPosition::Start, subtle_mode)
806                        .documentation_aside(DocumentationSide::Left, move |_| {
807                            Label::new("Display predictions inline only when holding a modifier key (alt by default).").into_any_element()
808                        })
809                        .handler({
810                            let fs = fs.clone();
811                            move |_, cx| {
812                                telemetry::event!(
813                                    "Edit Prediction Setting Changed",
814                                    setting = "mode",
815                                    value = "subtle",
816                                );
817                                toggle_edit_prediction_mode(fs.clone(), EditPredictionsMode::Subtle, cx)
818                            }
819                        }),
820                );
821
822        menu = menu.separator().header("Privacy");
823
824        if matches!(provider, EditPredictionProvider::Zed) {
825            if let Some(provider) = &self.edit_prediction_provider {
826                let data_collection = provider.data_collection_state(cx);
827
828                if data_collection.is_supported() {
829                    let provider = provider.clone();
830                    let enabled = data_collection.is_enabled();
831                    let is_open_source = data_collection.is_project_open_source();
832                    let is_collecting = data_collection.is_enabled();
833                    let (icon_name, icon_color) = if is_open_source && is_collecting {
834                        (IconName::Check, Color::Success)
835                    } else {
836                        (IconName::Check, Color::Accent)
837                    };
838
839                    menu = menu.item(
840                        ContextMenuEntry::new("Training Data Collection")
841                            .toggleable(IconPosition::Start, data_collection.is_enabled())
842                            .icon(icon_name)
843                            .icon_color(icon_color)
844                            .disabled(!provider.can_toggle_data_collection(cx))
845                            .documentation_aside(DocumentationSide::Left, move |cx| {
846                                let (msg, label_color, icon_name, icon_color) = match (is_open_source, is_collecting) {
847                                    (true, true) => (
848                                        "Project identified as open source, and you're sharing data.",
849                                        Color::Default,
850                                        IconName::Check,
851                                        Color::Success,
852                                    ),
853                                    (true, false) => (
854                                        "Project identified as open source, but you're not sharing data.",
855                                        Color::Muted,
856                                        IconName::Close,
857                                        Color::Muted,
858                                    ),
859                                    (false, true) => (
860                                        "Project not identified as open source. No data captured.",
861                                        Color::Muted,
862                                        IconName::Close,
863                                        Color::Muted,
864                                    ),
865                                    (false, false) => (
866                                        "Project not identified as open source, and setting turned off.",
867                                        Color::Muted,
868                                        IconName::Close,
869                                        Color::Muted,
870                                    ),
871                                };
872                                v_flex()
873                                    .gap_2()
874                                    .child(
875                                        Label::new(indoc!{
876                                            "Help us improve our open dataset model by sharing data from open source repositories. \
877                                            Omega must detect a license file in your repo for this setting to take effect. \
878                                            Files with sensitive data and secrets are excluded by default."
879                                        })
880                                    )
881                                    .child(
882                                        h_flex()
883                                            .items_start()
884                                            .pt_2()
885                                            .pr_1()
886                                            .flex_1()
887                                            .gap_1p5()
888                                            .border_t_1()
889                                            .border_color(cx.theme().colors().border_variant)
890                                            .child(h_flex().flex_shrink_0().h(line_height).child(Icon::new(icon_name).size(IconSize::XSmall).color(icon_color)))
891                                            .child(div().child(msg).w_full().text_sm().text_color(label_color.color(cx)))
892                                    )
893                                    .into_any_element()
894                            })
895                            .handler(move |_, cx| {
896                                provider.toggle_data_collection(cx);
897
898                                if !enabled {
899                                    telemetry::event!(
900                                        "Data Collection Enabled",
901                                        source = "Edit Prediction Status Menu"
902                                    );
903                                } else {
904                                    telemetry::event!(
905                                        "Data Collection Disabled",
906                                        source = "Edit Prediction Status Menu"
907                                    );
908                                }
909                            })
910                    );
911
912                    if is_collecting && !is_open_source {
913                        menu = menu.item(
914                            ContextMenuEntry::new("No data captured.")
915                                .disabled(true)
916                                .icon(IconName::Close)
917                                .icon_color(Color::Error)
918                                .icon_size(IconSize::Small),
919                        );
920                    }
921                }
922            }
923        }
924
925        menu = menu.item(
926            ContextMenuEntry::new("Configure Excluded Files")
927                .icon(IconName::Lock)
928                .icon_color(Color::Muted)
929                .documentation_aside(DocumentationSide::Left, |_| {
930                    Label::new(indoc!{"
931                        Open your settings to add sensitive paths for which Omega will never predict edits."}).into_any_element()
932                })
933                .handler(move |window, cx| {
934                    telemetry::event!(
935                        "Edit Prediction Menu Action",
936                        action = "configure_excluded_files",
937                    );
938                    if let Some(workspace) = Workspace::for_window(window, cx) {
939                        let workspace = workspace.downgrade();
940                        window
941                            .spawn(cx, async |cx| {
942                                open_disabled_globs_setting_in_editor(
943                                    workspace,
944                                    cx,
945                                ).await
946                            })
947                            .detach_and_log_err(cx);
948                    }
949                }),
950        ).item(
951            ContextMenuEntry::new("View Docs")
952                .icon(IconName::FileGeneric)
953                .icon_color(Color::Muted)
954                .handler(move |_, cx| {
955                    telemetry::event!(
956                        "Edit Prediction Menu Action",
957                        action = "view_docs",
958                    );
959                    cx.open_url(PRIVACY_DOCS);
960                })
961        );
962
963        if !self.editor_enabled.unwrap_or(true) {
964            let icons = self
965                .edit_prediction_provider
966                .as_ref()
967                .map(|p| p.icons(cx))
968                .unwrap_or_else(|| {
969                    edit_prediction_types::EditPredictionIconSet::new(IconName::OmegaPredict)
970                });
971            menu = menu.item(
972                ContextMenuEntry::new("This file is excluded.")
973                    .disabled(true)
974                    .icon(icons.disabled)
975                    .icon_size(IconSize::Small),
976            );
977        }
978
979        if let Some(editor_focus_handle) = self.editor_focus_handle.clone() {
980            menu = menu
981                .separator()
982                .header("Actions")
983                .entry(
984                    "Predict Edit at Cursor",
985                    Some(Box::new(ShowEditPrediction)),
986                    {
987                        let editor_focus_handle = editor_focus_handle.clone();
988                        move |window, cx| {
989                            telemetry::event!(
990                                "Edit Prediction Menu Action",
991                                action = "predict_at_cursor",
992                            );
993                            editor_focus_handle.dispatch_action(&ShowEditPrediction, window, cx);
994                        }
995                    },
996                )
997                .context(editor_focus_handle)
998                .when(
999                    cx.has_flag::<PredictEditsRatePredictionsFeatureFlag>(),
1000                    |this| this.action("Rate Predictions", RatePredictions.boxed_clone()),
1001                );
1002        }
1003
1004        menu
1005    }
1006
1007    fn build_copilot_context_menu(
1008        &self,
1009        window: &mut Window,
1010        cx: &mut Context<Self>,
1011    ) -> Entity<ContextMenu> {
1012        let all_language_settings = all_language_settings(None, cx);
1013        let next_edit_suggestions = all_language_settings
1014            .edit_predictions
1015            .copilot
1016            .enable_next_edit_suggestions
1017            .unwrap_or(true);
1018        let copilot_config = copilot_chat::CopilotChatConfiguration {
1019            enterprise_uri: all_language_settings
1020                .edit_predictions
1021                .copilot
1022                .enterprise_uri
1023                .clone(),
1024        };
1025        let settings_url = copilot_settings_url(copilot_config.enterprise_uri.as_deref());
1026
1027        ContextMenu::build(window, cx, |menu, window, cx| {
1028            let menu = self.build_language_settings_menu(menu, window, cx);
1029            let menu =
1030                self.add_provider_switching_section(menu, EditPredictionProvider::Copilot, cx);
1031
1032            let menu = self.add_configure_providers_item(menu);
1033            let menu = menu
1034                .separator()
1035                .item(
1036                    ContextMenuEntry::new("Copilot: Next Edit Suggestions")
1037                        .toggleable(IconPosition::Start, next_edit_suggestions)
1038                        .handler({
1039                            let fs = self.fs.clone();
1040                            move |_, cx| {
1041                                update_settings_file(fs.clone(), cx, move |settings, _| {
1042                                    settings
1043                                        .project
1044                                        .all_languages
1045                                        .edit_predictions
1046                                        .get_or_insert_default()
1047                                        .copilot
1048                                        .get_or_insert_default()
1049                                        .enable_next_edit_suggestions =
1050                                        Some(!next_edit_suggestions);
1051                                });
1052                            }
1053                        }),
1054                )
1055                .separator()
1056                .link(
1057                    "Go to Copilot Settings",
1058                    OpenBrowser { url: settings_url }.boxed_clone(),
1059                )
1060                .action("Sign Out", copilot::SignOut.boxed_clone());
1061            menu
1062        })
1063    }
1064
1065    fn build_codestral_context_menu(
1066        &self,
1067        window: &mut Window,
1068        cx: &mut Context<Self>,
1069    ) -> Entity<ContextMenu> {
1070        ContextMenu::build(window, cx, |menu, window, cx| {
1071            let menu = self.build_language_settings_menu(menu, window, cx);
1072            let menu =
1073                self.add_provider_switching_section(menu, EditPredictionProvider::Codestral, cx);
1074
1075            let menu = self.add_configure_providers_item(menu);
1076            menu
1077        })
1078    }
1079
1080    fn build_edit_prediction_context_menu(
1081        &self,
1082        provider: EditPredictionProvider,
1083        window: &mut Window,
1084        cx: &mut Context<Self>,
1085    ) -> Entity<ContextMenu> {
1086        ContextMenu::build(window, cx, |mut menu, window, cx| {
1087            let user = self.user_store.read(cx).current_user();
1088
1089            let needs_sign_in = user.is_none()
1090                && matches!(
1091                    provider,
1092                    EditPredictionProvider::None | EditPredictionProvider::Zed
1093                );
1094
1095            if needs_sign_in {
1096                menu = menu
1097                    .custom_row(move |_window, cx| {
1098                        let description = indoc! {
1099                            "You get 2,000 accepted suggestions at every keystroke for free, \
1100                            powered by Zeta, our open-source, open-data model"
1101                        };
1102
1103                        v_flex()
1104                            .max_w_64()
1105                            .h(rems_from_px(148.))
1106                            .child(render_zeta_tab_animation(cx))
1107                            .child(Label::new("Edit Prediction"))
1108                            .child(
1109                                Label::new(description)
1110                                    .color(Color::Muted)
1111                                    .size(LabelSize::Small),
1112                            )
1113                            .into_any_element()
1114                    })
1115                    .separator()
1116                    .entry("Sign In & Start Using", None, |window, cx| {
1117                        telemetry::event!(
1118                            "Edit Prediction Menu Action",
1119                            action = "sign_in",
1120                            provider = "zed",
1121                        );
1122                        let client = Client::global(cx);
1123                        window
1124                            .spawn(cx, async move |cx| {
1125                                client
1126                                    .sign_in_with_optional_connect(true, &cx)
1127                                    .await
1128                                    .log_err();
1129                            })
1130                            .detach();
1131                    })
1132                    .link_with_handler(
1133                        "Learn More",
1134                        OpenBrowser {
1135                            url: zed_urls::edit_prediction_docs(cx).into(),
1136                        }
1137                        .boxed_clone(),
1138                        |_window, _cx| {
1139                            telemetry::event!(
1140                                "Edit Prediction Menu Action",
1141                                action = "view_docs",
1142                                source = "upsell",
1143                            );
1144                        },
1145                    )
1146                    .separator();
1147            } else {
1148                let mercury_payment_required = matches!(provider, EditPredictionProvider::Mercury)
1149                    && edit_prediction::EditPredictionStore::try_global(cx).is_some_and(
1150                        |ep_store| ep_store.read(cx).mercury_has_payment_required_error(),
1151                    );
1152
1153                if mercury_payment_required {
1154                    menu = menu
1155                        .header("Mercury")
1156                        .item(ContextMenuEntry::new("Free tier limit reached").disabled(true))
1157                        .item(
1158                            ContextMenuEntry::new(
1159                                "Upgrade to a paid plan to continue using the service",
1160                            )
1161                            .disabled(true),
1162                        )
1163                        .separator();
1164                }
1165
1166                if let Some(usage) = self
1167                    .edit_prediction_provider
1168                    .as_ref()
1169                    .and_then(|provider| provider.usage(cx))
1170                {
1171                    menu = menu.header("Usage");
1172                    menu = menu
1173                        .custom_entry(
1174                            move |_window, cx| {
1175                                let used_percentage = match usage.limit {
1176                                    UsageLimit::Limited(limit) => {
1177                                        Some((usage.amount as f32 / limit as f32) * 100.)
1178                                    }
1179                                    UsageLimit::Unlimited => None,
1180                                };
1181
1182                                h_flex()
1183                                    .flex_1()
1184                                    .gap_1p5()
1185                                    .children(used_percentage.map(|percent| {
1186                                        ProgressBar::new("usage", percent, 100., cx)
1187                                    }))
1188                                    .child(
1189                                        Label::new(match usage.limit {
1190                                            UsageLimit::Limited(limit) => {
1191                                                format!("{} / {limit}", usage.amount)
1192                                            }
1193                                            UsageLimit::Unlimited => {
1194                                                format!("{} / ∞", usage.amount)
1195                                            }
1196                                        })
1197                                        .size(LabelSize::Small)
1198                                        .color(Color::Muted),
1199                                    )
1200                                    .into_any_element()
1201                            },
1202                            move |_, cx| cx.open_url(&zed_urls::account_url(cx)),
1203                        )
1204                        .when(usage.over_limit(), |menu| -> ContextMenu {
1205                            menu.entry("Subscribe to increase your limit", None, |_window, cx| {
1206                                telemetry::event!(
1207                                    "Edit Prediction Menu Action",
1208                                    action = "upsell_clicked",
1209                                    reason = "usage_limit",
1210                                );
1211                                cx.open_url(&zed_urls::account_url(cx))
1212                            })
1213                        })
1214                        .separator();
1215                } else if self.user_store.read(cx).account_too_young() {
1216                    menu = menu
1217                        .custom_entry(
1218                            |_window, _cx| {
1219                                Label::new("Your GitHub account is less than 30 days old.")
1220                                    .size(LabelSize::Small)
1221                                    .color(Color::Warning)
1222                                    .into_any_element()
1223                            },
1224                            |_window, cx| cx.open_url(&zed_urls::account_url(cx)),
1225                        )
1226                        .entry(
1227                            "Hosted edit predictions are unavailable in this Omega build.",
1228                            None,
1229                            |_window, _cx| {},
1230                        )
1231                        .separator();
1232                } else if self.user_store.read(cx).has_overdue_invoices() {
1233                    menu = menu
1234                        .custom_entry(
1235                            |_window, _cx| {
1236                                Label::new("Hosted billing is unavailable in Omega")
1237                                    .size(LabelSize::Small)
1238                                    .color(Color::Warning)
1239                                    .into_any_element()
1240                            },
1241                            |_window, _cx| {},
1242                        )
1243                        .entry(
1244                            "This Omega build does not use Zed billing or hosted prediction plans.",
1245                            None,
1246                            |_window, _cx| {},
1247                        )
1248                        .separator();
1249                }
1250            }
1251
1252            if !needs_sign_in {
1253                menu = self.build_language_settings_menu(menu, window, cx);
1254            }
1255            menu = self.add_provider_switching_section(menu, provider, cx);
1256
1257            if cx.is_staff() {
1258                if let Some(store) = EditPredictionStore::try_global(cx) {
1259                    store.update(cx, |store, cx| {
1260                        store.refresh_available_experiments(cx);
1261                    });
1262                    let store = store.read(cx);
1263                    let experiments = store.available_experiments().to_vec();
1264                    let preferred = store.preferred_experiment().map(|s| s.to_owned());
1265                    let active = store.active_experiment().map(|s| s.to_owned());
1266
1267                    let preferred_for_submenu = preferred.clone();
1268                    menu = menu
1269                        .separator()
1270                        .submenu("Experiment", move |menu, _window, _cx| {
1271                            let mut menu = menu.toggleable_entry(
1272                                "Default",
1273                                preferred_for_submenu.is_none(),
1274                                IconPosition::Start,
1275                                None,
1276                                {
1277                                    move |_window, cx| {
1278                                        if let Some(store) = EditPredictionStore::try_global(cx) {
1279                                            store.update(cx, |store, _cx| {
1280                                                store.set_preferred_experiment(None);
1281                                            });
1282                                        }
1283                                    }
1284                                },
1285                            );
1286                            for experiment in &experiments {
1287                                let is_selected = active.as_deref() == Some(experiment.as_str())
1288                                    || preferred.as_deref() == Some(experiment.as_str());
1289                                let experiment_name = experiment.clone();
1290                                menu = menu.toggleable_entry(
1291                                    experiment.clone(),
1292                                    is_selected,
1293                                    IconPosition::Start,
1294                                    None,
1295                                    move |_window, cx| {
1296                                        if let Some(store) = EditPredictionStore::try_global(cx) {
1297                                            store.update(cx, |store, _cx| {
1298                                                store.set_preferred_experiment(Some(
1299                                                    experiment_name.clone(),
1300                                                ));
1301                                            });
1302                                        }
1303                                    },
1304                                );
1305                            }
1306                            menu
1307                        });
1308                }
1309            }
1310
1311            let menu = self.add_configure_providers_item(menu);
1312            menu
1313        })
1314    }
1315
1316    pub fn update_enabled(&mut self, editor: Entity<Editor>, cx: &mut Context<Self>) {
1317        let editor = editor.read(cx);
1318        let snapshot = editor.buffer().read(cx).snapshot(cx);
1319        let suggestion_anchor = editor.selections.newest_anchor().start;
1320        let language = snapshot.language_at(suggestion_anchor);
1321        let file = snapshot.file_at(suggestion_anchor).cloned();
1322        self.editor_enabled = {
1323            let file = file.as_ref();
1324            Some(
1325                file.map(|file| {
1326                    all_language_settings(Some(file), cx)
1327                        .edit_predictions_enabled_for_file(file, cx)
1328                })
1329                .unwrap_or(true),
1330            )
1331        };
1332        self.editor_show_predictions = editor.edit_predictions_enabled();
1333        self.edit_prediction_provider = editor.edit_prediction_provider();
1334        self.language = language.cloned();
1335        self.file = file;
1336        self.editor_focus_handle = Some(editor.focus_handle(cx));
1337
1338        cx.notify();
1339    }
1340}
1341
1342impl StatusItemView for EditPredictionButton {
1343    fn set_active_pane_item(
1344        &mut self,
1345        item: Option<&dyn ItemHandle>,
1346        _: &mut Window,
1347        cx: &mut Context<Self>,
1348    ) {
1349        if let Some(editor) = item.and_then(|item| item.act_as::<Editor>(cx)) {
1350            self.editor_subscription = Some((
1351                cx.observe(&editor, Self::update_enabled),
1352                editor.entity_id().as_u64() as usize,
1353            ));
1354            self.update_enabled(editor, cx);
1355        } else {
1356            self.language = None;
1357            self.editor_subscription = None;
1358            self.editor_enabled = None;
1359        }
1360        cx.notify();
1361    }
1362
1363    fn hide_setting(&self, _: &App) -> Option<HideStatusItem> {
1364        // This button is already gated on having a non-disabled edit
1365        // prediction provider, which the user manages through provider/AI
1366        // settings.
1367        None
1368    }
1369}
1370
1371async fn open_disabled_globs_setting_in_editor(
1372    workspace: WeakEntity<Workspace>,
1373    cx: &mut AsyncWindowContext,
1374) -> Result<()> {
1375    let settings_editor = workspace
1376        .update_in(cx, |_, window, cx| {
1377            create_and_open_local_file(paths::settings_file(), window, cx, || {
1378                settings::initial_user_settings_content().as_ref().into()
1379            })
1380        })?
1381        .await?
1382        .downcast::<Editor>()
1383        .unwrap();
1384
1385    settings_editor
1386        .downgrade()
1387        .update_in(cx, |item, window, cx| {
1388            let text = item.buffer().read(cx).snapshot(cx).text();
1389
1390            let settings = cx.global::<SettingsStore>();
1391
1392            // Ensure that we always have "edit_predictions { "disabled_globs": [] }"
1393            let Some(edits) = settings
1394                .edits_for_update(&text, |file| {
1395                    file.project
1396                        .all_languages
1397                        .edit_predictions
1398                        .get_or_insert_with(Default::default)
1399                        .disabled_globs
1400                        .get_or_insert_with(Vec::new);
1401                })
1402                .log_err()
1403            else {
1404                return;
1405            };
1406
1407            if !edits.is_empty() {
1408                item.edit(
1409                    edits
1410                        .into_iter()
1411                        .map(|(r, s)| (MultiBufferOffset(r.start)..MultiBufferOffset(r.end), s)),
1412                    cx,
1413                );
1414            }
1415
1416            let text = item.buffer().read(cx).snapshot(cx).text();
1417
1418            static DISABLED_GLOBS_REGEX: LazyLock<Regex> = LazyLock::new(|| {
1419                Regex::new(r#""disabled_globs":\s*\[\s*(?P<content>(?:.|\n)*?)\s*\]"#).unwrap()
1420            });
1421            // Only capture [...]
1422            let range = DISABLED_GLOBS_REGEX.captures(&text).and_then(|captures| {
1423                captures
1424                    .name("content")
1425                    .map(|inner_match| inner_match.start()..inner_match.end())
1426            });
1427            if let Some(range) = range {
1428                let range = MultiBufferOffset(range.start)..MultiBufferOffset(range.end);
1429                item.change_selections(
1430                    SelectionEffects::scroll(Autoscroll::newest()),
1431                    window,
1432                    cx,
1433                    |selections| {
1434                        selections.select_ranges(vec![range]);
1435                    },
1436                );
1437            }
1438        })?;
1439
1440    anyhow::Ok(())
1441}
1442
1443pub fn set_completion_provider(fs: Arc<dyn Fs>, cx: &mut App, provider: EditPredictionProvider) {
1444    update_settings_file(fs, cx, move |settings, _| {
1445        settings
1446            .project
1447            .all_languages
1448            .edit_predictions
1449            .get_or_insert_default()
1450            .provider = Some(provider);
1451    });
1452}
1453
1454pub fn get_available_providers(cx: &mut App) -> Vec<EditPredictionProvider> {
1455    let mut providers = Vec::new();
1456
1457    providers.push(EditPredictionProvider::Zed);
1458
1459    let app_state = workspace::AppState::global(cx);
1460    if copilot::GlobalCopilotAuth::try_get_or_init(app_state, cx)
1461        .is_some_and(|copilot| copilot.0.read(cx).is_authenticated())
1462    {
1463        providers.push(EditPredictionProvider::Copilot);
1464    };
1465
1466    if codestral::codestral_api_key(cx).is_some() {
1467        providers.push(EditPredictionProvider::Codestral);
1468    }
1469
1470    if edit_prediction::ollama::is_available(cx) {
1471        providers.push(EditPredictionProvider::Ollama);
1472    }
1473
1474    if all_language_settings(None, cx)
1475        .edit_predictions
1476        .open_ai_compatible_api
1477        .is_some()
1478    {
1479        providers.push(EditPredictionProvider::OpenAiCompatibleApi);
1480    }
1481
1482    if edit_prediction::mercury::mercury_api_token(cx)
1483        .read(cx)
1484        .has_key()
1485    {
1486        providers.push(EditPredictionProvider::Mercury);
1487    }
1488
1489    providers
1490}
1491
1492fn toggle_show_edit_predictions_for_language(
1493    language: Arc<Language>,
1494    fs: Arc<dyn Fs>,
1495    cx: &mut App,
1496) {
1497    let show_edit_predictions =
1498        all_language_settings(None, cx).show_edit_predictions(Some(&language), cx);
1499    update_settings_file(fs, cx, move |settings, _| {
1500        settings
1501            .project
1502            .all_languages
1503            .languages
1504            .0
1505            .entry(language.name().0.to_string())
1506            .or_default()
1507            .show_edit_predictions = Some(!show_edit_predictions);
1508    });
1509}
1510
1511fn hide_copilot(fs: Arc<dyn Fs>, cx: &mut App) {
1512    update_settings_file(fs, cx, move |settings, _| {
1513        settings
1514            .project
1515            .all_languages
1516            .edit_predictions
1517            .get_or_insert(Default::default())
1518            .provider = Some(EditPredictionProvider::None);
1519    });
1520}
1521
1522fn toggle_edit_prediction_mode(fs: Arc<dyn Fs>, mode: EditPredictionsMode, cx: &mut App) {
1523    let settings = AllLanguageSettings::get_global(cx);
1524    let current_mode = settings.edit_predictions_mode();
1525
1526    if current_mode != mode {
1527        update_settings_file(fs, cx, move |settings, _cx| {
1528            if let Some(edit_predictions) = settings.project.all_languages.edit_predictions.as_mut()
1529            {
1530                edit_predictions.mode = Some(mode);
1531            } else {
1532                settings.project.all_languages.edit_predictions =
1533                    Some(settings::EditPredictionSettingsContent {
1534                        mode: Some(mode),
1535                        ..Default::default()
1536                    });
1537            }
1538        });
1539    }
1540}
1541
1542fn render_zeta_tab_animation(cx: &App) -> impl IntoElement {
1543    let tab = |n: u64, inverted: bool| {
1544        let text_color = cx.theme().colors().text;
1545
1546        h_flex().child(
1547            h_flex()
1548                .text_size(TextSize::XSmall.rems(cx))
1549                .text_color(text_color)
1550                .child("tab")
1551                .with_animation(
1552                    ElementId::Integer(n),
1553                    Animation::new(Duration::from_secs(3)).repeat(),
1554                    move |tab, delta| {
1555                        let n_f32 = n as f32;
1556
1557                        let offset = if inverted {
1558                            0.2 * (4.0 - n_f32)
1559                        } else {
1560                            0.2 * n_f32
1561                        };
1562
1563                        let phase = (delta - offset + 1.0) % 1.0;
1564                        let pulse = if phase < 0.6 {
1565                            let t = phase / 0.6;
1566                            1.0 - (0.5 - t).abs() * 2.0
1567                        } else {
1568                            0.0
1569                        };
1570
1571                        let eased = ease_in_out(pulse);
1572                        let opacity = 0.1 + 0.5 * eased;
1573
1574                        tab.text_color(text_color.opacity(opacity))
1575                    },
1576                ),
1577        )
1578    };
1579
1580    let tab_sequence = |inverted: bool| {
1581        h_flex()
1582            .gap_1()
1583            .child(tab(0, inverted))
1584            .child(tab(1, inverted))
1585            .child(tab(2, inverted))
1586            .child(tab(3, inverted))
1587            .child(tab(4, inverted))
1588    };
1589
1590    h_flex()
1591        .my_1p5()
1592        .p_4()
1593        .justify_center()
1594        .gap_2()
1595        .rounded_xs()
1596        .border_1()
1597        .border_dashed()
1598        .border_color(cx.theme().colors().border)
1599        .bg(gpui::pattern_slash(
1600            cx.theme().colors().border.opacity(0.5),
1601            1.,
1602            8.,
1603        ))
1604        .child(tab_sequence(true))
1605        .child(Icon::new(IconName::OmegaPredict))
1606        .child(tab_sequence(false))
1607}
1608
1609fn emit_edit_prediction_menu_opened(
1610    provider: &str,
1611    file: &Option<Arc<dyn File>>,
1612    language: &Option<Arc<Language>>,
1613    project: &WeakEntity<Project>,
1614    cx: &App,
1615) {
1616    let language_name = language.as_ref().map(|l| l.name());
1617    let edit_predictions_enabled_for_language =
1618        LanguageSettings::resolve(None, language_name.as_ref(), cx).show_edit_predictions;
1619    let file_extension = file
1620        .as_ref()
1621        .and_then(|f| {
1622            std::path::Path::new(f.file_name(cx))
1623                .extension()
1624                .and_then(|e| e.to_str())
1625        })
1626        .map(|s| s.to_string());
1627    let is_via_ssh = project
1628        .upgrade()
1629        .map(|p| p.read(cx).is_via_remote_server())
1630        .unwrap_or(false);
1631    telemetry::event!(
1632        "Toolbar Menu Opened",
1633        name = "Edit Predictions",
1634        provider,
1635        file_extension,
1636        edit_predictions_enabled_for_language,
1637        is_via_ssh,
1638    );
1639}
1640
1641fn copilot_settings_url(enterprise_uri: Option<&str>) -> Arc<str> {
1642    match enterprise_uri {
1643        Some(uri) => format!("{}{}", uri.trim_end_matches('/'), COPILOT_SETTINGS_PATH).into(),
1644        None => COPILOT_SETTINGS_URL.into(),
1645    }
1646}
1647
1648#[cfg(test)]
1649mod tests {
1650    use super::*;
1651    use gpui::TestAppContext;
1652
1653    #[gpui::test]
1654    async fn test_copilot_settings_url_with_enterprise_uri(cx: &mut TestAppContext) {
1655        cx.update(|cx| {
1656            let settings_store = SettingsStore::test(cx);
1657            cx.set_global(settings_store);
1658        });
1659
1660        cx.update_global(|settings_store: &mut SettingsStore, cx| {
1661            settings_store
1662                .set_user_settings(
1663                    r#"{"edit_predictions":{"copilot":{"enterprise_uri":"https://my-company.ghe.com"}}}"#,
1664                    cx,
1665                )
1666                .unwrap();
1667        });
1668
1669        let url = cx.update(|cx| {
1670            let all_language_settings = all_language_settings(None, cx);
1671            copilot_settings_url(
1672                all_language_settings
1673                    .edit_predictions
1674                    .copilot
1675                    .enterprise_uri
1676                    .as_deref(),
1677            )
1678        });
1679
1680        assert_eq!(url.as_ref(), "https://my-company.ghe.com/settings/copilot");
1681    }
1682
1683    #[gpui::test]
1684    async fn test_copilot_settings_url_with_enterprise_uri_trailing_slash(cx: &mut TestAppContext) {
1685        cx.update(|cx| {
1686            let settings_store = SettingsStore::test(cx);
1687            cx.set_global(settings_store);
1688        });
1689
1690        cx.update_global(|settings_store: &mut SettingsStore, cx| {
1691            settings_store
1692                .set_user_settings(
1693                    r#"{"edit_predictions":{"copilot":{"enterprise_uri":"https://my-company.ghe.com/"}}}"#,
1694                    cx,
1695                )
1696                .unwrap();
1697        });
1698
1699        let url = cx.update(|cx| {
1700            let all_language_settings = all_language_settings(None, cx);
1701            copilot_settings_url(
1702                all_language_settings
1703                    .edit_predictions
1704                    .copilot
1705                    .enterprise_uri
1706                    .as_deref(),
1707            )
1708        });
1709
1710        assert_eq!(url.as_ref(), "https://my-company.ghe.com/settings/copilot");
1711    }
1712
1713    #[gpui::test]
1714    async fn test_copilot_settings_url_without_enterprise_uri(cx: &mut TestAppContext) {
1715        cx.update(|cx| {
1716            let settings_store = SettingsStore::test(cx);
1717            cx.set_global(settings_store);
1718        });
1719
1720        let url = cx.update(|cx| {
1721            let all_language_settings = all_language_settings(None, cx);
1722            copilot_settings_url(
1723                all_language_settings
1724                    .edit_predictions
1725                    .copilot
1726                    .enterprise_uri
1727                    .as_deref(),
1728            )
1729        });
1730
1731        assert_eq!(url.as_ref(), "https://github.com/settings/copilot");
1732    }
1733}
1734
Served at tenant.openagents/omega Member data and write actions are omitted.