From 98a83b47e6ebf3a0527d32d13645bcde763a548b Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Mon, 17 Nov 2025 19:13:15 -0300 Subject: [PATCH] agent_ui: Make input fields in Bedrock settings keyboard navigable (#42916) Closes https://github.com/zed-industries/zed/issues/36587 This PR enables jumping from one input to the other, in the Bedrock settings section, with tab. Release Notes: - N/A --- .../language_models/src/provider/bedrock.rs | 74 +++++++++++++++---- crates/ui_input/src/input_field.rs | 27 +++++++ crates/zed/src/zed.rs | 1 + 3 files changed, 87 insertions(+), 15 deletions(-) diff --git a/crates/language_models/src/provider/bedrock.rs b/crates/language_models/src/provider/bedrock.rs index 14dd575f23952ee732c5d9714d2e091cf50d606f..61f36428d2e69af013103c8ca06b38d8d4a96e8d 100644 --- a/crates/language_models/src/provider/bedrock.rs +++ b/crates/language_models/src/provider/bedrock.rs @@ -24,7 +24,10 @@ use bedrock::{ use collections::{BTreeMap, HashMap}; use credentials_provider::CredentialsProvider; use futures::{FutureExt, Stream, StreamExt, future::BoxFuture, stream::BoxStream}; -use gpui::{AnyView, App, AsyncApp, Context, Entity, FontWeight, Subscription, Task}; +use gpui::{ + AnyView, App, AsyncApp, Context, Entity, FocusHandle, FontWeight, Subscription, Task, Window, + actions, +}; use gpui_tokio::Tokio; use http_client::HttpClient; use language_model::{ @@ -47,6 +50,8 @@ use util::ResultExt; use crate::AllLanguageModelSettings; +actions!(bedrock, [Tab, TabPrev]); + const PROVIDER_ID: LanguageModelProviderId = LanguageModelProviderId::new("amazon-bedrock"); const PROVIDER_NAME: LanguageModelProviderName = LanguageModelProviderName::new("Amazon Bedrock"); @@ -1012,6 +1017,7 @@ struct ConfigurationView { region_editor: Entity, state: Entity, load_credentials_task: Option>, + focus_handle: FocusHandle, } impl ConfigurationView { @@ -1022,11 +1028,41 @@ impl ConfigurationView { const PLACEHOLDER_REGION: &'static str = "us-east-1"; fn new(state: Entity, window: &mut Window, cx: &mut Context) -> Self { + let focus_handle = cx.focus_handle(); + cx.observe(&state, |_, _, cx| { cx.notify(); }) .detach(); + let access_key_id_editor = cx.new(|cx| { + InputField::new(window, cx, Self::PLACEHOLDER_ACCESS_KEY_ID_TEXT) + .label("Access Key ID") + .tab_index(0) + .tab_stop(true) + }); + + let secret_access_key_editor = cx.new(|cx| { + InputField::new(window, cx, Self::PLACEHOLDER_SECRET_ACCESS_KEY_TEXT) + .label("Secret Access Key") + .tab_index(1) + .tab_stop(true) + }); + + let session_token_editor = cx.new(|cx| { + InputField::new(window, cx, Self::PLACEHOLDER_SESSION_TOKEN_TEXT) + .label("Session Token (Optional)") + .tab_index(2) + .tab_stop(true) + }); + + let region_editor = cx.new(|cx| { + InputField::new(window, cx, Self::PLACEHOLDER_REGION) + .label("Region") + .tab_index(3) + .tab_stop(true) + }); + let load_credentials_task = Some(cx.spawn({ let state = state.clone(); async move |this, cx| { @@ -1046,22 +1082,13 @@ impl ConfigurationView { })); Self { - access_key_id_editor: cx.new(|cx| { - InputField::new(window, cx, Self::PLACEHOLDER_ACCESS_KEY_ID_TEXT) - .label("Access Key ID") - }), - secret_access_key_editor: cx.new(|cx| { - InputField::new(window, cx, Self::PLACEHOLDER_SECRET_ACCESS_KEY_TEXT) - .label("Secret Access Key") - }), - session_token_editor: cx.new(|cx| { - InputField::new(window, cx, Self::PLACEHOLDER_SESSION_TOKEN_TEXT) - .label("Session Token (Optional)") - }), - region_editor: cx - .new(|cx| InputField::new(window, cx, Self::PLACEHOLDER_REGION).label("Region")), + access_key_id_editor, + secret_access_key_editor, + session_token_editor, + region_editor, state, load_credentials_task, + focus_handle, } } @@ -1141,6 +1168,19 @@ impl ConfigurationView { fn should_render_editor(&self, cx: &Context) -> bool { self.state.read(cx).is_authenticated() } + + fn on_tab(&mut self, _: &menu::SelectNext, window: &mut Window, _: &mut Context) { + window.focus_next(); + } + + fn on_tab_prev( + &mut self, + _: &menu::SelectPrevious, + window: &mut Window, + _: &mut Context, + ) { + window.focus_prev(); + } } impl Render for ConfigurationView { @@ -1190,6 +1230,9 @@ impl Render for ConfigurationView { v_flex() .size_full() + .track_focus(&self.focus_handle) + .on_action(cx.listener(Self::on_tab)) + .on_action(cx.listener(Self::on_tab_prev)) .on_action(cx.listener(ConfigurationView::save_credentials)) .child(Label::new("To use Zed's agent with Bedrock, you can set a custom authentication strategy through the settings.json, or use static credentials.")) .child(Label::new("But, to access models on AWS, you need to:").mt_1()) @@ -1234,6 +1277,7 @@ impl ConfigurationView { fn render_static_credentials_ui(&self) -> impl IntoElement { v_flex() .my_2() + .tab_group() .gap_1p5() .child( Label::new("Static Keys") diff --git a/crates/ui_input/src/input_field.rs b/crates/ui_input/src/input_field.rs index 82f7f0261facef8a7c6a422b2ff4ed335229aeb3..9e8c519ca9acc68c0d968f099f62ad336ee0754a 100644 --- a/crates/ui_input/src/input_field.rs +++ b/crates/ui_input/src/input_field.rs @@ -37,6 +37,10 @@ pub struct InputField { disabled: bool, /// The minimum width of for the input min_width: Length, + /// The tab index for keyboard navigation order. + tab_index: Option, + /// Whether this field is a tab stop (can be focused via Tab key). + tab_stop: bool, } impl Focusable for InputField { @@ -63,6 +67,8 @@ impl InputField { start_icon: None, disabled: false, min_width: px(192.).into(), + tab_index: None, + tab_stop: true, } } @@ -86,6 +92,16 @@ impl InputField { self } + pub fn tab_index(mut self, index: isize) -> Self { + self.tab_index = Some(index); + self + } + + pub fn tab_stop(mut self, tab_stop: bool) -> Self { + self.tab_stop = tab_stop; + self + } + pub fn set_disabled(&mut self, disabled: bool, cx: &mut Context) { self.disabled = disabled; self.editor @@ -151,6 +167,16 @@ impl Render for InputField { ..Default::default() }; + let focus_handle = self.editor.focus_handle(cx); + + let configured_handle = if let Some(tab_index) = self.tab_index { + focus_handle.tab_index(tab_index).tab_stop(self.tab_stop) + } else if !self.tab_stop { + focus_handle.tab_stop(false) + } else { + focus_handle + }; + v_flex() .id(self.placeholder.clone()) .w_full() @@ -168,6 +194,7 @@ impl Render for InputField { }) .child( h_flex() + .track_focus(&configured_handle) .min_w(self.min_width) .min_h_8() .w_full() diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 83307818ac0ec73ca704e1dbb10fd551ecdd3a42..998d1831a1b5e4179677d33a80fd36718e833511 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -4702,6 +4702,7 @@ mod tests { "assistant", "assistant2", "auto_update", + "bedrock", "branches", "buffer_search", "channel_modal",