edit prediction: Add ability to switch providers from the status bar menu (#41504)

Closes https://github.com/zed-industries/zed/issues/41500

<img width="500" height="1122" alt="Screenshot 2025-10-29 at 9  43@2x"
src="https://github.com/user-attachments/assets/ac2a81ad-99bb-43cd-b032-f2485fc23166"
/>

Release Notes:

- Added the ability to switch between configured edit prediction
providers through the status bar menu.
This commit is contained in:
Danilo Leal
2025-10-29 22:16:13 -03:00
committed by GitHub
parent 802b0e4968
commit 992448b560
5 changed files with 144 additions and 38 deletions
+8
View File
@@ -66,6 +66,14 @@ impl CodestralCompletionProvider {
Self::api_key(cx).is_some()
}
/// This is so we can immediately show Codestral as a provider users can
/// switch to in the edit prediction menu, if the API has been added
pub fn ensure_api_key_loaded(http_client: Arc<dyn HttpClient>, cx: &mut App) {
MistralLanguageModelProvider::global(http_client, cx)
.load_codestral_api_key(cx)
.detach();
}
fn api_key(cx: &App) -> Option<Arc<str>> {
MistralLanguageModelProvider::try_global(cx)
.and_then(|provider| provider.codestral_api_key(CODESTRAL_API_URL, cx))
@@ -1,5 +1,5 @@
use anyhow::Result;
use client::{UserStore, zed_urls};
use client::{Client, UserStore, zed_urls};
use cloud_llm_client::UsageLimit;
use codestral::CodestralCompletionProvider;
use copilot::{Copilot, Status};
@@ -192,6 +192,7 @@ impl Render for EditPredictionButton {
Some(ContextMenu::build(window, cx, |menu, _, _| {
let fs = fs.clone();
let activate_url = activate_url.clone();
menu.entry("Sign In", None, move |_, cx| {
cx.open_url(activate_url.as_str())
})
@@ -244,15 +245,8 @@ impl Render for EditPredictionButton {
} else {
Some(ContextMenu::build(window, cx, |menu, _, _| {
let fs = fs.clone();
menu.entry("Use Zed AI instead", None, move |_, cx| {
set_completion_provider(
fs.clone(),
cx,
EditPredictionProvider::Zed,
)
})
.separator()
.entry(
menu.entry(
"Configure Codestral API Key",
None,
move |window, cx| {
@@ -262,6 +256,18 @@ impl Render for EditPredictionButton {
);
},
)
.separator()
.entry(
"Use Zed AI instead",
None,
move |_, cx| {
set_completion_provider(
fs.clone(),
cx,
EditPredictionProvider::Zed,
)
},
)
}))
}
})
@@ -412,6 +418,7 @@ impl EditPredictionButton {
fs: Arc<dyn Fs>,
user_store: Entity<UserStore>,
popover_menu_handle: PopoverMenuHandle<ContextMenu>,
client: Arc<Client>,
cx: &mut Context<Self>,
) -> Self {
if let Some(copilot) = Copilot::global(cx) {
@@ -421,6 +428,8 @@ impl EditPredictionButton {
cx.observe_global::<SettingsStore>(move |_, cx| cx.notify())
.detach();
CodestralCompletionProvider::ensure_api_key_loaded(client.http_client(), cx);
Self {
editor_subscription: None,
editor_enabled: None,
@@ -435,6 +444,89 @@ impl EditPredictionButton {
}
}
fn get_available_providers(&self, cx: &App) -> Vec<EditPredictionProvider> {
let mut providers = Vec::new();
providers.push(EditPredictionProvider::Zed);
if let Some(copilot) = Copilot::global(cx) {
if matches!(copilot.read(cx).status(), Status::Authorized) {
providers.push(EditPredictionProvider::Copilot);
}
}
if let Some(supermaven) = Supermaven::global(cx) {
if let Supermaven::Spawned(agent) = supermaven.read(cx) {
if matches!(agent.account_status, AccountStatus::Ready) {
providers.push(EditPredictionProvider::Supermaven);
}
}
}
if CodestralCompletionProvider::has_api_key(cx) {
providers.push(EditPredictionProvider::Codestral);
}
providers
}
fn add_provider_switching_section(
&self,
mut menu: ContextMenu,
current_provider: EditPredictionProvider,
cx: &App,
) -> ContextMenu {
let available_providers = self.get_available_providers(cx);
let other_providers: Vec<_> = available_providers
.into_iter()
.filter(|p| *p != current_provider && *p != EditPredictionProvider::None)
.collect();
if !other_providers.is_empty() {
menu = menu.separator().header("Switch Providers");
for provider in other_providers {
let fs = self.fs.clone();
menu = match provider {
EditPredictionProvider::Zed => menu.item(
ContextMenuEntry::new("Zed AI")
.documentation_aside(
DocumentationSide::Left,
DocumentationEdge::Top,
|_| {
Label::new("Zed's edit prediction is powered by Zeta, an open-source, dataset mode.")
.into_any_element()
},
)
.handler(move |_, cx| {
set_completion_provider(fs.clone(), cx, provider);
}),
),
EditPredictionProvider::Copilot => {
menu.entry("GitHub Copilot", None, move |_, cx| {
set_completion_provider(fs.clone(), cx, provider);
})
}
EditPredictionProvider::Supermaven => {
menu.entry("Supermaven", None, move |_, cx| {
set_completion_provider(fs.clone(), cx, provider);
})
}
EditPredictionProvider::Codestral => {
menu.entry("Codestral", None, move |_, cx| {
set_completion_provider(fs.clone(), cx, provider);
})
}
EditPredictionProvider::None => continue,
};
}
}
menu
}
pub fn build_copilot_start_menu(
&mut self,
window: &mut Window,
@@ -572,8 +664,10 @@ impl EditPredictionButton {
}
menu = menu.separator().header("Privacy");
if let Some(provider) = &self.edit_prediction_provider {
let data_collection = provider.data_collection_state(cx);
if data_collection.is_supported() {
let provider = provider.clone();
let enabled = data_collection.is_enabled();
@@ -691,7 +785,7 @@ impl EditPredictionButton {
}
}),
).item(
ContextMenuEntry::new("View Documentation")
ContextMenuEntry::new("View Docs")
.icon(IconName::FileGeneric)
.icon_color(Color::Muted)
.handler(move |_, cx| {
@@ -711,6 +805,7 @@ impl EditPredictionButton {
if let Some(editor_focus_handle) = self.editor_focus_handle.clone() {
menu = menu
.separator()
.header("Actions")
.entry(
"Predict Edit at Cursor",
Some(Box::new(ShowEditPrediction)),
@@ -721,7 +816,11 @@ impl EditPredictionButton {
}
},
)
.context(editor_focus_handle);
.context(editor_focus_handle)
.when(
cx.has_flag::<PredictEditsRateCompletionsFeatureFlag>(),
|this| this.action("Rate Completions", RateCompletions.boxed_clone()),
);
}
menu
@@ -733,15 +832,11 @@ impl EditPredictionButton {
cx: &mut Context<Self>,
) -> Entity<ContextMenu> {
ContextMenu::build(window, cx, |menu, window, cx| {
self.build_language_settings_menu(menu, window, cx)
.separator()
.entry("Use Zed AI instead", None, {
let fs = self.fs.clone();
move |_window, cx| {
set_completion_provider(fs.clone(), cx, EditPredictionProvider::Zed)
}
})
.separator()
let menu = self.build_language_settings_menu(menu, window, cx);
let menu =
self.add_provider_switching_section(menu, EditPredictionProvider::Copilot, cx);
menu.separator()
.link(
"Go to Copilot Settings",
OpenBrowser {
@@ -759,8 +854,11 @@ impl EditPredictionButton {
cx: &mut Context<Self>,
) -> Entity<ContextMenu> {
ContextMenu::build(window, cx, |menu, window, cx| {
self.build_language_settings_menu(menu, window, cx)
.separator()
let menu = self.build_language_settings_menu(menu, window, cx);
let menu =
self.add_provider_switching_section(menu, EditPredictionProvider::Supermaven, cx);
menu.separator()
.action("Sign Out", supermaven::SignOut.boxed_clone())
})
}
@@ -770,14 +868,12 @@ impl EditPredictionButton {
window: &mut Window,
cx: &mut Context<Self>,
) -> Entity<ContextMenu> {
let fs = self.fs.clone();
ContextMenu::build(window, cx, |menu, window, cx| {
self.build_language_settings_menu(menu, window, cx)
.separator()
.entry("Use Zed AI instead", None, move |_, cx| {
set_completion_provider(fs.clone(), cx, EditPredictionProvider::Zed)
})
.separator()
let menu = self.build_language_settings_menu(menu, window, cx);
let menu =
self.add_provider_switching_section(menu, EditPredictionProvider::Codestral, cx);
menu.separator()
.entry("Configure Codestral API Key", None, move |window, cx| {
window.dispatch_action(zed_actions::agent::OpenSettings.boxed_clone(), cx);
})
@@ -872,10 +968,10 @@ impl EditPredictionButton {
.separator();
}
self.build_language_settings_menu(menu, window, cx).when(
cx.has_flag::<PredictEditsRateCompletionsFeatureFlag>(),
|this| this.action("Rate Completions", RateCompletions.boxed_clone()),
)
let menu = self.build_language_settings_menu(menu, window, cx);
let menu = self.add_provider_switching_section(menu, EditPredictionProvider::Zed, cx);
menu
})
}
@@ -1377,11 +1377,12 @@ impl Render for ConfigurationView {
v_flex().gap_2().child(Label::new(LABEL)).child(
Button::new("sign_in", "Sign in to use GitHub Copilot")
.full_width()
.style(ButtonStyle::Outlined)
.icon_color(Color::Muted)
.icon(IconName::Github)
.icon_position(IconPosition::Start)
.icon_size(IconSize::Medium)
.full_width()
.icon_size(IconSize::Small)
.on_click(|_, window, cx| copilot::initiate_sign_in(window, cx)),
)
}
@@ -753,9 +753,9 @@ struct ConfigurationView {
impl ConfigurationView {
fn new(state: Entity<State>, window: &mut Window, cx: &mut Context<Self>) -> Self {
let api_key_editor =
cx.new(|cx| InputField::new(window, cx, "0aBCDEFGhIjKLmNOpqrSTUVwxyzabCDE1f2"));
cx.new(|cx| InputField::new(window, cx, "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"));
let codestral_api_key_editor =
cx.new(|cx| InputField::new(window, cx, "0aBCDEFGhIjKLmNOpqrSTUVwxyzabCDE1f2"));
cx.new(|cx| InputField::new(window, cx, "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"));
cx.observe(&state, |_, _, cx| {
cx.notify();
+1
View File
@@ -388,6 +388,7 @@ pub fn initialize_workspace(
app_state.fs.clone(),
app_state.user_store.clone(),
edit_prediction_menu_handle.clone(),
app_state.client.clone(),
cx,
)
});