From 2fb3d593bc8f9608f0041c8c6183214be7425d5d Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Sun, 9 Nov 2025 14:32:05 -0300 Subject: [PATCH] agent_ui: Add component to standardize the configured LLM card (#42314) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR adds a new component to the `language_models` crate called `ConfiguredApiCard`: Screenshot 2025-11-09 at 2  07@2x We were previously recreating this component from scratch with regular divs in all LLM providers render function, which was redundant as they all essentially looked the same and didn't have any major variations aside from labels. We can clean up a bunch of similar code with this change, which is cool! Release Notes: - N/A --- .../language_models/src/provider/anthropic.rs | 77 ++++------ .../language_models/src/provider/bedrock.rs | 77 +++++----- .../src/provider/copilot_chat.rs | 67 +++++---- .../language_models/src/provider/deepseek.rs | 66 +++------ crates/language_models/src/provider/google.rs | 80 ++++------ .../language_models/src/provider/mistral.rs | 137 +++++------------- crates/language_models/src/provider/ollama.rs | 52 ++----- .../language_models/src/provider/open_ai.rs | 72 +++------ .../src/provider/open_router.rs | 74 +++------- crates/language_models/src/provider/vercel.rs | 74 +++------- crates/language_models/src/provider/x_ai.rs | 74 +++------- crates/language_models/src/ui.rs | 2 + .../src/ui/configured_api_card.rs | 86 +++++++++++ 13 files changed, 370 insertions(+), 568 deletions(-) create mode 100644 crates/language_models/src/ui/configured_api_card.rs diff --git a/crates/language_models/src/provider/anthropic.rs b/crates/language_models/src/provider/anthropic.rs index 86cc81cb7bc7b89967e69949ced891a7cb845cea..287c76fc6dfea530ce53b48178024ef185b98134 100644 --- a/crates/language_models/src/provider/anthropic.rs +++ b/crates/language_models/src/provider/anthropic.rs @@ -20,13 +20,13 @@ use std::pin::Pin; use std::str::FromStr; use std::sync::{Arc, LazyLock}; use strum::IntoEnumIterator; -use ui::{Icon, IconName, List, Tooltip, prelude::*}; +use ui::{List, prelude::*}; use ui_input::InputField; use util::ResultExt; use zed_env_vars::{EnvVar, env_var}; use crate::api_key::ApiKeyState; -use crate::ui::InstructionListItem; +use crate::ui::{ConfiguredApiCard, InstructionListItem}; pub use settings::AnthropicAvailableModel as AvailableModel; @@ -909,9 +909,21 @@ impl ConfigurationView { impl Render for ConfigurationView { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { let env_var_set = self.state.read(cx).api_key_state.is_from_env_var(); + let configured_card_label = if env_var_set { + format!("API key set in {API_KEY_ENV_VAR_NAME} environment variable") + } else { + let api_url = AnthropicLanguageModelProvider::api_url(cx); + if api_url == ANTHROPIC_API_URL { + "API key configured".to_string() + } else { + format!("API key configured for {}", api_url) + } + }; if self.load_credentials_task.is_some() { - div().child(Label::new("Loading credentials...")).into_any() + div() + .child(Label::new("Loading credentials...")) + .into_any_element() } else if self.should_render_editor(cx) { v_flex() .size_full() @@ -941,56 +953,17 @@ impl Render for ConfigurationView { .size(LabelSize::Small) .color(Color::Muted), ) - .into_any() + .into_any_element() } else { - h_flex() - .mt_1() - .p_1() - .justify_between() - .rounded_md() - .border_1() - .border_color(cx.theme().colors().border) - .bg(cx.theme().colors().background) - .child( - h_flex() - .flex_1() - .min_w_0() - .gap_1() - .child(Icon::new(IconName::Check).color(Color::Success)) - .child( - div() - .w_full() - .overflow_x_hidden() - .text_ellipsis() - .child(Label::new(if env_var_set { - format!("API key set in {API_KEY_ENV_VAR_NAME} environment variable") - } else { - let api_url = AnthropicLanguageModelProvider::api_url(cx); - if api_url == ANTHROPIC_API_URL { - "API key configured".to_string() - } else { - format!("API key configured for {}", api_url) - } - })) - ), - ) - .child( - h_flex() - .flex_shrink_0() - .child( - Button::new("reset-key", "Reset Key") - .label_size(LabelSize::Small) - .icon(Some(IconName::Trash)) - .icon_size(IconSize::Small) - .icon_position(IconPosition::Start) - .disabled(env_var_set) - .when(env_var_set, |this| { - this.tooltip(Tooltip::text(format!("To reset your API key, unset the {API_KEY_ENV_VAR_NAME} environment variable."))) - }) - .on_click(cx.listener(|this, _, window, cx| this.reset_api_key(window, cx))), - ), - ) - .into_any() + ConfiguredApiCard::new(configured_card_label) + .disabled(env_var_set) + .on_click(cx.listener(|this, _, window, cx| this.reset_api_key(window, cx))) + .when(env_var_set, |this| { + this.tooltip_label(format!( + "To reset your API key, unset the {API_KEY_ENV_VAR_NAME} environment variable." + )) + }) + .into_any_element() } } } diff --git a/crates/language_models/src/provider/bedrock.rs b/crates/language_models/src/provider/bedrock.rs index 5699dd8e6693c26bd62f65fb160e0e30a62dda63..14dd575f23952ee732c5d9714d2e091cf50d606f 100644 --- a/crates/language_models/src/provider/bedrock.rs +++ b/crates/language_models/src/provider/bedrock.rs @@ -2,7 +2,7 @@ use std::pin::Pin; use std::str::FromStr; use std::sync::Arc; -use crate::ui::InstructionListItem; +use crate::ui::{ConfiguredApiCard, InstructionListItem}; use anyhow::{Context as _, Result, anyhow}; use aws_config::stalled_stream_protection::StalledStreamProtectionConfig; use aws_config::{BehaviorVersion, Region}; @@ -41,7 +41,7 @@ use serde_json::Value; use settings::{BedrockAvailableModel as AvailableModel, Settings, SettingsStore}; use smol::lock::OnceCell; use strum::{EnumIter, IntoEnumIterator, IntoStaticStr}; -use ui::{Icon, IconName, List, Tooltip, prelude::*}; +use ui::{List, prelude::*}; use ui_input::InputField; use util::ResultExt; @@ -1155,47 +1155,37 @@ impl Render for ConfigurationView { return div().child(Label::new("Loading credentials...")).into_any(); } + let configured_label = if env_var_set { + format!( + "Access Key ID is set in {ZED_BEDROCK_ACCESS_KEY_ID_VAR}, Secret Key is set in {ZED_BEDROCK_SECRET_ACCESS_KEY_VAR}, Region is set in {ZED_BEDROCK_REGION_VAR} environment variables." + ) + } else { + match bedrock_method { + Some(BedrockAuthMethod::Automatic) => "You are using automatic credentials.".into(), + Some(BedrockAuthMethod::NamedProfile) => "You are using named profile.".into(), + Some(BedrockAuthMethod::SingleSignOn) => { + "You are using a single sign on profile.".into() + } + None => "You are using static credentials.".into(), + } + }; + + let tooltip_label = if env_var_set { + Some(format!( + "To reset your credentials, unset the {ZED_BEDROCK_ACCESS_KEY_ID_VAR}, {ZED_BEDROCK_SECRET_ACCESS_KEY_VAR}, and {ZED_BEDROCK_REGION_VAR} environment variables." + )) + } else if bedrock_method.is_some() { + Some("You cannot reset credentials as they're being derived, check Zed settings to understand how.".to_string()) + } else { + None + }; + if self.should_render_editor(cx) { - return h_flex() - .mt_1() - .p_1() - .justify_between() - .rounded_md() - .border_1() - .border_color(cx.theme().colors().border) - .bg(cx.theme().colors().background) - .child( - h_flex() - .gap_1() - .child(Icon::new(IconName::Check).color(Color::Success)) - .child(Label::new(if env_var_set { - format!("Access Key ID is set in {ZED_BEDROCK_ACCESS_KEY_ID_VAR}, Secret Key is set in {ZED_BEDROCK_SECRET_ACCESS_KEY_VAR}, Region is set in {ZED_BEDROCK_REGION_VAR} environment variables.") - } else { - match bedrock_method { - Some(BedrockAuthMethod::Automatic) => "You are using automatic credentials".into(), - Some(BedrockAuthMethod::NamedProfile) => { - "You are using named profile".into() - }, - Some(BedrockAuthMethod::SingleSignOn) => "You are using a single sign on profile".into(), - None => "You are using static credentials".into(), - } - })), - ) - .child( - Button::new("reset-key", "Reset Key") - .icon(Some(IconName::Trash)) - .icon_size(IconSize::Small) - .icon_position(IconPosition::Start) - .disabled(env_var_set || bedrock_method.is_some()) - .when(env_var_set, |this| { - this.tooltip(Tooltip::text(format!("To reset your credentials, unset the {ZED_BEDROCK_ACCESS_KEY_ID_VAR}, {ZED_BEDROCK_SECRET_ACCESS_KEY_VAR}, and {ZED_BEDROCK_REGION_VAR} environment variables."))) - }) - .when(bedrock_method.is_some(), |this| { - this.tooltip(Tooltip::text("You cannot reset credentials as they're being derived, check Zed settings to understand how")) - }) - .on_click(cx.listener(|this, _, window, cx| this.reset_credentials(window, cx))), - ) - .into_any(); + return ConfiguredApiCard::new(configured_label) + .disabled(env_var_set || bedrock_method.is_some()) + .on_click(cx.listener(|this, _, window, cx| this.reset_credentials(window, cx))) + .when_some(tooltip_label, |this, label| this.tooltip_label(label)) + .into_any_element(); } v_flex() @@ -1241,7 +1231,7 @@ impl Render for ConfigurationView { } impl ConfigurationView { - fn render_static_credentials_ui(&self) -> AnyElement { + fn render_static_credentials_ui(&self) -> impl IntoElement { v_flex() .my_2() .gap_1p5() @@ -1278,6 +1268,5 @@ impl ConfigurationView { .child(self.secret_access_key_editor.clone()) .child(self.session_token_editor.clone()) .child(self.region_editor.clone()) - .into_any_element() } } diff --git a/crates/language_models/src/provider/copilot_chat.rs b/crates/language_models/src/provider/copilot_chat.rs index 6c665a0c1f06aa44e2b86f96517f7998fc02f4d3..0d95120322a592f1732aa53b3470108ccde76473 100644 --- a/crates/language_models/src/provider/copilot_chat.rs +++ b/crates/language_models/src/provider/copilot_chat.rs @@ -29,6 +29,8 @@ use settings::SettingsStore; use ui::{CommonAnimationExt, prelude::*}; use util::debug_panic; +use crate::ui::ConfiguredApiCard; + const PROVIDER_ID: LanguageModelProviderId = LanguageModelProviderId::new("copilot_chat"); const PROVIDER_NAME: LanguageModelProviderName = LanguageModelProviderName::new("GitHub Copilot Chat"); @@ -1326,27 +1328,12 @@ impl ConfigurationView { impl Render for ConfigurationView { fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { if self.state.read(cx).is_authenticated(cx) { - h_flex() - .mt_1() - .p_1() - .justify_between() - .rounded_md() - .border_1() - .border_color(cx.theme().colors().border) - .bg(cx.theme().colors().background) - .child( - h_flex() - .gap_1() - .child(Icon::new(IconName::Check).color(Color::Success)) - .child(Label::new("Authorized")), - ) - .child( - Button::new("sign_out", "Sign Out") - .label_size(LabelSize::Small) - .on_click(|_, window, cx| { - window.dispatch_action(copilot::SignOut.boxed_clone(), cx); - }), - ) + ConfiguredApiCard::new("Authorized") + .button_label("Sign Out") + .on_click(|_, window, cx| { + window.dispatch_action(copilot::SignOut.boxed_clone(), cx); + }) + .into_any_element() } else { let loading_icon = Icon::new(IconName::ArrowCircle).with_rotate_animation(4); @@ -1357,37 +1344,49 @@ impl Render for ConfigurationView { Status::Starting { task: _ } => h_flex() .gap_2() .child(loading_icon) - .child(Label::new("Starting Copilot…")), + .child(Label::new("Starting Copilot…")) + .into_any_element(), Status::SigningIn { prompt: _ } | Status::SignedOut { awaiting_signing_in: true, } => h_flex() .gap_2() .child(loading_icon) - .child(Label::new("Signing into Copilot…")), + .child(Label::new("Signing into Copilot…")) + .into_any_element(), Status::Error(_) => { const LABEL: &str = "Copilot had issues starting. Please try restarting it. If the issue persists, try reinstalling Copilot."; v_flex() .gap_6() .child(Label::new(LABEL)) .child(svg().size_8().path(IconName::CopilotError.path())) + .into_any_element() } _ => { const LABEL: &str = "To use Zed's agent with GitHub Copilot, you need to be logged in to GitHub. Note that your GitHub account must have an active Copilot Chat subscription."; - 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::Small) - .on_click(|_, window, cx| copilot::initiate_sign_in(window, cx)), - ) + 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::Small) + .on_click(|_, window, cx| { + copilot::initiate_sign_in(window, cx) + }), + ) + .into_any_element() } }, - None => v_flex().gap_6().child(Label::new(ERROR_LABEL)), + None => v_flex() + .gap_6() + .child(Label::new(ERROR_LABEL)) + .into_any_element(), } } } diff --git a/crates/language_models/src/provider/deepseek.rs b/crates/language_models/src/provider/deepseek.rs index 103d068d671acf331fbba73072e3edf2fcc10411..1d573fd964d0f183393bb766c492566f622a4901 100644 --- a/crates/language_models/src/provider/deepseek.rs +++ b/crates/language_models/src/provider/deepseek.rs @@ -19,11 +19,12 @@ use std::pin::Pin; use std::str::FromStr; use std::sync::{Arc, LazyLock}; -use ui::{Icon, IconName, List, prelude::*}; +use ui::{List, prelude::*}; use ui_input::InputField; use util::ResultExt; use zed_env_vars::{EnvVar, env_var}; +use crate::ui::ConfiguredApiCard; use crate::{api_key::ApiKeyState, ui::InstructionListItem}; const PROVIDER_ID: LanguageModelProviderId = LanguageModelProviderId::new("deepseek"); @@ -601,9 +602,21 @@ impl ConfigurationView { impl Render for ConfigurationView { fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { let env_var_set = self.state.read(cx).api_key_state.is_from_env_var(); + let configured_card_label = if env_var_set { + format!("API key set in {API_KEY_ENV_VAR_NAME} environment variable") + } else { + let api_url = DeepSeekLanguageModelProvider::api_url(cx); + if api_url == DEEPSEEK_API_URL { + "API key configured".to_string() + } else { + format!("API key configured for {}", api_url) + } + }; if self.load_credentials_task.is_some() { - div().child(Label::new("Loading credentials...")).into_any() + div() + .child(Label::new("Loading credentials...")) + .into_any_element() } else if self.should_render_editor(cx) { v_flex() .size_full() @@ -628,51 +641,12 @@ impl Render for ConfigurationView { .size(LabelSize::Small) .color(Color::Muted), ) - .into_any() + .into_any_element() } else { - h_flex() - .mt_1() - .p_1() - .justify_between() - .rounded_md() - .border_1() - .border_color(cx.theme().colors().border) - .bg(cx.theme().colors().background) - .child( - h_flex() - .flex_1() - .min_w_0() - .gap_1() - .child(Icon::new(IconName::Check).color(Color::Success)) - .child(div().w_full().overflow_x_hidden().text_ellipsis().child( - Label::new(if env_var_set { - format!( - "API key set in {API_KEY_ENV_VAR_NAME} environment variable" - ) - } else { - let api_url = DeepSeekLanguageModelProvider::api_url(cx); - if api_url == DEEPSEEK_API_URL { - "API key configured".to_string() - } else { - format!("API key configured for {}", api_url) - } - }), - )), - ) - .child( - h_flex().flex_shrink_0().child( - Button::new("reset-key", "Reset Key") - .label_size(LabelSize::Small) - .icon(Some(IconName::Trash)) - .icon_size(IconSize::Small) - .icon_position(IconPosition::Start) - .disabled(env_var_set) - .on_click( - cx.listener(|this, _, window, cx| this.reset_api_key(window, cx)), - ), - ), - ) - .into_any() + ConfiguredApiCard::new(configured_card_label) + .disabled(env_var_set) + .on_click(cx.listener(|this, _, window, cx| this.reset_api_key(window, cx))) + .into_any_element() } } } diff --git a/crates/language_models/src/provider/google.rs b/crates/language_models/src/provider/google.rs index a9c97ca939ac55c621bf38b62736af91ab2211ee..e33b118e30fca60e147bd2f311e844626da9b368 100644 --- a/crates/language_models/src/provider/google.rs +++ b/crates/language_models/src/provider/google.rs @@ -28,14 +28,14 @@ use std::sync::{ atomic::{self, AtomicU64}, }; use strum::IntoEnumIterator; -use ui::{Icon, IconName, List, Tooltip, prelude::*}; +use ui::{List, prelude::*}; use ui_input::InputField; use util::ResultExt; use zed_env_vars::EnvVar; use crate::api_key::ApiKey; use crate::api_key::ApiKeyState; -use crate::ui::InstructionListItem; +use crate::ui::{ConfiguredApiCard, InstructionListItem}; const PROVIDER_ID: LanguageModelProviderId = language_model::GOOGLE_PROVIDER_ID; const PROVIDER_NAME: LanguageModelProviderName = language_model::GOOGLE_PROVIDER_NAME; @@ -835,9 +835,24 @@ impl ConfigurationView { impl Render for ConfigurationView { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { let env_var_set = self.state.read(cx).api_key_state.is_from_env_var(); + let configured_card_label = if env_var_set { + format!( + "API key set in {} environment variable", + API_KEY_ENV_VAR.name + ) + } else { + let api_url = GoogleLanguageModelProvider::api_url(cx); + if api_url == google_ai::API_URL { + "API key configured".to_string() + } else { + format!("API key configured for {}", api_url) + } + }; if self.load_credentials_task.is_some() { - div().child(Label::new("Loading credentials...")).into_any() + div() + .child(Label::new("Loading credentials...")) + .into_any_element() } else if self.should_render_editor(cx) { v_flex() .size_full() @@ -864,58 +879,15 @@ impl Render for ConfigurationView { ) .size(LabelSize::Small).color(Color::Muted), ) - .into_any() + .into_any_element() } else { - h_flex() - .mt_1() - .p_1() - .justify_between() - .rounded_md() - .border_1() - .border_color(cx.theme().colors().border) - .bg(cx.theme().colors().background) - .child( - h_flex() - .flex_1() - .min_w_0() - .gap_1() - .child(Icon::new(IconName::Check).color(Color::Success)) - .child( - div() - .w_full() - .overflow_x_hidden() - .text_ellipsis() - .child(Label::new( - if env_var_set { - format!("API key set in {} environment variable", API_KEY_ENV_VAR.name) - } else { - let api_url = GoogleLanguageModelProvider::api_url(cx); - if api_url == google_ai::API_URL { - "API key configured".to_string() - } else { - format!("API key configured for {}", api_url) - } - } - )) - ), - ) - .child( - h_flex() - .flex_shrink_0() - .child( - Button::new("reset-key", "Reset Key") - .label_size(LabelSize::Small) - .icon(Some(IconName::Trash)) - .icon_size(IconSize::Small) - .icon_position(IconPosition::Start) - .disabled(env_var_set) - .when(env_var_set, |this| { - this.tooltip(Tooltip::text(format!("To reset your API key, make sure {GEMINI_API_KEY_VAR_NAME} and {GOOGLE_AI_API_KEY_VAR_NAME} environment variables are unset."))) - }) - .on_click(cx.listener(|this, _, window, cx| this.reset_api_key(window, cx))), - ), - ) - .into_any() + ConfiguredApiCard::new(configured_card_label) + .disabled(env_var_set) + .on_click(cx.listener(|this, _, window, cx| this.reset_api_key(window, cx))) + .when(env_var_set, |this| { + this.tooltip_label(format!("To reset your API key, make sure {GEMINI_API_KEY_VAR_NAME} and {GOOGLE_AI_API_KEY_VAR_NAME} environment variables are unset.")) + }) + .into_any_element() } } } diff --git a/crates/language_models/src/provider/mistral.rs b/crates/language_models/src/provider/mistral.rs index e0bfa5d8eb66ec4ec390bc534ceebe0663610197..2d30dfca21d8cbc4fd1be3575801919148f705b3 100644 --- a/crates/language_models/src/provider/mistral.rs +++ b/crates/language_models/src/provider/mistral.rs @@ -19,11 +19,12 @@ use std::pin::Pin; use std::str::FromStr; use std::sync::{Arc, LazyLock}; use strum::IntoEnumIterator; -use ui::{Icon, IconName, List, Tooltip, prelude::*}; +use ui::{List, prelude::*}; use ui_input::InputField; use util::ResultExt; use zed_env_vars::{EnvVar, env_var}; +use crate::ui::ConfiguredApiCard; use crate::{api_key::ApiKeyState, ui::InstructionListItem}; const PROVIDER_ID: LanguageModelProviderId = LanguageModelProviderId::new("mistral"); @@ -883,6 +884,12 @@ impl ConfigurationView { let key_state = &self.state.read(cx).codestral_api_key_state; let should_show_editor = !key_state.has_key(); let env_var_set = key_state.is_from_env_var(); + let configured_card_label = if env_var_set { + format!("API key set in {CODESTRAL_API_KEY_ENV_VAR_NAME} environment variable") + } else { + "Codestral API key configured".to_string() + }; + if should_show_editor { v_flex() .id("codestral") @@ -910,42 +917,19 @@ impl ConfigurationView { .size(LabelSize::Small).color(Color::Muted), ).into_any() } else { - h_flex() - .id("codestral") - .mt_2() - .p_1() - .justify_between() - .rounded_md() - .border_1() - .border_color(cx.theme().colors().border) - .bg(cx.theme().colors().background) - .child( - h_flex() - .gap_1() - .child(Icon::new(IconName::Check).color(Color::Success)) - .child(Label::new(if env_var_set { - format!("API key set in {CODESTRAL_API_KEY_ENV_VAR_NAME} environment variable") - } else { - "Codestral API key configured".to_string() - })), + ConfiguredApiCard::new(configured_card_label) + .disabled(env_var_set) + .on_click(cx.listener(|this, _, window, cx| this.reset_api_key(window, cx))) + .when(env_var_set, |this| { + this.tooltip_label(format!( + "To reset your API key, \ + unset the {CODESTRAL_API_KEY_ENV_VAR_NAME} environment variable." + )) + }) + .on_click( + cx.listener(|this, _, window, cx| this.reset_codestral_api_key(window, cx)), ) - .child( - Button::new("reset-key", "Reset Key") - .label_size(LabelSize::Small) - .icon(Some(IconName::Trash)) - .icon_size(IconSize::Small) - .icon_position(IconPosition::Start) - .disabled(env_var_set) - .when(env_var_set, |this| { - this.tooltip(Tooltip::text(format!( - "To reset your API key, \ - unset the {CODESTRAL_API_KEY_ENV_VAR_NAME} environment variable." - ))) - }) - .on_click( - cx.listener(|this, _, window, cx| this.reset_codestral_api_key(window, cx)), - ), - ).into_any() + .into_any_element() } } } @@ -953,6 +937,16 @@ impl ConfigurationView { impl Render for ConfigurationView { fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { let env_var_set = self.state.read(cx).api_key_state.is_from_env_var(); + let configured_card_label = if env_var_set { + format!("API key set in {API_KEY_ENV_VAR_NAME} environment variable") + } else { + let api_url = MistralLanguageModelProvider::api_url(cx); + if api_url == MISTRAL_API_URL { + "API key configured".to_string() + } else { + format!("API key configured for {}", api_url) + } + }; if self.load_credentials_task.is_some() { div().child(Label::new("Loading credentials...")).into_any() @@ -987,68 +981,17 @@ impl Render for ConfigurationView { } else { v_flex() .size_full() + .gap_1() .child( - h_flex() - .mt_1() - .p_1() - .justify_between() - .rounded_md() - .border_1() - .border_color(cx.theme().colors().border) - .bg(cx.theme().colors().background) - .child( - h_flex() - .flex_1() - .min_w_0() - .gap_1() - .child(Icon::new(IconName::Check).color(Color::Success)) - .child( - div() - .w_full() - .overflow_x_hidden() - .text_ellipsis() - .child( - Label::new( - if env_var_set { - format!( - "API key set in {API_KEY_ENV_VAR_NAME} environment variable" - ) - } else { - let api_url = MistralLanguageModelProvider::api_url(cx); - if api_url == MISTRAL_API_URL { - "API key configured".to_string() - } else { - format!( - "API key configured for {}", - api_url - ) - } - } - ) - ), - ), - ) - .child( - h_flex() - .flex_shrink_0() - .child( - Button::new("reset-key", "Reset Key") - .label_size(LabelSize::Small) - .icon(Some(IconName::Trash)) - .icon_size(IconSize::Small) - .icon_position(IconPosition::Start) - .disabled(env_var_set) - .when(env_var_set, |this| { - this.tooltip(Tooltip::text(format!( - "To reset your API key, \ - unset the {API_KEY_ENV_VAR_NAME} environment variable." - ))) - }) - .on_click(cx.listener(|this, _, window, cx| { - this.reset_api_key(window, cx) - })), - ), - ), + ConfiguredApiCard::new(configured_card_label) + .disabled(env_var_set) + .on_click(cx.listener(|this, _, window, cx| this.reset_api_key(window, cx))) + .when(env_var_set, |this| { + this.tooltip_label(format!( + "To reset your API key, \ + unset the {API_KEY_ENV_VAR_NAME} environment variable." + )) + }), ) .child(self.render_codestral_api_key_editor(cx)) .into_any() diff --git a/crates/language_models/src/provider/ollama.rs b/crates/language_models/src/provider/ollama.rs index 6341baa6f36db36a180d14c957b49dadd901e9a0..a0aada7d1a7b557e1e5aa07f19dd3e38492fc972 100644 --- a/crates/language_models/src/provider/ollama.rs +++ b/crates/language_models/src/provider/ollama.rs @@ -28,7 +28,7 @@ use zed_env_vars::{EnvVar, env_var}; use crate::AllLanguageModelSettings; use crate::api_key::ApiKeyState; -use crate::ui::InstructionListItem; +use crate::ui::{ConfiguredApiCard, InstructionListItem}; const OLLAMA_DOWNLOAD_URL: &str = "https://ollama.com/download"; const OLLAMA_LIBRARY_URL: &str = "https://ollama.com/library"; @@ -749,9 +749,14 @@ impl ConfigurationView { )) } - fn render_api_key_editor(&self, cx: &Context) -> Div { + fn render_api_key_editor(&self, cx: &Context) -> impl IntoElement { let state = self.state.read(cx); let env_var_set = state.api_key_state.is_from_env_var(); + let configured_card_label = if env_var_set { + format!("API key set in {API_KEY_ENV_VAR_NAME} environment variable.") + } else { + "API key configured".to_string() + }; if !state.api_key_state.has_key() { v_flex() @@ -764,40 +769,15 @@ impl ConfigurationView { .size(LabelSize::Small) .color(Color::Muted), ) + .into_any_element() } else { - h_flex() - .p_3() - .justify_between() - .rounded_md() - .border_1() - .border_color(cx.theme().colors().border) - .bg(cx.theme().colors().elevated_surface_background) - .child( - h_flex() - .gap_2() - .child(Icon::new(IconName::Check).color(Color::Success)) - .child( - Label::new( - if env_var_set { - format!("API key set in {API_KEY_ENV_VAR_NAME} environment variable.") - } else { - "API key configured".to_string() - } - ) - ) - ) - .child( - Button::new("reset-api-key", "Reset API Key") - .label_size(LabelSize::Small) - .icon(IconName::Undo) - .icon_size(IconSize::Small) - .icon_position(IconPosition::Start) - .layer(ElevationIndex::ModalSurface) - .when(env_var_set, |this| { - this.tooltip(Tooltip::text(format!("To reset your API key, unset the {API_KEY_ENV_VAR_NAME} environment variable."))) - }) - .on_click(cx.listener(|this, _, window, cx| this.reset_api_key(window, cx))), - ) + ConfiguredApiCard::new(configured_card_label) + .disabled(env_var_set) + .on_click(cx.listener(|this, _, window, cx| this.reset_api_key(window, cx))) + .when(env_var_set, |this| { + this.tooltip_label(format!("To reset your API key, unset the {API_KEY_ENV_VAR_NAME} environment variable.")) + }) + .into_any_element() } } @@ -909,7 +889,7 @@ impl Render for ConfigurationView { ) .child( IconButton::new("refresh-models", IconName::RotateCcw) - .tooltip(Tooltip::text("Refresh models")) + .tooltip(Tooltip::text("Refresh Models")) .on_click(cx.listener(|this, _, _, cx| { this.state.update(cx, |state, _| { state.fetched_models.clear(); diff --git a/crates/language_models/src/provider/open_ai.rs b/crates/language_models/src/provider/open_ai.rs index aa925a9b582adae5c1ade0b9cdce6765e8614cb6..cabd78c35be58667fd799fe34de07e1d1bfa5808 100644 --- a/crates/language_models/src/provider/open_ai.rs +++ b/crates/language_models/src/provider/open_ai.rs @@ -20,11 +20,12 @@ use std::pin::Pin; use std::str::FromStr as _; use std::sync::{Arc, LazyLock}; use strum::IntoEnumIterator; -use ui::{ElevationIndex, List, Tooltip, prelude::*}; +use ui::{List, prelude::*}; use ui_input::InputField; use util::ResultExt; use zed_env_vars::{EnvVar, env_var}; +use crate::ui::ConfiguredApiCard; use crate::{api_key::ApiKeyState, ui::InstructionListItem}; const PROVIDER_ID: LanguageModelProviderId = language_model::OPEN_AI_PROVIDER_ID; @@ -762,6 +763,16 @@ impl ConfigurationView { impl Render for ConfigurationView { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { let env_var_set = self.state.read(cx).api_key_state.is_from_env_var(); + let configured_card_label = if env_var_set { + format!("API key set in {API_KEY_ENV_VAR_NAME} environment variable") + } else { + let api_url = OpenAiLanguageModelProvider::api_url(cx); + if api_url == OPEN_AI_API_URL { + "API key configured".to_string() + } else { + format!("API key configured for {}", api_url) + } + }; let api_key_section = if self.should_render_editor(cx) { v_flex() @@ -795,58 +806,15 @@ impl Render for ConfigurationView { ) .size(LabelSize::Small).color(Color::Muted), ) - .into_any() + .into_any_element() } else { - h_flex() - .mt_1() - .p_1() - .justify_between() - .rounded_md() - .border_1() - .border_color(cx.theme().colors().border) - .bg(cx.theme().colors().background) - .child( - h_flex() - .flex_1() - .min_w_0() - .gap_1() - .child(Icon::new(IconName::Check).color(Color::Success)) - .child( - div() - .w_full() - .overflow_x_hidden() - .text_ellipsis() - .child(Label::new( - if env_var_set { - format!("API key set in {API_KEY_ENV_VAR_NAME} environment variable") - } else { - let api_url = OpenAiLanguageModelProvider::api_url(cx); - if api_url == OPEN_AI_API_URL { - "API key configured".to_string() - } else { - format!("API key configured for {}", api_url) - } - } - )) - ), - ) - .child( - h_flex() - .flex_shrink_0() - .child( - Button::new("reset-api-key", "Reset API Key") - .label_size(LabelSize::Small) - .icon(IconName::Undo) - .icon_size(IconSize::Small) - .icon_position(IconPosition::Start) - .layer(ElevationIndex::ModalSurface) - .when(env_var_set, |this| { - this.tooltip(Tooltip::text(format!("To reset your API key, unset the {API_KEY_ENV_VAR_NAME} environment variable."))) - }) - .on_click(cx.listener(|this, _, window, cx| this.reset_api_key(window, cx))), - ), - ) - .into_any() + ConfiguredApiCard::new(configured_card_label) + .disabled(env_var_set) + .on_click(cx.listener(|this, _, window, cx| this.reset_api_key(window, cx))) + .when(env_var_set, |this| { + this.tooltip_label(format!("To reset your API key, unset the {API_KEY_ENV_VAR_NAME} environment variable.")) + }) + .into_any_element() }; let compatible_api_section = h_flex() diff --git a/crates/language_models/src/provider/open_router.rs b/crates/language_models/src/provider/open_router.rs index 0cc3711489ab25b80c9e995558f18917fbfad343..6326968a916a7d6a21811ee22c328564e1ec4682 100644 --- a/crates/language_models/src/provider/open_router.rs +++ b/crates/language_models/src/provider/open_router.rs @@ -17,11 +17,12 @@ use settings::{OpenRouterAvailableModel as AvailableModel, Settings, SettingsSto use std::pin::Pin; use std::str::FromStr as _; use std::sync::{Arc, LazyLock}; -use ui::{Icon, IconName, List, Tooltip, prelude::*}; +use ui::{List, prelude::*}; use ui_input::InputField; use util::ResultExt; use zed_env_vars::{EnvVar, env_var}; +use crate::ui::ConfiguredApiCard; use crate::{api_key::ApiKeyState, ui::InstructionListItem}; const PROVIDER_ID: LanguageModelProviderId = LanguageModelProviderId::new("openrouter"); @@ -777,9 +778,21 @@ impl ConfigurationView { impl Render for ConfigurationView { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { let env_var_set = self.state.read(cx).api_key_state.is_from_env_var(); + let configured_card_label = if env_var_set { + format!("API key set in {API_KEY_ENV_VAR_NAME} environment variable") + } else { + let api_url = OpenRouterLanguageModelProvider::api_url(cx); + if api_url == OPEN_ROUTER_API_URL { + "API key configured".to_string() + } else { + format!("API key configured for {}", api_url) + } + }; if self.load_credentials_task.is_some() { - div().child(Label::new("Loading credentials...")).into_any() + div() + .child(Label::new("Loading credentials...")) + .into_any_element() } else if self.should_render_editor(cx) { v_flex() .size_full() @@ -806,56 +819,15 @@ impl Render for ConfigurationView { ) .size(LabelSize::Small).color(Color::Muted), ) - .into_any() + .into_any_element() } else { - h_flex() - .mt_1() - .p_1() - .justify_between() - .rounded_md() - .border_1() - .border_color(cx.theme().colors().border) - .bg(cx.theme().colors().background) - .child( - h_flex() - .flex_1() - .min_w_0() - .gap_1() - .child(Icon::new(IconName::Check).color(Color::Success)) - .child( - div() - .w_full() - .overflow_x_hidden() - .text_ellipsis() - .child(Label::new(if env_var_set { - format!("API key set in {API_KEY_ENV_VAR_NAME} environment variable") - } else { - let api_url = OpenRouterLanguageModelProvider::api_url(cx); - if api_url == OPEN_ROUTER_API_URL { - "API key configured".to_string() - } else { - format!("API key configured for {}", api_url) - } - })) - ), - ) - .child( - h_flex() - .flex_shrink_0() - .child( - Button::new("reset-key", "Reset Key") - .label_size(LabelSize::Small) - .icon(Some(IconName::Trash)) - .icon_size(IconSize::Small) - .icon_position(IconPosition::Start) - .disabled(env_var_set) - .when(env_var_set, |this| { - this.tooltip(Tooltip::text(format!("To reset your API key, unset the {API_KEY_ENV_VAR_NAME} environment variable."))) - }) - .on_click(cx.listener(|this, _, window, cx| this.reset_api_key(window, cx))), - ), - ) - .into_any() + ConfiguredApiCard::new(configured_card_label) + .disabled(env_var_set) + .on_click(cx.listener(|this, _, window, cx| this.reset_api_key(window, cx))) + .when(env_var_set, |this| { + this.tooltip_label(format!("To reset your API key, unset the {API_KEY_ENV_VAR_NAME} environment variable.")) + }) + .into_any_element() } } } diff --git a/crates/language_models/src/provider/vercel.rs b/crates/language_models/src/provider/vercel.rs index 9adc794ceaf255756736b401fff45f1131d4b310..20db24274aae0249efcfc897cb1bdfdcce8f1220 100644 --- a/crates/language_models/src/provider/vercel.rs +++ b/crates/language_models/src/provider/vercel.rs @@ -14,13 +14,16 @@ pub use settings::VercelAvailableModel as AvailableModel; use settings::{Settings, SettingsStore}; use std::sync::{Arc, LazyLock}; use strum::IntoEnumIterator; -use ui::{ElevationIndex, List, Tooltip, prelude::*}; +use ui::{List, prelude::*}; use ui_input::InputField; use util::ResultExt; use vercel::{Model, VERCEL_API_URL}; use zed_env_vars::{EnvVar, env_var}; -use crate::{api_key::ApiKeyState, ui::InstructionListItem}; +use crate::{ + api_key::ApiKeyState, + ui::{ConfiguredApiCard, InstructionListItem}, +}; const PROVIDER_ID: LanguageModelProviderId = LanguageModelProviderId::new("vercel"); const PROVIDER_NAME: LanguageModelProviderName = LanguageModelProviderName::new("Vercel"); @@ -448,6 +451,16 @@ impl ConfigurationView { impl Render for ConfigurationView { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { let env_var_set = self.state.read(cx).api_key_state.is_from_env_var(); + let configured_card_label = if env_var_set { + format!("API key set in {API_KEY_ENV_VAR_NAME} environment variable") + } else { + let api_url = VercelLanguageModelProvider::api_url(cx); + if api_url == VERCEL_API_URL { + "API key configured".to_string() + } else { + format!("API key configured for {}", api_url) + } + }; let api_key_section = if self.should_render_editor(cx) { v_flex() @@ -477,56 +490,15 @@ impl Render for ConfigurationView { .size(LabelSize::Small) .color(Color::Muted), ) - .into_any() + .into_any_element() } else { - h_flex() - .mt_1() - .p_1() - .justify_between() - .rounded_md() - .border_1() - .border_color(cx.theme().colors().border) - .bg(cx.theme().colors().background) - .child( - h_flex() - .flex_1() - .min_w_0() - .gap_1() - .child(Icon::new(IconName::Check).color(Color::Success)) - .child( - div() - .w_full() - .overflow_x_hidden() - .text_ellipsis() - .child(Label::new(if env_var_set { - format!("API key set in {API_KEY_ENV_VAR_NAME} environment variable") - } else { - let api_url = VercelLanguageModelProvider::api_url(cx); - if api_url == VERCEL_API_URL { - "API key configured".to_string() - } else { - format!("API key configured for {}", api_url) - } - })) - ), - ) - .child( - h_flex() - .flex_shrink_0() - .child( - Button::new("reset-api-key", "Reset API Key") - .label_size(LabelSize::Small) - .icon(IconName::Undo) - .icon_size(IconSize::Small) - .icon_position(IconPosition::Start) - .layer(ElevationIndex::ModalSurface) - .when(env_var_set, |this| { - this.tooltip(Tooltip::text(format!("To reset your API key, unset the {API_KEY_ENV_VAR_NAME} environment variable."))) - }) - .on_click(cx.listener(|this, _, window, cx| this.reset_api_key(window, cx))), - ), - ) - .into_any() + ConfiguredApiCard::new(configured_card_label) + .disabled(env_var_set) + .when(env_var_set, |this| { + this.tooltip_label(format!("To reset your API key, unset the {API_KEY_ENV_VAR_NAME} environment variable.")) + }) + .on_click(cx.listener(|this, _, window, cx| this.reset_api_key(window, cx))) + .into_any_element() }; if self.load_credentials_task.is_some() { diff --git a/crates/language_models/src/provider/x_ai.rs b/crates/language_models/src/provider/x_ai.rs index 979824442c6d03a8f735448003425e94a83e46ea..e7ee71ba86e202fe17d567923f4b04d3c886ae08 100644 --- a/crates/language_models/src/provider/x_ai.rs +++ b/crates/language_models/src/provider/x_ai.rs @@ -14,13 +14,16 @@ pub use settings::XaiAvailableModel as AvailableModel; use settings::{Settings, SettingsStore}; use std::sync::{Arc, LazyLock}; use strum::IntoEnumIterator; -use ui::{ElevationIndex, List, Tooltip, prelude::*}; +use ui::{List, prelude::*}; use ui_input::InputField; use util::ResultExt; use x_ai::{Model, XAI_API_URL}; use zed_env_vars::{EnvVar, env_var}; -use crate::{api_key::ApiKeyState, ui::InstructionListItem}; +use crate::{ + api_key::ApiKeyState, + ui::{ConfiguredApiCard, InstructionListItem}, +}; const PROVIDER_ID: LanguageModelProviderId = LanguageModelProviderId::new("x_ai"); const PROVIDER_NAME: LanguageModelProviderName = LanguageModelProviderName::new("xAI"); @@ -445,6 +448,16 @@ impl ConfigurationView { impl Render for ConfigurationView { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { let env_var_set = self.state.read(cx).api_key_state.is_from_env_var(); + let configured_card_label = if env_var_set { + format!("API key set in {API_KEY_ENV_VAR_NAME} environment variable") + } else { + let api_url = XAiLanguageModelProvider::api_url(cx); + if api_url == XAI_API_URL { + "API key configured".to_string() + } else { + format!("API key configured for {}", api_url) + } + }; let api_key_section = if self.should_render_editor(cx) { v_flex() @@ -474,56 +487,15 @@ impl Render for ConfigurationView { .size(LabelSize::Small) .color(Color::Muted), ) - .into_any() + .into_any_element() } else { - h_flex() - .mt_1() - .p_1() - .justify_between() - .rounded_md() - .border_1() - .border_color(cx.theme().colors().border) - .bg(cx.theme().colors().background) - .child( - h_flex() - .flex_1() - .min_w_0() - .gap_1() - .child(Icon::new(IconName::Check).color(Color::Success)) - .child( - div() - .w_full() - .overflow_x_hidden() - .text_ellipsis() - .child(Label::new(if env_var_set { - format!("API key set in {API_KEY_ENV_VAR_NAME} environment variable") - } else { - let api_url = XAiLanguageModelProvider::api_url(cx); - if api_url == XAI_API_URL { - "API key configured".to_string() - } else { - format!("API key configured for {}", api_url) - } - })) - ), - ) - .child( - h_flex() - .flex_shrink_0() - .child( - Button::new("reset-api-key", "Reset API Key") - .label_size(LabelSize::Small) - .icon(IconName::Undo) - .icon_size(IconSize::Small) - .icon_position(IconPosition::Start) - .layer(ElevationIndex::ModalSurface) - .when(env_var_set, |this| { - this.tooltip(Tooltip::text(format!("To reset your API key, unset the {API_KEY_ENV_VAR_NAME} environment variable."))) - }) - .on_click(cx.listener(|this, _, window, cx| this.reset_api_key(window, cx))), - ), - ) - .into_any() + ConfiguredApiCard::new(configured_card_label) + .disabled(env_var_set) + .when(env_var_set, |this| { + this.tooltip_label(format!("To reset your API key, unset the {API_KEY_ENV_VAR_NAME} environment variable.")) + }) + .on_click(cx.listener(|this, _, window, cx| this.reset_api_key(window, cx))) + .into_any_element() }; if self.load_credentials_task.is_some() { diff --git a/crates/language_models/src/ui.rs b/crates/language_models/src/ui.rs index 80321656007ab3dfa19a5171d5bead18c9a5cc99..1d7796ecc2b6c2a78b3ebc02dc9cd29bd8cfa2c6 100644 --- a/crates/language_models/src/ui.rs +++ b/crates/language_models/src/ui.rs @@ -1,2 +1,4 @@ +pub mod configured_api_card; pub mod instruction_list_item; +pub use configured_api_card::ConfiguredApiCard; pub use instruction_list_item::InstructionListItem; diff --git a/crates/language_models/src/ui/configured_api_card.rs b/crates/language_models/src/ui/configured_api_card.rs new file mode 100644 index 0000000000000000000000000000000000000000..063ac1717f3aa5de1a448e26c94df7530fec588f --- /dev/null +++ b/crates/language_models/src/ui/configured_api_card.rs @@ -0,0 +1,86 @@ +use gpui::{ClickEvent, IntoElement, ParentElement, SharedString}; +use ui::{Tooltip, prelude::*}; + +#[derive(IntoElement)] +pub struct ConfiguredApiCard { + label: SharedString, + button_label: Option, + tooltip_label: Option, + disabled: bool, + on_click: Option>, +} + +impl ConfiguredApiCard { + pub fn new(label: impl Into) -> Self { + Self { + label: label.into(), + button_label: None, + tooltip_label: None, + disabled: false, + on_click: None, + } + } + + pub fn on_click( + mut self, + handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, + ) -> Self { + self.on_click = Some(Box::new(handler)); + self + } + + pub fn button_label(mut self, button_label: impl Into) -> Self { + self.button_label = Some(button_label.into()); + self + } + + pub fn tooltip_label(mut self, tooltip_label: impl Into) -> Self { + self.tooltip_label = Some(tooltip_label.into()); + self + } + + pub fn disabled(mut self, disabled: bool) -> Self { + self.disabled = disabled; + self + } +} + +impl RenderOnce for ConfiguredApiCard { + fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { + let button_label = self.button_label.unwrap_or("Reset Key".into()); + let button_id = SharedString::new(format!("id-{}", button_label)); + + h_flex() + .mt_0p5() + .p_1() + .justify_between() + .rounded_md() + .border_1() + .border_color(cx.theme().colors().border) + .bg(cx.theme().colors().background) + .child( + h_flex() + .flex_1() + .min_w_0() + .gap_1() + .child(Icon::new(IconName::Check).color(Color::Success)) + .child(Label::new(self.label).truncate()), + ) + .child( + Button::new(button_id, button_label) + .label_size(LabelSize::Small) + .icon(IconName::Undo) + .icon_size(IconSize::Small) + .icon_color(Color::Muted) + .icon_position(IconPosition::Start) + .disabled(self.disabled) + .when_some(self.tooltip_label, |this, label| { + this.tooltip(Tooltip::text(label)) + }) + .when_some( + self.on_click.filter(|_| !self.disabled), + |this, on_click| this.on_click(on_click), + ), + ) + } +}