Files
zed/crates/language_models/src/api_key_editor.rs
T
Cameron Mcloughlin 1b61228523 Settings UI agent providers (#58876)
Manage various LLM settings (previously in agent panel) in settings UI.

Behind a feature flag, not staff shipped

Release Notes:

- N/A or Added/Fixed/Improved ...
2026-06-16 15:07:25 +00:00

156 lines
5.6 KiB
Rust

use std::rc::Rc;
use anyhow::Result;
use gpui::{App, Context, Entity, Subscription, Task, Window};
use language_model::ApiKeyState;
use ui::{Tooltip, prelude::*};
use ui_input::InputField;
/// The current credential state of a single-API-key provider, as reported by the
/// provider when constructing an [`ApiKeyEditor`].
pub enum ApiKeyStatus {
/// No key is configured; show the input field.
Unset,
/// A key is configured via the UI; show a "configured" row with a reset.
Configured,
/// The key comes from an environment variable and can't be edited here.
FromEnvVar(SharedString),
}
/// Maps a provider's [`ApiKeyState`] to the [`ApiKeyStatus`] the editor renders.
/// Shared so the API-key providers don't each duplicate this mapping.
pub fn api_key_status(state: &ApiKeyState) -> ApiKeyStatus {
if state.is_from_env_var() {
ApiKeyStatus::FromEnvVar(state.env_var_name().clone())
} else if state.has_key() {
ApiKeyStatus::Configured
} else {
ApiKeyStatus::Unset
}
}
/// A compact, reusable control for editing a provider's single API key, intended
/// to be returned from `LanguageModelProvider::configuration_view_v2` as an
/// inline control.
///
/// It is deliberately provider-agnostic: the provider supplies closures that
/// read the current [`ApiKeyStatus`] and store/clear the key against its own
/// state, so all credential knowledge stays in the provider.
pub struct ApiKeyEditor {
input: Entity<InputField>,
api_key_url: SharedString,
status: Rc<dyn Fn(&App) -> ApiKeyStatus>,
set_key: Rc<dyn Fn(String, &mut App) -> Task<Result<()>>>,
reset_key: Rc<dyn Fn(&mut App) -> Task<Result<()>>>,
_subscription: Subscription,
}
impl ApiKeyEditor {
pub fn new<S: 'static>(
state: Entity<S>,
api_key_url: impl Into<SharedString>,
placeholder: &str,
status: impl Fn(&S, &App) -> ApiKeyStatus + 'static,
set_key: impl Fn(&Entity<S>, String, &mut App) -> Task<Result<()>> + 'static,
reset_key: impl Fn(&Entity<S>, &mut App) -> Task<Result<()>> + 'static,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let input = cx.new(|cx| {
InputField::new(window, cx, placeholder)
.masked(true)
.tab_index(0)
});
let subscription = cx.observe(&state, |_, _, cx| cx.notify());
let status_state = state.clone();
let set_state = state.clone();
Self {
input,
api_key_url: api_key_url.into(),
status: Rc::new(move |cx| status(status_state.read(cx), cx)),
set_key: Rc::new(move |key, cx| set_key(&set_state, key, cx)),
reset_key: Rc::new(move |cx| reset_key(&state, cx)),
_subscription: subscription,
}
}
fn save(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
let key = self.input.read(cx).text(cx).trim().to_string();
if key.is_empty() {
return;
}
self.input
.update(cx, |input, cx| input.set_text("", window, cx));
(self.set_key.clone())(key, cx).detach_and_log_err(cx);
}
fn reset(&mut self, cx: &mut Context<Self>) {
(self.reset_key.clone())(cx).detach_and_log_err(cx);
}
fn render_where_to_find_key(&self) -> impl IntoElement {
let url = self.api_key_url.clone();
let click_url = url.to_string();
h_flex()
.id("where-to-find-key")
.gap_0p5()
.cursor_pointer()
.child(
Icon::new(IconName::Info)
.size(IconSize::XSmall)
.color(Color::Muted),
)
.child(
Label::new("Where to find key")
.size(LabelSize::Small)
.color(Color::Muted),
)
.tooltip(Tooltip::text(format!("Create an API key at {url}")))
.on_click(move |_, _window, cx| cx.open_url(&click_url))
}
}
impl Render for ApiKeyEditor {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
match (self.status)(cx) {
ApiKeyStatus::FromEnvVar(env_var_name) => Label::new(format!("Set via {env_var_name}"))
.size(LabelSize::Small)
.color(Color::Muted)
.into_any_element(),
ApiKeyStatus::Configured => h_flex()
.gap_2()
.items_center()
.child(
Icon::new(IconName::Check)
.size(IconSize::Small)
.color(Color::Success),
)
.child(
Label::new("Configured")
.size(LabelSize::Small)
.color(Color::Muted),
)
.child(
Button::new("reset-api-key", "Reset")
.style(ButtonStyle::Outlined)
.label_size(LabelSize::Small)
.tab_index(0isize)
.on_click(cx.listener(|this, _, _window, cx| this.reset(cx))),
)
.into_any_element(),
ApiKeyStatus::Unset => v_flex()
.w_full()
.gap_1()
.child(self.render_where_to_find_key())
.child(
div()
.w_full()
.on_action(cx.listener(Self::save))
.child(self.input.clone()),
)
.into_any_element(),
}
}
}