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();
+ }
+ },
+ )),
+ )
+ }),
)
}
}