From a525d54c5a023dae239b28686ee6bcb761c1cd7d Mon Sep 17 00:00:00 2001 From: Anthony Ronning <101225832+AnthonyRonning@users.noreply.github.com> Date: Tue, 27 Oct 2026 06:24:07 +0000 Subject: [PATCH] Fix feedback form typing with application Vim --- apps/maple-agent/CLAUDE.md | 11 +++++ apps/maple-agent/app/src/ui/chat/feedback.rs | 24 +++++++++- apps/maple-agent/app/src/ui/chat/mod.rs | 4 +- .../maple-agent/app/src/ui/chat/navigation.rs | 1 + apps/maple-agent/app/src/ui/chat/tests.rs | 45 +++++++++++++++++++ 5 files changed, 82 insertions(+), 3 deletions(-) diff --git a/apps/maple-agent/CLAUDE.md b/apps/maple-agent/CLAUDE.md index 02820055c..4a484f881 100644 --- a/apps/maple-agent/CLAUDE.md +++ b/apps/maple-agent/CLAUDE.md @@ -103,6 +103,17 @@ popup before a nested one). The popup handles the rest: A text field inside a menu that goes away calls `Popup::refocus`, so the menu keeps the keyboard. Menu keys live in the keymap's `Menu` context. +## Ordinary forms and application Vim + +Ordinary `TextInput` fields use `.application_vim(enabled)` and an +`.on_application_escape(...)` handoff, as in the project-folder dialog. They +keep standard text editing; `.composer_vim(...)` belongs only to the composer. +Propagate changes to application Vim into open forms. Modal typing must leave +Chat's composer fallback before its application-navigation handling runs. +Test forms beneath the real Chat key context with `simulate_keystrokes`, +including literal `i`, `j`, `k`, and `l`; `simulate_input` alone bypasses the +key bindings that can steal those characters. + ## Performance This app must feel instant. Treat frame time and UI-thread stalls as bugs. diff --git a/apps/maple-agent/app/src/ui/chat/feedback.rs b/apps/maple-agent/app/src/ui/chat/feedback.rs index 229a71de8..03aaa5ff4 100644 --- a/apps/maple-agent/app/src/ui/chat/feedback.rs +++ b/apps/maple-agent/app/src/ui/chat/feedback.rs @@ -5,7 +5,7 @@ use crate::ui::{text_input::TextInput, theme, widgets}; use gpui::{Context, Div, Entity, Focusable, Window, div, prelude::*, px}; pub(super) struct FeedbackForm { - input: Entity, + pub(super) input: Entity, category: &'static str, request: Option, busy: bool, @@ -22,8 +22,16 @@ impl ChatScreen { self.sidebar .update(cx, |sidebar, cx| sidebar.close_popups(cx)); let owner = cx.entity().downgrade(); + let escape = owner.clone(); let input = cx.new(|cx| { TextInput::new("What would you like us to know?", cx) + .application_vim(self.application_vim_enabled) + .on_application_escape(move |window, cx| { + let escape = escape.clone(); + window.defer(cx, move |_, cx| { + escape.update(cx, |this, cx| this.close_feedback(cx)).ok(); + }); + }) .multiline(8) .with_tab_index(0) .on_key(move |event, _, window, cx| { @@ -61,6 +69,20 @@ impl ChatScreen { }); cx.notify(); } + pub(super) fn set_feedback_application_vim_enabled( + &mut self, + enabled: bool, + cx: &mut Context, + ) { + if let Some(form) = self.feedback.as_mut() { + if self.application_vim_enabled != enabled { + form.focus_pending = true; + } + form.input.update(cx, |input, cx| { + input.set_application_vim_enabled(enabled, cx) + }); + } + } pub(super) fn close_feedback(&mut self, cx: &mut Context) { if self.feedback.as_ref().is_some_and(|f| f.busy) { return; diff --git a/apps/maple-agent/app/src/ui/chat/mod.rs b/apps/maple-agent/app/src/ui/chat/mod.rs index c35ffdfea..9ae6d3f2a 100644 --- a/apps/maple-agent/app/src/ui/chat/mod.rs +++ b/apps/maple-agent/app/src/ui/chat/mod.rs @@ -4231,7 +4231,7 @@ impl ChatScreen { window: &mut Window, cx: &mut Context, ) { - if self.project_folder_dialog.is_some() { + if self.project_folder_dialog.is_some() || self.feedback.is_some() { return; } let Some(composer) = self.composer.clone() else { @@ -4259,7 +4259,7 @@ impl ChatScreen { return; } // A modal dialog owns the keyboard; nothing types past it. - if self.trust_prompt.is_some() || self.confirm.is_some() || self.feedback.is_some() { + if self.trust_prompt.is_some() || self.confirm.is_some() { return; } // A focused text field already receives typing: the composer, the diff --git a/apps/maple-agent/app/src/ui/chat/navigation.rs b/apps/maple-agent/app/src/ui/chat/navigation.rs index f8dda9b7d..7a33b4071 100644 --- a/apps/maple-agent/app/src/ui/chat/navigation.rs +++ b/apps/maple-agent/app/src/ui/chat/navigation.rs @@ -140,6 +140,7 @@ impl ChatScreen { dialog.set_application_vim_enabled(enabled, cx) }); } + self.set_feedback_application_vim_enabled(enabled, cx); for input in [self.composer.as_ref(), self.pending_question_input.as_ref()] .into_iter() .flatten() diff --git a/apps/maple-agent/app/src/ui/chat/tests.rs b/apps/maple-agent/app/src/ui/chat/tests.rs index 2e81c7f46..c3b0d51ed 100644 --- a/apps/maple-agent/app/src/ui/chat/tests.rs +++ b/apps/maple-agent/app/src/ui/chat/tests.rs @@ -6151,6 +6151,51 @@ mod state_tests { cx.simulate_click(bounds.center(), gpui::Modifiers::default()); } + #[gpui::test] + fn feedback_form_accepts_normal_typing_with_application_vim(cx: &mut TestAppContext) { + let (chat, cx) = chat_window(cx, |this, cx| { + this.set_application_vim_enabled(true, cx); + this.composer.as_ref().unwrap().update(cx, |input, cx| { + input.set_vim_enabled(true, cx); + input.set_text("Unsent chat draft", cx); + }); + }); + press(cx, "send-feedback"); + let input = chat.update(cx, |this, _| this.feedback.as_ref().unwrap().input.clone()); + let composer = chat.update(cx, |this, _| this.composer.clone().unwrap()); + cx.simulate_keystrokes("i j k l o g g i n g"); + assert_eq!(input.update(cx, |input, _| input.text()), "ijklogging"); + assert_eq!(input.update(cx, |input, _| input.vim_mode()), None); + cx.simulate_keystrokes("left backspace right shift-enter"); + cx.simulate_input("Second line"); + assert_eq!( + input.update(cx, |input, _| input.text()), + "ijkloggig\nSecond line" + ); + for enabled in [false, true] { + chat.update(cx, |this, cx| this.set_application_vim_enabled(enabled, cx)); + cx.run_until_parked(); + cx.simulate_keystrokes("x"); + } + assert_eq!( + input.update(cx, |input, _| input.text()), + "ijkloggig\nSecond linexx" + ); + let focus = cx.update(|window, app| window.focused(app)); + cx.simulate_keystrokes("tab tab tab tab tab tab"); + assert_eq!(cx.update(|window, app| window.focused(app)), focus); + assert_eq!( + composer.update(cx, |input, _| input.text()), + "Unsent chat draft" + ); + cx.simulate_keystrokes("escape"); + assert!(chat.update(cx, |this, _| this.feedback.is_none())); + assert_eq!( + composer.update(cx, |input, _| input.text()), + "Unsent chat draft" + ); + } + #[gpui::test] fn feedback_form_keeps_typing_out_of_chat_and_rejects_empty_submission( cx: &mut TestAppContext,