Detailed changes
@@ -260,11 +260,15 @@ impl AgentConfiguration {
h_flex()
.w_full()
.gap_1p5()
- .child(
+ .child(if let Some(icon_path) = provider.icon_path() {
+ Icon::from_external_svg(icon_path)
+ .size(IconSize::Small)
+ .color(Color::Muted)
+ } else {
Icon::new(provider.icon())
.size(IconSize::Small)
- .color(Color::Muted),
- )
+ .color(Color::Muted)
+ })
.child(
h_flex()
.w_full()
@@ -73,7 +73,8 @@ impl Render for AgentModelSelector {
.map(|model| model.model.name().0)
.unwrap_or_else(|| SharedString::from("Select a Model"));
- let provider_icon = model.as_ref().map(|model| model.provider.icon());
+ let provider_icon_path = model.as_ref().and_then(|model| model.provider.icon_path());
+ let provider_icon_name = model.as_ref().map(|model| model.provider.icon());
let color = if self.menu_handle.is_deployed() {
Color::Accent
} else {
@@ -85,8 +86,17 @@ impl Render for AgentModelSelector {
PickerPopoverMenu::new(
self.selector.clone(),
ButtonLike::new("active-model")
- .when_some(provider_icon, |this, icon| {
- this.child(Icon::new(icon).color(color).size(IconSize::XSmall))
+ .when_some(provider_icon_path.clone(), |this, icon_path| {
+ this.child(
+ Icon::from_external_svg(icon_path)
+ .color(color)
+ .size(IconSize::XSmall),
+ )
+ })
+ .when(provider_icon_path.is_none(), |this| {
+ this.when_some(provider_icon_name, |this, icon| {
+ this.child(Icon::new(icon).color(color).size(IconSize::XSmall))
+ })
})
.selected_style(ButtonStyle::Tinted(TintColor::Accent))
.child(
@@ -5,8 +5,8 @@ use futures::{StreamExt, channel::mpsc};
use fuzzy::{StringMatch, StringMatchCandidate, match_strings};
use gpui::{Action, AnyElement, App, BackgroundExecutor, DismissEvent, FocusHandle, Task};
use language_model::{
- AuthenticateError, ConfiguredModel, LanguageModel, LanguageModelProviderId,
- LanguageModelRegistry,
+ AuthenticateError, ConfiguredModel, LanguageModel, LanguageModelProvider,
+ LanguageModelProviderId, LanguageModelRegistry,
};
use ordered_float::OrderedFloat;
use picker::{Picker, PickerDelegate};
@@ -56,7 +56,7 @@ fn all_models(cx: &App) -> GroupedModels {
.into_iter()
.map(|model| ModelInfo {
model,
- icon: provider.icon(),
+ icon: ProviderIcon::from_provider(provider.as_ref()),
})
})
.collect();
@@ -69,7 +69,7 @@ fn all_models(cx: &App) -> GroupedModels {
.into_iter()
.map(|model| ModelInfo {
model,
- icon: provider.icon(),
+ icon: ProviderIcon::from_provider(provider.as_ref()),
})
})
.collect();
@@ -77,10 +77,26 @@ fn all_models(cx: &App) -> GroupedModels {
GroupedModels::new(all, recommended)
}
+#[derive(Clone)]
+enum ProviderIcon {
+ Name(IconName),
+ Path(SharedString),
+}
+
+impl ProviderIcon {
+ fn from_provider(provider: &dyn LanguageModelProvider) -> Self {
+ if let Some(path) = provider.icon_path() {
+ Self::Path(path)
+ } else {
+ Self::Name(provider.icon())
+ }
+ }
+}
+
#[derive(Clone)]
struct ModelInfo {
model: Arc<dyn LanguageModel>,
- icon: IconName,
+ icon: ProviderIcon,
}
pub struct LanguageModelPickerDelegate {
@@ -519,11 +535,16 @@ impl PickerDelegate for LanguageModelPickerDelegate {
h_flex()
.w_full()
.gap_1p5()
- .child(
- Icon::new(model_info.icon)
+ .child(match &model_info.icon {
+ ProviderIcon::Name(icon_name) => Icon::new(*icon_name)
.color(model_icon_color)
.size(IconSize::Small),
- )
+ ProviderIcon::Path(icon_path) => {
+ Icon::from_external_svg(icon_path.clone())
+ .color(model_icon_color)
+ .size(IconSize::Small)
+ }
+ })
.child(Label::new(model_info.model.name().0).truncate()),
)
.end_slot(div().pr_3().when(is_selected, |this| {
@@ -672,7 +693,7 @@ mod tests {
.into_iter()
.map(|(provider, name)| ModelInfo {
model: Arc::new(TestLanguageModel::new(name, provider)),
- icon: IconName::Ai,
+ icon: ProviderIcon::Name(IconName::Ai),
})
.collect()
}
@@ -2097,7 +2097,8 @@ impl TextThreadEditor {
.default_model()
.map(|default| default.provider);
- let provider_icon = match active_provider {
+ let provider_icon_path = active_provider.as_ref().and_then(|p| p.icon_path());
+ let provider_icon_name = match &active_provider {
Some(provider) => provider.icon(),
None => IconName::Ai,
};
@@ -2109,6 +2110,16 @@ impl TextThreadEditor {
(Color::Muted, IconName::ChevronDown)
};
+ let provider_icon_element = if let Some(icon_path) = provider_icon_path {
+ Icon::from_external_svg(icon_path)
+ .color(color)
+ .size(IconSize::XSmall)
+ } else {
+ Icon::new(provider_icon_name)
+ .color(color)
+ .size(IconSize::XSmall)
+ };
+
PickerPopoverMenu::new(
self.language_model_selector.clone(),
ButtonLike::new("active-model")
@@ -2116,7 +2127,7 @@ impl TextThreadEditor {
.child(
h_flex()
.gap_0p5()
- .child(Icon::new(provider_icon).color(color).size(IconSize::XSmall))
+ .child(provider_icon_element)
.child(
Label::new(model_name)
.color(color)
@@ -1,9 +1,25 @@
use gpui::{Action, IntoElement, ParentElement, RenderOnce, point};
-use language_model::{LanguageModelRegistry, ZED_CLOUD_PROVIDER_ID};
+use language_model::{LanguageModelProvider, LanguageModelRegistry, ZED_CLOUD_PROVIDER_ID};
use ui::{Divider, List, ListBulletItem, prelude::*};
+#[derive(Clone)]
+enum ProviderIcon {
+ Name(IconName),
+ Path(SharedString),
+}
+
+impl ProviderIcon {
+ fn from_provider(provider: &dyn LanguageModelProvider) -> Self {
+ if let Some(path) = provider.icon_path() {
+ Self::Path(path)
+ } else {
+ Self::Name(provider.icon())
+ }
+ }
+}
+
pub struct ApiKeysWithProviders {
- configured_providers: Vec<(IconName, SharedString)>,
+ configured_providers: Vec<(ProviderIcon, SharedString)>,
}
impl ApiKeysWithProviders {
@@ -26,14 +42,19 @@ impl ApiKeysWithProviders {
}
}
- fn compute_configured_providers(cx: &App) -> Vec<(IconName, SharedString)> {
+ fn compute_configured_providers(cx: &App) -> Vec<(ProviderIcon, SharedString)> {
LanguageModelRegistry::read_global(cx)
.providers()
.iter()
.filter(|provider| {
provider.is_authenticated(cx) && provider.id() != ZED_CLOUD_PROVIDER_ID
})
- .map(|provider| (provider.icon(), provider.name().0))
+ .map(|provider| {
+ (
+ ProviderIcon::from_provider(provider.as_ref()),
+ provider.name().0,
+ )
+ })
.collect()
}
}
@@ -47,7 +68,14 @@ impl Render for ApiKeysWithProviders {
.map(|(icon, name)| {
h_flex()
.gap_1p5()
- .child(Icon::new(icon).size(IconSize::XSmall).color(Color::Muted))
+ .child(match icon {
+ ProviderIcon::Name(icon_name) => Icon::new(icon_name)
+ .size(IconSize::XSmall)
+ .color(Color::Muted),
+ ProviderIcon::Path(icon_path) => Icon::from_external_svg(icon_path)
+ .size(IconSize::XSmall)
+ .color(Color::Muted),
+ })
.child(Label::new(name))
});
div()
@@ -11,7 +11,7 @@ use crate::{AgentPanelOnboardingCard, ApiKeysWithoutProviders, ZedAiOnboarding};
pub struct AgentPanelOnboarding {
user_store: Entity<UserStore>,
client: Arc<Client>,
- configured_providers: Vec<(IconName, SharedString)>,
+ has_configured_providers: bool,
continue_with_zed_ai: Arc<dyn Fn(&mut Window, &mut App)>,
}
@@ -28,7 +28,7 @@ impl AgentPanelOnboarding {
language_model::Event::ProviderStateChanged(_)
| language_model::Event::AddedProvider(_)
| language_model::Event::RemovedProvider(_) => {
- this.configured_providers = Self::compute_available_providers(cx)
+ this.has_configured_providers = Self::has_configured_providers(cx)
}
_ => {}
},
@@ -38,20 +38,16 @@ impl AgentPanelOnboarding {
Self {
user_store,
client,
- configured_providers: Self::compute_available_providers(cx),
+ has_configured_providers: Self::has_configured_providers(cx),
continue_with_zed_ai: Arc::new(continue_with_zed_ai),
}
}
- fn compute_available_providers(cx: &App) -> Vec<(IconName, SharedString)> {
+ fn has_configured_providers(cx: &App) -> bool {
LanguageModelRegistry::read_global(cx)
.providers()
.iter()
- .filter(|provider| {
- provider.is_authenticated(cx) && provider.id() != ZED_CLOUD_PROVIDER_ID
- })
- .map(|provider| (provider.icon(), provider.name().0))
- .collect()
+ .any(|provider| provider.is_authenticated(cx) && provider.id() != ZED_CLOUD_PROVIDER_ID)
}
}
@@ -81,7 +77,7 @@ impl Render for AgentPanelOnboarding {
}),
)
.map(|this| {
- if enrolled_in_trial || is_pro_user || !self.configured_providers.is_empty() {
+ if enrolled_in_trial || is_pro_user || self.has_configured_providers {
this
} else {
this.child(ApiKeysWithoutProviders::new())
@@ -295,7 +295,7 @@ pub struct DebugLocatorManifestEntry {}
pub struct LanguageModelProviderManifestEntry {
/// Display name for the provider.
pub name: String,
- /// Icon name from Zed's icon set (optional).
+ /// Path to an SVG icon file relative to the extension root (e.g., "icons/provider.svg").
#[serde(default)]
pub icon: Option<String>,
/// Default models to show even before API connection.
@@ -5,7 +5,7 @@ interface llm-provider {
id: string,
/// Display name for the provider.
name: string,
- /// Icon name from Zed's icon set (optional).
+ /// Path to an SVG icon file relative to the extension root (e.g., "icons/provider.svg").
icon: option<string>,
}
@@ -5,7 +5,7 @@ interface llm-provider {
id: string,
/// Display name for the provider.
name: string,
- /// Icon name from Zed's icon set (optional).
+ /// Path to an SVG icon file relative to the extension root (e.g., "icons/provider.svg").
icon: option<string>,
}
@@ -254,6 +254,21 @@ async fn copy_extension_resources(
}
}
+ for (_, provider_entry) in &manifest.language_model_providers {
+ if let Some(icon_path) = &provider_entry.icon {
+ let source_icon = extension_path.join(icon_path);
+ let dest_icon = output_dir.join(icon_path);
+
+ // Create parent directory if needed
+ if let Some(parent) = dest_icon.parent() {
+ fs::create_dir_all(parent)?;
+ }
+
+ fs::copy(&source_icon, &dest_icon)
+ .with_context(|| format!("failed to copy LLM provider icon '{}'", icon_path))?;
+ }
+ }
+
if !manifest.languages.is_empty() {
let output_languages_dir = output_dir.join("languages");
fs::create_dir_all(&output_languages_dir)?;
@@ -32,8 +32,8 @@ use futures::{
select_biased,
};
use gpui::{
- App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, Global, Task, WeakEntity,
- actions,
+ App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, Global, SharedString, Task,
+ WeakEntity, actions,
};
use http_client::{AsyncBody, HttpClient, HttpClientWithUrl};
use language::{
@@ -67,6 +67,7 @@ struct LlmProviderWithModels {
provider_info: LlmProviderInfo,
models: Vec<LlmModelInfo>,
is_authenticated: bool,
+ icon_path: Option<SharedString>,
}
pub use extension::{
@@ -1463,10 +1464,23 @@ impl ExtensionStore {
.unwrap_or(Ok(false))
.unwrap_or(false);
+ // Resolve icon path if provided
+ let icon_path = provider_info.icon.as_ref().map(|icon| {
+ let icon_file_path = extension_path.join(icon);
+ // Canonicalize to resolve symlinks (dev extensions are symlinked)
+ let absolute_icon_path = icon_file_path
+ .canonicalize()
+ .unwrap_or(icon_file_path)
+ .to_string_lossy()
+ .to_string();
+ SharedString::from(absolute_icon_path)
+ });
+
llm_providers_with_models.push(LlmProviderWithModels {
provider_info,
models,
is_authenticated,
+ icon_path,
});
}
} else {
@@ -1564,12 +1578,13 @@ impl ExtensionStore {
let pinfo = llm_provider.provider_info.clone();
let mods = llm_provider.models.clone();
let auth = llm_provider.is_authenticated;
+ let icon = llm_provider.icon_path.clone();
this.proxy.register_language_model_provider(
provider_id.clone(),
Box::new(move |cx: &mut App| {
let provider = Arc::new(ExtensionLanguageModelProvider::new(
- wasm_ext, pinfo, mods, auth, cx,
+ wasm_ext, pinfo, mods, auth, icon, cx,
));
language_model::LanguageModelRegistry::global(cx).update(
cx,
@@ -36,6 +36,7 @@ use util::ResultExt as _;
pub struct ExtensionLanguageModelProvider {
pub extension: WasmExtension,
pub provider_info: LlmProviderInfo,
+ icon_path: Option<SharedString>,
state: Entity<ExtensionLlmProviderState>,
}
@@ -52,6 +53,7 @@ impl ExtensionLanguageModelProvider {
provider_info: LlmProviderInfo,
models: Vec<LlmModelInfo>,
is_authenticated: bool,
+ icon_path: Option<SharedString>,
cx: &mut App,
) -> Self {
let state = cx.new(|_| ExtensionLlmProviderState {
@@ -62,6 +64,7 @@ impl ExtensionLanguageModelProvider {
Self {
extension,
provider_info,
+ icon_path,
state,
}
}
@@ -89,6 +92,10 @@ impl LanguageModelProvider for ExtensionLanguageModelProvider {
ui::IconName::ZedAssistant
}
+ fn icon_path(&self) -> Option<SharedString> {
+ self.icon_path.clone()
+ }
+
fn default_model(&self, cx: &App) -> Option<Arc<dyn LanguageModel>> {
let state = self.state.read(cx);
state
@@ -746,6 +746,11 @@ pub trait LanguageModelProvider: 'static {
fn icon(&self) -> IconName {
IconName::ZedAssistant
}
+ /// Returns the path to an external SVG icon for this provider, if any.
+ /// When present, this takes precedence over `icon()`.
+ fn icon_path(&self) -> Option<SharedString> {
+ None
+ }
fn default_model(&self, cx: &App) -> Option<Arc<dyn LanguageModel>>;
fn default_fast_model(&self, cx: &App) -> Option<Arc<dyn LanguageModel>>;
fn provided_models(&self, cx: &App) -> Vec<Arc<dyn LanguageModel>>;
@@ -490,7 +490,7 @@ impl zed::Extension for AnthropicProvider {
vec![LlmProviderInfo {
id: "anthropic".into(),
name: "Anthropic".into(),
- icon: Some("anthropic".into()),
+ icon: None,
}]
}
@@ -567,7 +567,7 @@ impl zed::Extension for GoogleAiProvider {
vec![LlmProviderInfo {
id: "google-ai".into(),
name: "Google AI".into(),
- icon: Some("google-ai".into()),
+ icon: None,
}]
}
@@ -445,7 +445,7 @@ impl zed::Extension for OpenAiProvider {
vec![LlmProviderInfo {
id: "openai".into(),
name: "OpenAI".into(),
- icon: Some("openai".into()),
+ icon: None,
}]
}