From 2b39fba540c0309fc3d4f604dd60fb39c2a3a987 Mon Sep 17 00:00:00 2001 From: Xiaobo Liu Date: Sat, 14 Mar 2026 06:37:18 +0800 Subject: [PATCH] agent_ui: Mask API key input in Add LLM provider modal (#50379) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Release Notes: - Added Mask API key input in Add LLM provider modal 截屏2026-02-28 17 35 22 --------- Signed-off-by: Xiaobo Liu Co-authored-by: Danilo Leal --- assets/icons/eye_off.svg | 6 +++ .../add_llm_provider_modal.rs | 19 ++++---- crates/icons/src/icons.rs | 1 + crates/ui_input/src/input_field.rs | 44 ++++++++++++++++++- 4 files changed, 61 insertions(+), 9 deletions(-) create mode 100644 assets/icons/eye_off.svg diff --git a/assets/icons/eye_off.svg b/assets/icons/eye_off.svg new file mode 100644 index 0000000000000000000000000000000000000000..3057c3050c36c72be314f9b0646d44932c52e4ee --- /dev/null +++ b/assets/icons/eye_off.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/crates/agent_ui/src/agent_configuration/add_llm_provider_modal.rs b/crates/agent_ui/src/agent_configuration/add_llm_provider_modal.rs index 3d18d734af4890ef06a67dccec0c0e884a219a79..334aaf4026527938144cf12e25c9a7a23d5c28ac 100644 --- a/crates/agent_ui/src/agent_configuration/add_llm_provider_modal.rs +++ b/crates/agent_ui/src/agent_configuration/add_llm_provider_modal.rs @@ -68,14 +68,17 @@ impl AddLlmProviderInput { let provider_name = single_line_input("Provider Name", provider.name(), None, 1, window, cx); let api_url = single_line_input("API URL", provider.api_url(), None, 2, window, cx); - let api_key = single_line_input( - "API Key", - "000000000000000000000000000000000000000000000000", - None, - 3, - window, - cx, - ); + let api_key = cx.new(|cx| { + InputField::new( + window, + cx, + "000000000000000000000000000000000000000000000000", + ) + .label("API Key") + .tab_index(3) + .tab_stop(true) + .masked(true) + }); Self { provider_name, diff --git a/crates/icons/src/icons.rs b/crates/icons/src/icons.rs index 17db6371114e1623280c22a23dd44e8efc6fa594..70bc0fc52784c4e50c715ddafab533beeccf3f93 100644 --- a/crates/icons/src/icons.rs +++ b/crates/icons/src/icons.rs @@ -114,6 +114,7 @@ pub enum IconName { ExpandUp, ExpandVertical, Eye, + EyeOff, FastForward, FastForwardOff, File, diff --git a/crates/ui_input/src/input_field.rs b/crates/ui_input/src/input_field.rs index 59a05497627838364b4037c44b236ab70c2b3c6b..16932b58e87cb9df83c14919b79bd048f33275fe 100644 --- a/crates/ui_input/src/input_field.rs +++ b/crates/ui_input/src/input_field.rs @@ -3,6 +3,7 @@ use component::{example_group, single_example}; use gpui::{App, FocusHandle, Focusable, Hsla, Length}; use std::sync::Arc; +use ui::Tooltip; use ui::prelude::*; use crate::ErasedEditor; @@ -38,6 +39,8 @@ pub struct InputField { tab_index: Option, /// Whether this field is a tab stop (can be focused via Tab key). tab_stop: bool, + /// Whether the field content is masked (for sensitive fields like passwords or API keys). + masked: Option, } impl Focusable for InputField { @@ -63,6 +66,7 @@ impl InputField { min_width: px(192.).into(), tab_index: None, tab_stop: true, + masked: None, } } @@ -96,6 +100,12 @@ impl InputField { self } + /// Sets this field as a masked/sensitive input (e.g., for passwords or API keys). + pub fn masked(mut self, masked: bool) -> Self { + self.masked = Some(masked); + self + } + pub fn is_empty(&self, cx: &App) -> bool { self.editor().text(cx).trim().is_empty() } @@ -115,12 +125,20 @@ impl InputField { pub fn set_text(&self, text: &str, window: &mut Window, cx: &mut App) { self.editor().set_text(text, window, cx) } + + pub fn set_masked(&self, masked: bool, window: &mut Window, cx: &mut App) { + self.editor().set_masked(masked, window, cx) + } } impl Render for InputField { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { let editor = self.editor.clone(); + if let Some(masked) = self.masked { + self.editor.set_masked(masked, window, cx); + } + let theme_color = cx.theme().colors(); let style = InputFieldStyle { @@ -172,7 +190,31 @@ impl Render for InputField { this.gap_1() .child(Icon::new(icon).size(IconSize::Small).color(Color::Muted)) }) - .child(self.editor.render(window, cx)), + .child(self.editor.render(window, cx)) + .when_some(self.masked, |this, is_masked| { + this.child( + IconButton::new( + "toggle-masked", + if is_masked { + IconName::Eye + } else { + IconName::EyeOff + }, + ) + .icon_size(IconSize::Small) + .icon_color(Color::Muted) + .tooltip(Tooltip::text(if is_masked { "Show" } else { "Hide" })) + .on_click(cx.listener( + |this, _, window, cx| { + if let Some(ref mut masked) = this.masked { + *masked = !*masked; + this.editor.set_masked(*masked, window, cx); + cx.notify(); + } + }, + )), + ) + }), ) } }