assistant panel: Show if env var with API key is set (#16453)

This makes it easier to debug why resetting a key doesn't work. We now
show when the key is set via an env var and if so, we disable the
reset-key button and instead give instructions.

![screenshot-2024-08-19-11 22
05@2x](https://github.com/user-attachments/assets/6c75dc82-cb61-4661-9647-f77fca8fdf41)


Release Notes:

- N/A

Co-authored-by: Bennet <bennet@zed.dev>
This commit is contained in:
Thorsten Ball 2024-08-19 11:34:58 +02:00 committed by GitHub
parent 14fa4abce4
commit f651333896
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 68 additions and 18 deletions

View File

@ -19,7 +19,7 @@ use settings::{Settings, SettingsStore};
use std::{sync::Arc, time::Duration}; use std::{sync::Arc, time::Duration};
use strum::IntoEnumIterator; use strum::IntoEnumIterator;
use theme::ThemeSettings; use theme::ThemeSettings;
use ui::{prelude::*, Icon, IconName}; use ui::{prelude::*, Icon, IconName, Tooltip};
use util::ResultExt; use util::ResultExt;
const PROVIDER_ID: &str = "anthropic"; const PROVIDER_ID: &str = "anthropic";
@ -54,8 +54,11 @@ pub struct AnthropicLanguageModelProvider {
state: gpui::Model<State>, state: gpui::Model<State>,
} }
const ANTHROPIC_API_KEY_VAR: &'static str = "ANTHROPIC_API_KEY";
pub struct State { pub struct State {
api_key: Option<String>, api_key: Option<String>,
api_key_from_env: bool,
_subscription: Subscription, _subscription: Subscription,
} }
@ -67,6 +70,7 @@ impl State {
delete_credentials.await.ok(); delete_credentials.await.ok();
this.update(&mut cx, |this, cx| { this.update(&mut cx, |this, cx| {
this.api_key = None; this.api_key = None;
this.api_key_from_env = false;
cx.notify(); cx.notify();
}) })
}) })
@ -105,18 +109,20 @@ impl State {
.clone(); .clone();
cx.spawn(|this, mut cx| async move { cx.spawn(|this, mut cx| async move {
let api_key = if let Ok(api_key) = std::env::var("ANTHROPIC_API_KEY") { let (api_key, from_env) = if let Ok(api_key) = std::env::var(ANTHROPIC_API_KEY_VAR)
api_key {
(api_key, true)
} else { } else {
let (_, api_key) = cx let (_, api_key) = cx
.update(|cx| cx.read_credentials(&api_url))? .update(|cx| cx.read_credentials(&api_url))?
.await? .await?
.ok_or_else(|| anyhow!("credentials not found"))?; .ok_or_else(|| anyhow!("credentials not found"))?;
String::from_utf8(api_key)? (String::from_utf8(api_key)?, false)
}; };
this.update(&mut cx, |this, cx| { this.update(&mut cx, |this, cx| {
this.api_key = Some(api_key); this.api_key = Some(api_key);
this.api_key_from_env = from_env;
cx.notify(); cx.notify();
}) })
}) })
@ -128,6 +134,7 @@ impl AnthropicLanguageModelProvider {
pub fn new(http_client: Arc<dyn HttpClient>, cx: &mut AppContext) -> Self { pub fn new(http_client: Arc<dyn HttpClient>, cx: &mut AppContext) -> Self {
let state = cx.new_model(|cx| State { let state = cx.new_model(|cx| State {
api_key: None, api_key: None,
api_key_from_env: false,
_subscription: cx.observe_global::<SettingsStore>(|_, cx| { _subscription: cx.observe_global::<SettingsStore>(|_, cx| {
cx.notify(); cx.notify();
}), }),
@ -537,6 +544,8 @@ impl Render for ConfigurationView {
"Paste your Anthropic API key below and hit enter to use the assistant:", "Paste your Anthropic API key below and hit enter to use the assistant:",
]; ];
let env_var_set = self.state.read(cx).api_key_from_env;
if self.load_credentials_task.is_some() { if self.load_credentials_task.is_some() {
div().child(Label::new("Loading credentials...")).into_any() div().child(Label::new("Loading credentials...")).into_any()
} else if self.should_render_editor(cx) { } else if self.should_render_editor(cx) {
@ -558,7 +567,7 @@ impl Render for ConfigurationView {
) )
.child( .child(
Label::new( Label::new(
"You can also assign the ANTHROPIC_API_KEY environment variable and restart Zed.", "You can also assign the {ANTHROPIC_API_KEY_VAR} environment variable and restart Zed.",
) )
.size(LabelSize::Small), .size(LabelSize::Small),
) )
@ -571,13 +580,21 @@ impl Render for ConfigurationView {
h_flex() h_flex()
.gap_1() .gap_1()
.child(Icon::new(IconName::Check).color(Color::Success)) .child(Icon::new(IconName::Check).color(Color::Success))
.child(Label::new("API key configured.")), .child(Label::new(if env_var_set {
format!("API key set in {ANTHROPIC_API_KEY_VAR} environment variable.")
} else {
"API key configured.".to_string()
})),
) )
.child( .child(
Button::new("reset-key", "Reset key") Button::new("reset-key", "Reset key")
.icon(Some(IconName::Trash)) .icon(Some(IconName::Trash))
.icon_size(IconSize::Small) .icon_size(IconSize::Small)
.icon_position(IconPosition::Start) .icon_position(IconPosition::Start)
.disabled(env_var_set)
.when(env_var_set, |this| {
this.tooltip(|cx| Tooltip::text(format!("To reset your API key, unset the {ANTHROPIC_API_KEY_VAR} environment variable."), cx))
})
.on_click(cx.listener(|this, _, cx| this.reset_api_key(cx))), .on_click(cx.listener(|this, _, cx| this.reset_api_key(cx))),
) )
.into_any() .into_any()

View File

@ -14,7 +14,7 @@ use settings::{Settings, SettingsStore};
use std::{future, sync::Arc, time::Duration}; use std::{future, sync::Arc, time::Duration};
use strum::IntoEnumIterator; use strum::IntoEnumIterator;
use theme::ThemeSettings; use theme::ThemeSettings;
use ui::{prelude::*, Icon, IconName}; use ui::{prelude::*, Icon, IconName, Tooltip};
use util::ResultExt; use util::ResultExt;
use crate::{ use crate::{
@ -46,9 +46,12 @@ pub struct GoogleLanguageModelProvider {
pub struct State { pub struct State {
api_key: Option<String>, api_key: Option<String>,
api_key_from_env: bool,
_subscription: Subscription, _subscription: Subscription,
} }
const GOOGLE_AI_API_KEY_VAR: &'static str = "GOOGLE_AI_API_KEY";
impl State { impl State {
fn is_authenticated(&self) -> bool { fn is_authenticated(&self) -> bool {
self.api_key.is_some() self.api_key.is_some()
@ -61,6 +64,7 @@ impl State {
delete_credentials.await.ok(); delete_credentials.await.ok();
this.update(&mut cx, |this, cx| { this.update(&mut cx, |this, cx| {
this.api_key = None; this.api_key = None;
this.api_key_from_env = false;
cx.notify(); cx.notify();
}) })
}) })
@ -90,18 +94,20 @@ impl State {
.clone(); .clone();
cx.spawn(|this, mut cx| async move { cx.spawn(|this, mut cx| async move {
let api_key = if let Ok(api_key) = std::env::var("GOOGLE_AI_API_KEY") { let (api_key, from_env) = if let Ok(api_key) = std::env::var(GOOGLE_AI_API_KEY_VAR)
api_key {
(api_key, true)
} else { } else {
let (_, api_key) = cx let (_, api_key) = cx
.update(|cx| cx.read_credentials(&api_url))? .update(|cx| cx.read_credentials(&api_url))?
.await? .await?
.ok_or_else(|| anyhow!("credentials not found"))?; .ok_or_else(|| anyhow!("credentials not found"))?;
String::from_utf8(api_key)? (String::from_utf8(api_key)?, false)
}; };
this.update(&mut cx, |this, cx| { this.update(&mut cx, |this, cx| {
this.api_key = Some(api_key); this.api_key = Some(api_key);
this.api_key_from_env = from_env;
cx.notify(); cx.notify();
}) })
}) })
@ -113,6 +119,7 @@ impl GoogleLanguageModelProvider {
pub fn new(http_client: Arc<dyn HttpClient>, cx: &mut AppContext) -> Self { pub fn new(http_client: Arc<dyn HttpClient>, cx: &mut AppContext) -> Self {
let state = cx.new_model(|cx| State { let state = cx.new_model(|cx| State {
api_key: None, api_key: None,
api_key_from_env: false,
_subscription: cx.observe_global::<SettingsStore>(|_, cx| { _subscription: cx.observe_global::<SettingsStore>(|_, cx| {
cx.notify(); cx.notify();
}), }),
@ -422,6 +429,8 @@ impl Render for ConfigurationView {
"Paste your Google AI API key below and hit enter to use the assistant:", "Paste your Google AI API key below and hit enter to use the assistant:",
]; ];
let env_var_set = self.state.read(cx).api_key_from_env;
if self.load_credentials_task.is_some() { if self.load_credentials_task.is_some() {
div().child(Label::new("Loading credentials...")).into_any() div().child(Label::new("Loading credentials...")).into_any()
} else if self.should_render_editor(cx) { } else if self.should_render_editor(cx) {
@ -443,7 +452,7 @@ impl Render for ConfigurationView {
) )
.child( .child(
Label::new( Label::new(
"You can also assign the GOOGLE_AI_API_KEY environment variable and restart Zed.", format!("You can also assign the {GOOGLE_AI_API_KEY_VAR} environment variable and restart Zed."),
) )
.size(LabelSize::Small), .size(LabelSize::Small),
) )
@ -456,13 +465,21 @@ impl Render for ConfigurationView {
h_flex() h_flex()
.gap_1() .gap_1()
.child(Icon::new(IconName::Check).color(Color::Success)) .child(Icon::new(IconName::Check).color(Color::Success))
.child(Label::new("API key configured.")), .child(Label::new(if env_var_set {
format!("API key set in {GOOGLE_AI_API_KEY_VAR} environment variable.")
} else {
"API key configured.".to_string()
})),
) )
.child( .child(
Button::new("reset-key", "Reset key") Button::new("reset-key", "Reset key")
.icon(Some(IconName::Trash)) .icon(Some(IconName::Trash))
.icon_size(IconSize::Small) .icon_size(IconSize::Small)
.icon_position(IconPosition::Start) .icon_position(IconPosition::Start)
.disabled(env_var_set)
.when(env_var_set, |this| {
this.tooltip(|cx| Tooltip::text(format!("To reset your API key, unset the {GOOGLE_AI_API_KEY_VAR} environment variable."), cx))
})
.on_click(cx.listener(|this, _, cx| this.reset_api_key(cx))), .on_click(cx.listener(|this, _, cx| this.reset_api_key(cx))),
) )
.into_any() .into_any()

View File

@ -16,7 +16,7 @@ use settings::{Settings, SettingsStore};
use std::{sync::Arc, time::Duration}; use std::{sync::Arc, time::Duration};
use strum::IntoEnumIterator; use strum::IntoEnumIterator;
use theme::ThemeSettings; use theme::ThemeSettings;
use ui::{prelude::*, Icon, IconName}; use ui::{prelude::*, Icon, IconName, Tooltip};
use util::ResultExt; use util::ResultExt;
use crate::{ use crate::{
@ -49,9 +49,12 @@ pub struct OpenAiLanguageModelProvider {
pub struct State { pub struct State {
api_key: Option<String>, api_key: Option<String>,
api_key_from_env: bool,
_subscription: Subscription, _subscription: Subscription,
} }
const OPENAI_API_KEY_VAR: &'static str = "OPENAI_API_KEY";
impl State { impl State {
fn is_authenticated(&self) -> bool { fn is_authenticated(&self) -> bool {
self.api_key.is_some() self.api_key.is_some()
@ -64,6 +67,7 @@ impl State {
delete_credentials.await.log_err(); delete_credentials.await.log_err();
this.update(&mut cx, |this, cx| { this.update(&mut cx, |this, cx| {
this.api_key = None; this.api_key = None;
this.api_key_from_env = false;
cx.notify(); cx.notify();
}) })
}) })
@ -92,17 +96,18 @@ impl State {
.api_url .api_url
.clone(); .clone();
cx.spawn(|this, mut cx| async move { cx.spawn(|this, mut cx| async move {
let api_key = if let Ok(api_key) = std::env::var("OPENAI_API_KEY") { let (api_key, from_env) = if let Ok(api_key) = std::env::var(OPENAI_API_KEY_VAR) {
api_key (api_key, true)
} else { } else {
let (_, api_key) = cx let (_, api_key) = cx
.update(|cx| cx.read_credentials(&api_url))? .update(|cx| cx.read_credentials(&api_url))?
.await? .await?
.ok_or_else(|| anyhow!("credentials not found"))?; .ok_or_else(|| anyhow!("credentials not found"))?;
String::from_utf8(api_key)? (String::from_utf8(api_key)?, false)
}; };
this.update(&mut cx, |this, cx| { this.update(&mut cx, |this, cx| {
this.api_key = Some(api_key); this.api_key = Some(api_key);
this.api_key_from_env = from_env;
cx.notify(); cx.notify();
}) })
}) })
@ -114,6 +119,7 @@ impl OpenAiLanguageModelProvider {
pub fn new(http_client: Arc<dyn HttpClient>, cx: &mut AppContext) -> Self { pub fn new(http_client: Arc<dyn HttpClient>, cx: &mut AppContext) -> Self {
let state = cx.new_model(|cx| State { let state = cx.new_model(|cx| State {
api_key: None, api_key: None,
api_key_from_env: false,
_subscription: cx.observe_global::<SettingsStore>(|_this: &mut State, cx| { _subscription: cx.observe_global::<SettingsStore>(|_this: &mut State, cx| {
cx.notify(); cx.notify();
}), }),
@ -476,6 +482,8 @@ impl Render for ConfigurationView {
"Paste your OpenAI API key below and hit enter to use the assistant:", "Paste your OpenAI API key below and hit enter to use the assistant:",
]; ];
let env_var_set = self.state.read(cx).api_key_from_env;
if self.load_credentials_task.is_some() { if self.load_credentials_task.is_some() {
div().child(Label::new("Loading credentials...")).into_any() div().child(Label::new("Loading credentials...")).into_any()
} else if self.should_render_editor(cx) { } else if self.should_render_editor(cx) {
@ -497,7 +505,7 @@ impl Render for ConfigurationView {
) )
.child( .child(
Label::new( Label::new(
"You can also assign the OPENAI_API_KEY environment variable and restart Zed.", format!("You can also assign the {OPENAI_API_KEY_VAR} environment variable and restart Zed."),
) )
.size(LabelSize::Small), .size(LabelSize::Small),
) )
@ -510,13 +518,21 @@ impl Render for ConfigurationView {
h_flex() h_flex()
.gap_1() .gap_1()
.child(Icon::new(IconName::Check).color(Color::Success)) .child(Icon::new(IconName::Check).color(Color::Success))
.child(Label::new("API key configured.")), .child(Label::new(if env_var_set {
format!("API key set in {OPENAI_API_KEY_VAR} environment variable.")
} else {
"API key configured.".to_string()
})),
) )
.child( .child(
Button::new("reset-key", "Reset key") Button::new("reset-key", "Reset key")
.icon(Some(IconName::Trash)) .icon(Some(IconName::Trash))
.icon_size(IconSize::Small) .icon_size(IconSize::Small)
.icon_position(IconPosition::Start) .icon_position(IconPosition::Start)
.disabled(env_var_set)
.when(env_var_set, |this| {
this.tooltip(|cx| Tooltip::text(format!("To reset your API key, unset the {OPENAI_API_KEY_VAR} environment variable."), cx))
})
.on_click(cx.listener(|this, _, cx| this.reset_api_key(cx))), .on_click(cx.listener(|this, _, cx| this.reset_api_key(cx))),
) )
.into_any() .into_any()