agent_ui: Mask API key input in Add LLM provider modal (#50379)

Xiaobo Liu and Danilo Leal created

Release Notes:

- Added Mask API key input in Add LLM provider modal


<img width="427" height="430" alt="截屏2026-02-28 17 35 22"
src="https://github.com/user-attachments/assets/ae628815-f7df-4ea0-90ea-a23bbd703521"
/>

---------

Signed-off-by: Xiaobo Liu <cppcoffee@gmail.com>
Co-authored-by: Danilo Leal <daniloleal09@gmail.com>

Change summary

assets/icons/eye_off.svg                                          |  6 
crates/agent_ui/src/agent_configuration/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(-)

Detailed changes

assets/icons/eye_off.svg 🔗

@@ -0,0 +1,6 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M9.5248 9.52487C9.32192 9.74576 9.07604 9.92297 8.80229 10.0457C8.52854 10.1685 8.23269 10.2341 7.93287 10.2384C7.63305 10.2427 7.33543 10.1857 7.05826 10.0709C6.78109 9.95608 6.53019 9.78592 6.32115 9.57088C6.11211 9.35584 5.94929 9.10052 5.84242 8.82002C5.73556 8.53953 5.68693 8.23974 5.69959 7.93908C5.71225 7.63842 5.78588 7.34389 5.9159 7.07326C6.04593 6.80263 6.22978 6.56148 6.45605 6.36487" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M6.58521 3.93988C7.04677 3.8469 7.51825 3.80005 7.99115 3.80055C9.27177 3.80055 10.5219 4.18055 11.584 4.89055C12.6461 5.60055 13.472 6.61055 13.956 7.79055C13.9839 7.85737 13.9989 7.92893 14 8.00131C14.0011 8.07369 13.9882 8.14566 13.9622 8.21327C13.706 8.81927 13.3778 9.39377 12.9841 9.92417" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M3.48047 5.37988C2.85585 5.97555 2.36015 6.69431 2.02605 7.49388C1.90005 7.80488 1.90005 8.15188 2.02605 8.46288C2.52405 9.64988 3.35605 10.6599 4.41705 11.3699C5.47805 12.0799 6.72205 12.4559 7.99305 12.4559C9.01905 12.4559 10.0291 12.2019 10.9311 11.7179" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M2 2L14 14" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

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,

crates/icons/src/icons.rs 🔗

@@ -114,6 +114,7 @@ pub enum IconName {
     ExpandUp,
     ExpandVertical,
     Eye,
+    EyeOff,
     FastForward,
     FastForwardOff,
     File,

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<isize>,
     /// 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<bool>,
 }
 
 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<Self>) -> 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();
+                                    }
+                                },
+                            )),
+                        )
+                    }),
             )
     }
 }