diff --git a/Cargo.lock b/Cargo.lock index 798ef5d5c8cdcbe2dfc6c81e62b289a99726e086..d24f606b8e9f2ecc584045210782da3b91be123a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5914,6 +5914,7 @@ dependencies = [ "theme_extension", "toml 0.8.23", "ui", + "ui_input", "url", "util", "wasmparser 0.221.3", diff --git a/crates/agent_ui/src/agent_configuration.rs b/crates/agent_ui/src/agent_configuration.rs index cc29d2137dc5f5dd55522f49475f5aff7dbef868..61f036f77ec400941659e8eca56083903a887cb8 100644 --- a/crates/agent_ui/src/agent_configuration.rs +++ b/crates/agent_ui/src/agent_configuration.rs @@ -35,7 +35,7 @@ use project::{ use settings::{Settings, SettingsStore, update_settings_file}; use ui::{ ButtonStyle, Chip, CommonAnimationExt, ContextMenu, ContextMenuEntry, Disclosure, Divider, - DividerColor, ElevationIndex, Indicator, LabelSize, PopoverMenu, Switch, SwitchColor, Tooltip, + DividerColor, ElevationIndex, Indicator, LabelSize, PopoverMenu, Switch, Tooltip, WithScrollbar, prelude::*, }; use util::ResultExt as _; @@ -883,7 +883,6 @@ impl AgentConfiguration { .child(context_server_configuration_menu) .child( Switch::new("context-server-switch", is_running.into()) - .color(SwitchColor::Accent) .on_click({ let context_server_manager = self.context_server_store.clone(); let fs = self.fs.clone(); diff --git a/crates/extension_host/Cargo.toml b/crates/extension_host/Cargo.toml index a0fb499013ac096c741af055d2ecd66f0e28be4a..d664d267b5b21f5d7e22c73dfc76a6d6cc98c88d 100644 --- a/crates/extension_host/Cargo.toml +++ b/crates/extension_host/Cargo.toml @@ -56,6 +56,7 @@ tempfile.workspace = true theme.workspace = true toml.workspace = true ui.workspace = true +ui_input.workspace = true url.workspace = true workspace.workspace = true util.workspace = true diff --git a/crates/extension_host/src/wasm_host/llm_provider.rs b/crates/extension_host/src/wasm_host/llm_provider.rs index 435bbb0909f0ce99e99816b9f1df788999d14aad..964e760cff827d11ab77d0868a61073d6988df01 100644 --- a/crates/extension_host/src/wasm_host/llm_provider.rs +++ b/crates/extension_host/src/wasm_host/llm_provider.rs @@ -12,16 +12,15 @@ use crate::wasm_host::wit::{ }; use anyhow::{Result, anyhow}; use credentials_provider::CredentialsProvider; -use editor::Editor; use extension::{LanguageModelAuthConfig, OAuthConfig}; use futures::future::BoxFuture; use futures::stream::BoxStream; use futures::{FutureExt, StreamExt}; -use gpui::Focusable; + use gpui::{ - AnyView, App, AppContext as _, AsyncApp, ClipboardItem, Context, DismissEvent, Entity, - EventEmitter, FocusHandle, MouseDownEvent, Subscription, Task, TextStyleRefinement, - UnderlineStyle, Window, WindowBounds, WindowOptions, point, px, rems, + AnyView, App, AsyncApp, ClipboardItem, DismissEvent, Entity, EventEmitter, FocusHandle, + Focusable, MouseDownEvent, Subscription, Task, TextStyleRefinement, UnderlineStyle, Window, + WindowBounds, WindowOptions, point, prelude::*, px, }; use language_model::tool_schema::LanguageModelToolSchemaFormat; use language_model::{ @@ -31,14 +30,15 @@ use language_model::{ LanguageModelProviderName, LanguageModelProviderState, LanguageModelRequest, LanguageModelToolChoice, LanguageModelToolUse, LanguageModelToolUseId, StopReason, TokenUsage, }; -use markdown::{Markdown, MarkdownElement, MarkdownStyle}; +use markdown::{HeadingLevelStyles, Markdown, MarkdownElement, MarkdownStyle}; use settings::Settings; use std::sync::Arc; use theme::ThemeSettings; use ui::{ - Button, ButtonLike, ButtonSize, ButtonStyle, ConfiguredApiCard, Headline, HeadlineSize, Icon, - Label, LabelSize, Vector, VectorName, prelude::*, + ButtonLike, ButtonLink, Checkbox, ConfiguredApiCard, SpinnerLabel, ToggleState, Vector, + VectorName, prelude::*, }; +use ui_input::InputField; use util::ResultExt as _; use workspace::Workspace; use workspace::oauth_device_flow_modal::{ @@ -355,7 +355,7 @@ struct ExtensionProviderConfigurationView { auth_config: Option, state: Entity, settings_markdown: Option>, - api_key_editor: Entity, + api_key_editor: Entity, loading_settings: bool, loading_credentials: bool, oauth_in_progress: bool, @@ -378,16 +378,17 @@ impl ExtensionProviderConfigurationView { window: &mut Window, cx: &mut Context, ) -> Self { - // Subscribe to state changes let state_subscription = cx.subscribe(&state, |_, _, _, cx| { cx.notify(); }); - // Create API key editor + let credential_label = auth_config + .as_ref() + .and_then(|c| c.credential_label.clone()) + .unwrap_or_else(|| "API Key".to_string()); + let api_key_editor = cx.new(|cx| { - let mut editor = Editor::single_line(window, cx); - editor.set_placeholder_text("Enter API key...", window, cx); - editor + InputField::new(window, cx, "Enter API key and hit enter").label(credential_label) }); let mut this = Self { @@ -408,12 +409,8 @@ impl ExtensionProviderConfigurationView { _subscriptions: vec![state_subscription], }; - // Load settings text from extension this.load_settings_text(cx); - - // Load existing credentials this.load_credentials(cx); - this } @@ -598,7 +595,7 @@ impl ExtensionProviderConfigurationView { // Clear the editor self.api_key_editor - .update(cx, |editor, cx| editor.set_text("", window, cx)); + .update(cx, |input, cx| input.clear(window, cx)); let credential_key = self.credential_key.clone(); let credentials_provider = ::global(cx); @@ -626,7 +623,7 @@ impl ExtensionProviderConfigurationView { fn reset_api_key(&mut self, window: &mut Window, cx: &mut Context) { // Clear the editor self.api_key_editor - .update(cx, |editor, cx| editor.set_text("", window, cx)); + .update(cx, |input, cx| input.clear(window, cx)); let credential_key = self.credential_key.clone(); let credentials_provider = ::global(cx); @@ -1067,9 +1064,8 @@ impl ExtensionProviderConfigurationView { } } -impl gpui::Render for ExtensionProviderConfigurationView { +impl Render for ExtensionProviderConfigurationView { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - // Use simplified horizontal layout for edit prediction mode if self.is_edit_prediction_mode() { return self .render_for_edit_prediction(window, cx) @@ -1084,96 +1080,70 @@ impl gpui::Render for ExtensionProviderConfigurationView { let has_api_key = self.has_api_key_config(); if is_loading { - return v_flex() + return h_flex() .gap_2() - .child(Label::new("Loading...").color(Color::Muted)) + .child( + h_flex() + .w_2() + .child(SpinnerLabel::sand().size(LabelSize::Small)), + ) + .child(LoadingLabel::new("Loading").size(LabelSize::Small)) .into_any_element(); } - let mut content = v_flex().gap_4().size_full(); + let mut content = v_flex().size_full().gap_2(); - // Render settings markdown if available if let Some(markdown) = &self.settings_markdown { - let style = settings_markdown_style(window, cx); - content = content.child(MarkdownElement::new(markdown.clone(), style)); + content = content.text_sm().child(MarkdownElement::new( + markdown.clone(), + markdown_styles(window, cx), + )); } - // Render env var checkboxes - one for each env var the extension declares if let Some(auth_config) = &self.auth_config { if let Some(env_vars) = &auth_config.env_vars { for env_var_name in env_vars { let is_allowed = allowed_env_vars.contains(env_var_name); let checkbox_label = - format!("Read API key from {} environment variable", env_var_name); + format!("Read API key from {} environment variable.", env_var_name); let env_var_for_click = env_var_name.clone(); content = content.child( - h_flex() - .gap_2() - .child( - ui::Checkbox::new( - SharedString::from(format!("env-var-{}", env_var_name)), - is_allowed.into(), - ) - .on_click(cx.listener( - move |this, _, _window, cx| { - this.toggle_env_var_permission( - env_var_for_click.clone(), - cx, - ); - }, - )), - ) - .child(Label::new(checkbox_label).size(LabelSize::Small)), + Checkbox::new( + SharedString::from(format!("env-var-{}", env_var_name)), + if is_allowed { + ToggleState::Selected + } else { + ToggleState::Unselected + }, + ) + .label(checkbox_label) + .on_click(cx.listener( + move |this, _, _window, cx| { + this.toggle_env_var_permission(env_var_for_click.clone(), cx); + }, + )), ); } - // Show status if any env var is being used if let Some(used_var) = &env_var_name_used { - let tooltip_label = format!( - "To reset this API key, unset the {} environment variable.", - used_var - ); content = content.child( - 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(ui::Icon::new(ui::IconName::Check).color(Color::Success)) - .child( - Label::new(format!( - "API key set in {} environment variable", - used_var - )) - .truncate(), - ), - ) - .child( - ui::Button::new("reset-key", "Reset Key") - .label_size(LabelSize::Small) - .icon(ui::IconName::Undo) - .icon_size(ui::IconSize::Small) - .icon_color(Color::Muted) - .icon_position(ui::IconPosition::Start) - .disabled(true) - .tooltip(ui::Tooltip::text(tooltip_label)), - ), + ConfiguredApiCard::new(format!( + "API key set in {} environment variable", + used_var + )) + .tooltip_label(format!( + "To reset this API key, unset the {} environment variable.", + used_var + )) + .disabled(true), ); + return content.into_any_element(); } } } - // If authenticated, show success state with sign out option if is_authenticated && env_var_name_used.is_none() { let (status_label, button_label) = if has_oauth && !has_api_key { ("Signed in", "Sign Out") @@ -1211,9 +1181,9 @@ impl gpui::Render for ExtensionProviderConfigurationView { let oauth_error = self.oauth_error.clone(); - let mut button = ui::Button::new("oauth-sign-in", button_label) + let mut button = Button::new("oauth-sign-in", button_label) .full_width() - .style(ui::ButtonStyle::Outlined) + .style(ButtonStyle::Outlined) .disabled(oauth_in_progress) .on_click(cx.listener(|this, _, window, cx| { this.start_oauth_sign_in(window, cx); @@ -1221,8 +1191,8 @@ impl gpui::Render for ExtensionProviderConfigurationView { if let Some(icon) = button_icon { button = button .icon(icon) - .icon_position(ui::IconPosition::Start) - .icon_size(ui::IconSize::Small) + .icon_position(IconPosition::Start) + .icon_size(IconSize::Small) .icon_color(Color::Muted); } @@ -1245,9 +1215,9 @@ impl gpui::Render for ExtensionProviderConfigurationView { h_flex() .gap_2() .child( - ui::Icon::new(ui::IconName::Warning) + Icon::new(IconName::Warning) .color(Color::Error) - .size(ui::IconSize::Small), + .size(IconSize::Small), ) .child( Label::new("Authentication failed") @@ -1274,45 +1244,27 @@ impl gpui::Render for ExtensionProviderConfigurationView { h_flex() .gap_2() .items_center() - .child(div().h_px().flex_1().bg(cx.theme().colors().border)) + .child(div().h_px().flex_1().bg(cx.theme().colors().border_variant)) .child(Label::new("or").size(LabelSize::Small).color(Color::Muted)) - .child(div().h_px().flex_1().bg(cx.theme().colors().border)), + .child(div().h_px().flex_1().bg(cx.theme().colors().border_variant)), ); } - let credential_label = self - .auth_config - .as_ref() - .and_then(|c| c.credential_label.clone()) - .unwrap_or_else(|| "API Key".to_string()); - content = content.child( - v_flex() - .gap_2() + div() .on_action(cx.listener(Self::save_api_key)) - .child( - Label::new(credential_label) - .size(LabelSize::Small) - .color(Color::Muted), - ) - .child(self.api_key_editor.clone()) - .child( - Label::new("Enter your API key and press Enter to save") - .size(LabelSize::Small) - .color(Color::Muted), - ), + .child(self.api_key_editor.clone()), ); } } - // Show OpenAI-compatible models notification for OpenAI extension if self.extension_provider_id == "openai" { content = content.child( h_flex() .gap_1() .child( - ui::Icon::new(ui::IconName::Info) - .size(ui::IconSize::Small) + Icon::new(IconName::Info) + .size(IconSize::XSmall) .color(Color::Muted), ) .child( @@ -1321,16 +1273,11 @@ impl gpui::Render for ExtensionProviderConfigurationView { .color(Color::Muted), ) .child( - ui::Button::new("learn-more", "Learn More") - .style(ui::ButtonStyle::Subtle) - .label_size(LabelSize::Small) - .icon(ui::IconName::ArrowUpRight) - .icon_size(ui::IconSize::Small) - .icon_color(Color::Muted) - .icon_position(ui::IconPosition::End) - .on_click(|_, _, cx| { - cx.open_url("https://zed.dev/docs/configuring-llm-providers#openai-compatible-providers"); - }), + ButtonLink::new( + "Learn More", + "https://zed.dev/docs/configuring-llm-providers#openai-compatible-providers", + ) + .label_size(LabelSize::Small), ), ); } @@ -1340,8 +1287,8 @@ impl gpui::Render for ExtensionProviderConfigurationView { } impl Focusable for ExtensionProviderConfigurationView { - fn focus_handle(&self, cx: &App) -> gpui::FocusHandle { - self.api_key_editor.focus_handle(cx) + fn focus_handle(&self, cx: &App) -> FocusHandle { + self.api_key_editor.read(cx).focus_handle(cx) } } @@ -1414,7 +1361,7 @@ impl OAuthCodeVerificationWindow { .items_center() .child( Icon::from_external_svg(icon_path.clone()) - .size(ui::IconSize::Custom(icon_size)) + .size(IconSize::Custom(icon_size)) .color(icon_color), ) .child( @@ -1534,7 +1481,7 @@ impl OAuthCodeVerificationWindow { } } -impl gpui::Render for OAuthCodeVerificationWindow { +impl Render for OAuthCodeVerificationWindow { fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { let prompt = match &self.status { OAuthDeviceFlowStatus::Prompting | OAuthDeviceFlowStatus::WaitingForAuthorization => { @@ -1569,26 +1516,61 @@ impl gpui::Render for OAuthCodeVerificationWindow { } } -fn settings_markdown_style(window: &Window, cx: &App) -> MarkdownStyle { - let theme_settings = ThemeSettings::get_global(cx); +fn markdown_styles(window: &Window, cx: &App) -> MarkdownStyle { + let settings = ThemeSettings::get_global(cx); let colors = cx.theme().colors(); + let mut text_style = window.text_style(); text_style.refine(&TextStyleRefinement { - font_family: Some(theme_settings.ui_font.family.clone()), - font_fallbacks: theme_settings.ui_font.fallbacks.clone(), - font_features: Some(theme_settings.ui_font.features.clone()), - color: Some(colors.text), + font_family: Some(settings.ui_font.family.clone()), + font_fallbacks: settings.ui_font.fallbacks.clone(), + font_features: Some(settings.ui_font.features.clone()), + font_size: Some(settings.ui_font_size(cx).into()), + line_height: Some(relative(1.5)), + color: Some(colors.text_muted), ..Default::default() }); MarkdownStyle { - base_text_style: text_style, + base_text_style: text_style.clone(), + syntax: cx.theme().syntax().clone(), selection_background_color: colors.element_selection_background, + heading_level_styles: Some(HeadingLevelStyles { + h1: Some(TextStyleRefinement { + font_size: Some(rems(1.15).into()), + ..Default::default() + }), + h2: Some(TextStyleRefinement { + font_size: Some(rems(1.1).into()), + ..Default::default() + }), + h3: Some(TextStyleRefinement { + font_size: Some(rems(1.05).into()), + ..Default::default() + }), + h4: Some(TextStyleRefinement { + font_size: Some(rems(1.).into()), + ..Default::default() + }), + h5: Some(TextStyleRefinement { + font_size: Some(rems(0.95).into()), + ..Default::default() + }), + h6: Some(TextStyleRefinement { + font_size: Some(rems(0.875).into()), + ..Default::default() + }), + }), inline_code: TextStyleRefinement { - background_color: Some(colors.editor_background), + font_family: Some(settings.buffer_font.family.clone()), + font_fallbacks: settings.buffer_font.fallbacks.clone(), + font_features: Some(settings.buffer_font.features.clone()), + font_size: Some(settings.buffer_font_size(cx).into()), + background_color: Some(colors.editor_foreground.opacity(0.08)), ..Default::default() }, link: TextStyleRefinement { + background_color: Some(colors.editor_foreground.opacity(0.025)), color: Some(colors.text_accent), underline: Some(UnderlineStyle { color: Some(colors.text_accent.opacity(0.5)), @@ -1597,7 +1579,6 @@ fn settings_markdown_style(window: &Window, cx: &App) -> MarkdownStyle { }), ..Default::default() }, - syntax: cx.theme().syntax().clone(), ..Default::default() } } diff --git a/crates/ui/src/components/toggle.rs b/crates/ui/src/components/toggle.rs index 86ff1d8eff8691a2610a4a7e2268aaf47502e306..f22ef743743351a12fd9c5e49c2d0c5ce241f380 100644 --- a/crates/ui/src/components/toggle.rs +++ b/crates/ui/src/components/toggle.rs @@ -344,6 +344,7 @@ pub struct Switch { label: Option, label_position: Option, label_size: LabelSize, + label_color: Color, full_width: bool, key_binding: Option, color: SwitchColor, @@ -361,6 +362,7 @@ impl Switch { label: None, label_position: None, label_size: LabelSize::Small, + label_color: Color::Default, full_width: false, key_binding: None, color: SwitchColor::default(), @@ -408,6 +410,11 @@ impl Switch { self } + pub fn label_color(mut self, color: Color) -> Self { + self.label_color = color; + self + } + pub fn full_width(mut self, full_width: bool) -> Self { self.full_width = full_width; self @@ -507,7 +514,11 @@ impl RenderOnce for Switch { self.label_position == Some(SwitchLabelPosition::Start), |this| { this.when_some(label.clone(), |this, label| { - this.child(Label::new(label).size(self.label_size)) + this.child( + Label::new(label) + .color(self.label_color) + .size(self.label_size), + ) }) }, ) @@ -516,7 +527,11 @@ impl RenderOnce for Switch { self.label_position == Some(SwitchLabelPosition::End), |this| { this.when_some(label, |this, label| { - this.child(Label::new(label).size(self.label_size)) + this.child( + Label::new(label) + .color(self.label_color) + .size(self.label_size), + ) }) }, )