Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions apps/maple-agent/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
24 changes: 23 additions & 1 deletion apps/maple-agent/app/src/ui/chat/feedback.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<TextInput>,
pub(super) input: Entity<TextInput>,
category: &'static str,
request: Option<Submission>,
busy: bool,
Expand All @@ -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| {
Expand Down Expand Up @@ -61,6 +69,20 @@ impl ChatScreen {
});
cx.notify();
}
pub(super) fn set_feedback_application_vim_enabled(
&mut self,
enabled: bool,
cx: &mut Context<Self>,
) {
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<Self>) {
if self.feedback.as_ref().is_some_and(|f| f.busy) {
return;
Expand Down
4 changes: 2 additions & 2 deletions apps/maple-agent/app/src/ui/chat/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4231,7 +4231,7 @@ impl ChatScreen {
window: &mut Window,
cx: &mut Context<Self>,
) {
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 {
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions apps/maple-agent/app/src/ui/chat/navigation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
45 changes: 45 additions & 0 deletions apps/maple-agent/app/src/ui/chat/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading