Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

19 changes: 18 additions & 1 deletion crates/agent_ui/src/agent_panel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,9 @@ use cloud_api_types::Plan;
use collections::HashMap;
use editor::{Editor, MultiBuffer};
use extension_host::ExtensionStore;
use feature_flags::{CreateThreadToolFeatureFlag, FeatureFlagAppExt as _};
use feature_flags::{
AgentSettingsUiFeatureFlag, CreateThreadToolFeatureFlag, FeatureFlagAppExt as _,
};

use fs::Fs;
use gpui::{
Expand Down Expand Up @@ -3569,6 +3571,21 @@ impl AgentPanel {
}

pub(crate) fn open_configuration(&mut self, window: &mut Window, cx: &mut Context<Self>) {
// When the agent settings have been moved into the settings UI, the
// panel no longer shows its own configuration overlay. Instead, route to
// the settings UI at the LLM providers page (where the model selector's
// "Configure" button expects to land).
if cx.has_flag::<AgentSettingsUiFeatureFlag>() {
window.dispatch_action(
Box::new(zed_actions::OpenSettingsAt {
path: "llm_providers".to_string(),
target: None,
}),
cx,
);
return;
}

if matches!(self.overlay_view, Some(OverlayView::Configuration)) {
self.clear_overlay(true, window, cx);
return;
Expand Down
12 changes: 12 additions & 0 deletions crates/feature_flags/src/flags.rs
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,18 @@ impl FeatureFlag for AgentThreadWorktreeLabelFlag {
}
register_feature_flag!(AgentThreadWorktreeLabelFlag);

/// Moves LLM provider and MCP server configuration out of the dedicated agent
/// panel page and into the settings UI. When enabled, the agent panel no longer
/// shows its configuration overlay and the settings UI exposes the "LLM
/// Providers" and "MCP Servers" sub-pages instead.
pub struct AgentSettingsUiFeatureFlag;

impl FeatureFlag for AgentSettingsUiFeatureFlag {
const NAME: &'static str = "agent-settings-ui";
type Value = PresenceFlag;
}
register_feature_flag!(AgentSettingsUiFeatureFlag);

pub struct AutoWatchFeatureFlag;

impl FeatureFlag for AutoWatchFeatureFlag {
Expand Down
27 changes: 27 additions & 0 deletions crates/language_model/src/language_model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,23 @@ pub trait LanguageModelProvider: 'static {
.into()
}

/// Returns the provider's configuration UI together with how it prefers to
/// be presented: [`ProviderConfigurationView::Inline`] for a compact control
/// that can sit in a list row (e.g. a single API-key field), or
/// [`ProviderConfigurationView::SubPage`] for a richer view that needs its
/// own surface.
///
/// The default reuses [`Self::configuration_view`] as a sub-page, so
/// providers only override this when they have a compact inline form.
fn configuration_view_v2(
&self,
target_agent: ConfigurationViewTargetAgent,
window: &mut Window,
cx: &mut App,
) -> ProviderConfigurationView {
ProviderConfigurationView::SubPage(self.configuration_view(target_agent, window, cx))
}

/// Copy shown the first time a user enables fast mode for a model from
/// this provider. Returning `None` skips the confirmation prompt and lets
/// the toggle apply silently.
Expand All @@ -341,6 +358,16 @@ pub trait LanguageModelProvider: 'static {
}
}

/// How a provider's configuration UI prefers to be presented by the settings UI.
#[derive(Clone)]
pub enum ProviderConfigurationView {
/// A compact control suitable for rendering inline in a list row, such as a
/// single API-key field.
Inline(AnyView),
/// A richer view that should be shown on its own dedicated sub-page.
SubPage(AnyView),
}

/// Provider-specific copy shown the first time a user enables fast mode.
#[derive(Debug, Clone)]
pub struct FastModeConfirmation {
Expand Down
155 changes: 155 additions & 0 deletions crates/language_models/src/api_key_editor.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
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(),
}
}
}
2 changes: 2 additions & 0 deletions crates/language_models/src/language_models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@ use language_model::{
};
use provider::deepseek::DeepSeekLanguageModelProvider;

mod api_key_editor;
pub mod extension;
pub mod provider;
mod settings;

pub use crate::api_key_editor::{ApiKeyEditor, ApiKeyStatus, api_key_status};
pub use crate::extension::init_proxy as init_extension_proxy;

use crate::provider::anthropic::AnthropicLanguageModelProvider;
Expand Down
28 changes: 26 additions & 2 deletions crates/language_models/src/provider/anthropic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ use language_model::{
ConfigurationViewTargetAgent, EnvVar, FastModeConfirmation, IconOrSvg, LanguageModel,
LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelId, LanguageModelName,
LanguageModelProvider, LanguageModelProviderId, LanguageModelProviderName,
LanguageModelProviderState, LanguageModelRequest, LanguageModelToolChoice, RateLimiter,
env_var,
LanguageModelProviderState, LanguageModelRequest, LanguageModelToolChoice,
ProviderConfigurationView, RateLimiter, env_var,
};
use settings::{Settings, SettingsStore};
use std::sync::{Arc, LazyLock};
Expand Down Expand Up @@ -290,6 +290,30 @@ impl LanguageModelProvider for AnthropicLanguageModelProvider {
.update(cx, |state, cx| state.set_api_key(None, cx))
}

fn configuration_view_v2(
&self,
_target_agent: language_model::ConfigurationViewTargetAgent,
window: &mut Window,
cx: &mut App,
) -> ProviderConfigurationView {
let state = self.state.clone();
ProviderConfigurationView::Inline(
cx.new(|cx| {
crate::ApiKeyEditor::new(
state,
"https://console.anthropic.com/settings/keys",
"sk-ant-...",
|state, _cx| crate::api_key_status(&state.api_key_state),
|state, key, cx| state.update(cx, |state, cx| state.set_api_key(Some(key), cx)),
|state, cx| state.update(cx, |state, cx| state.set_api_key(None, cx)),
window,
cx,
)
})
.into(),
)
}

fn fast_mode_confirmation(&self, _cx: &App) -> Option<FastModeConfirmation> {
Some(FastModeConfirmation {
title: "Enable Fast Mode for Anthropic?".into(),
Expand Down
14 changes: 13 additions & 1 deletion crates/language_models/src/provider/cloud.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use gpui::{AnyElement, AnyView, App, AppContext, Context, Entity, Subscription,
use language_model::{
AuthenticateError, FastModeConfirmation, IconOrSvg, LanguageModel, LanguageModelProvider,
LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState,
ZED_CLOUD_PROVIDER_ID, ZED_CLOUD_PROVIDER_NAME,
ProviderConfigurationView, ZED_CLOUD_PROVIDER_ID, ZED_CLOUD_PROVIDER_NAME,
};
use language_models_cloud::{CloudLlmTokenProvider, CloudModelProvider};
use rand::{Rng as _, SeedableRng as _, rngs::StdRng};
Expand Down Expand Up @@ -369,6 +369,18 @@ impl LanguageModelProvider for CloudLanguageModelProvider {
.into()
}

fn configuration_view_v2(
&self,
target_agent: language_model::ConfigurationViewTargetAgent,
window: &mut Window,
cx: &mut App,
) -> ProviderConfigurationView {
// The Zed sign-in/plan control is small enough that sending users to a
// dedicated sub-page just to reach it would be annoying, so render it
// inline even though it isn't an API-key field.
ProviderConfigurationView::Inline(self.configuration_view(target_agent, window, cx))
}

fn reset_credentials(&self, _cx: &mut App) -> Task<Result<()>> {
Task::ready(Ok(()))
}
Expand Down
14 changes: 13 additions & 1 deletion crates/language_models/src/provider/copilot_chat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ use language_model::{
LanguageModelName, LanguageModelProvider, LanguageModelProviderId, LanguageModelProviderName,
LanguageModelProviderState, LanguageModelRequest, LanguageModelRequestMessage,
LanguageModelToolChoice, LanguageModelToolResultContent, LanguageModelToolSchemaFormat,
LanguageModelToolUse, MessageContent, RateLimiter, Role, StopReason, TokenUsage,
LanguageModelToolUse, MessageContent, ProviderConfigurationView, RateLimiter, Role, StopReason,
TokenUsage,
};
use settings::SettingsStore;
use ui::prelude::*;
Expand Down Expand Up @@ -196,6 +197,17 @@ impl LanguageModelProvider for CopilotChatLanguageModelProvider {
.into()
}

fn configuration_view_v2(
&self,
target_agent: language_model::ConfigurationViewTargetAgent,
window: &mut Window,
cx: &mut App,
) -> ProviderConfigurationView {
// GitHub Copilot's control is just a sign-in button, so render it inline
// rather than behind a sub-page.
ProviderConfigurationView::Inline(self.configuration_view(target_agent, window, cx))
}

fn reset_credentials(&self, _cx: &mut App) -> Task<Result<()>> {
Task::ready(Err(anyhow!(
"Signing out of GitHub Copilot Chat is currently not supported."
Expand Down
Loading
Loading