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 86cc81cb7bc..287c76fc6df 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 5699dd8e669..14dd575f239 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 6c665a0c1f0..0d95120322a 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 103d068d671..1d573fd964d 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 a9c97ca939a..e33b118e30f 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 e0bfa5d8eb6..2d30dfca21d 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 6341baa6f36..a0aada7d1a7 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 aa925a9b582..cabd78c35be 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 0cc3711489a..6326968a916 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 9adc794ceaf..20db24274aa 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 979824442c6..e7ee71ba86e 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 80321656007..1d7796ecc2b 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 00000000000..063ac1717f3 --- /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), + ), + ) + } +}