T23c round 1 fix

- suggestions: arrows step past disabled rows and scroll the cursor into view;
  the cursor starts on an enabled row; Enter refuses a disabled one
- active_trigger takes the query's word test: one word for mentions, any
  non-space for the composer, so a file name keeps its dot
- DragDropOverlay takes the drop itself, which gives it the hitbox gpui needs
  to style a drag-over
- ParameterPanel shows the places of the step, value and both ends
- gallery: the prompt menu fills the draft; Reset restores the system prompt
- AGENTS: drag-over styles need a hitbox; debug_bounds never clear
This commit is contained in:
ZacharyZhang-NY committed 2026-09-26 06:59:37 -04:00
1 parent d362f08d41
commit eb4e9422c9
10 files changed
+347 -100

No files matched your search

+132 -12
View File
@@ -1,19 +1,22 @@
use gpui::{
AppContext as _, Context, Entity, Focusable, IntoElement, ParentElement, Render, SharedString,
Styled, TestAppContext, VisualTestContext, Window, div,
AppContext as _, Context, Entity, ExternalPaths, FileDropEvent, Focusable, IntoElement,
Modifiers, ParentElement, Render, SharedString, Styled, TestAppContext, VisualTestContext,
Window, div, point, px,
};
use super::settle;
use crate::{
chat::PromptInput,
forms::{self, Choice, TextInput},
theme::Theme,
motion,
theme::{ActiveTheme, Theme},
};
/// A composer over a field, and what it asked: sends, commands and context.
struct Compose {
field: Entity<TextInput>,
busy: bool,
commands: Vec<Choice>,
sent: usize,
picked: Vec<SharedString>,
}
@@ -24,13 +27,13 @@ impl Render for Compose {
let input = PromptInput::new("compose", &self.field, move |_, cx| {
send.update(cx, |compose, _| compose.sent += 1)
})
.commands(
[Choice::new("summarize", "/summarize")],
move |value, _, cx| command.update(cx, |compose, _| compose.picked.push(value.clone())),
)
.commands(self.commands.clone(), move |value, _, cx| {
command.update(cx, |compose, _| compose.picked.push(value.clone()))
})
.context([Choice::new("lift", "lift.rs")], move |value, _, cx| {
mention.update(cx, |compose, _| compose.picked.push(value.clone()))
});
})
.on_drop(|_, _, _| {});
let input = if self.busy {
input.busy(|_, _| {})
} else {
@@ -40,14 +43,20 @@ impl Render for Compose {
}
}
fn open(busy: bool, cx: &mut TestAppContext) -> (Entity<Compose>, &mut VisualTestContext) {
fn open(
busy: bool,
commands: Vec<Choice>,
cx: &mut TestAppContext,
) -> (Entity<Compose>, &mut VisualTestContext) {
cx.update(|cx| {
Theme::init(cx);
Theme::update(cx, |theme| theme.reduced_motion = true);
forms::bind_keys(cx);
});
let (compose, cx) = cx.add_window_view(move |window, cx| Compose {
field: cx.new(|cx| TextInput::new(window, cx).multi_line(1, 8)),
busy,
commands,
sent: 0,
picked: Vec::new(),
});
@@ -60,13 +69,44 @@ fn open(busy: bool, cx: &mut TestAppContext) -> (Entity<Compose>, &mut VisualTes
(compose, cx)
}
fn summarize() -> Vec<Choice> {
vec![Choice::new("summarize", "/summarize")]
}
/// Commands `c00` to `c19`, some disabled.
fn numbered(disabled: &[usize]) -> Vec<Choice> {
(0..20)
.map(|ix| {
let choice = Choice::new(format!("c{ix:02}"), format!("/c{ix:02}"));
if disabled.contains(&ix) {
choice.disabled()
} else {
choice
}
})
.collect()
}
/// Frames 2ms apart, past reduced motion's 1ms entrances.
fn frames(cx: &mut VisualTestContext) {
for _ in 0..3 {
std::thread::sleep(std::time::Duration::from_millis(2));
cx.update(|window, _| window.refresh());
cx.run_until_parked();
}
}
fn picked(compose: &Entity<Compose>, cx: &mut VisualTestContext) -> Vec<SharedString> {
compose.read_with(cx, |compose, _| compose.picked.clone())
}
fn text(compose: &Entity<Compose>, cx: &mut VisualTestContext) -> String {
cx.update(|_, cx| compose.read(cx).field.read(cx).text().to_string())
}
#[gpui::test]
fn enter_sends_and_shift_enter_breaks_the_line(cx: &mut TestAppContext) {
let (compose, cx) = open(false, cx);
let (compose, cx) = open(false, summarize(), cx);
cx.simulate_keystrokes("enter");
assert_eq!(
compose.read_with(cx, |compose, _| compose.sent),
@@ -90,7 +130,7 @@ fn enter_sends_and_shift_enter_breaks_the_line(cx: &mut TestAppContext) {
#[gpui::test]
fn a_slash_picks_a_command_and_an_at_sign_picks_context(cx: &mut TestAppContext) {
let (compose, cx) = open(false, cx);
let (compose, cx) = open(false, summarize(), cx);
cx.simulate_input("/sum");
settle(cx);
cx.simulate_keystrokes("enter");
@@ -109,10 +149,90 @@ fn a_slash_picks_a_command_and_an_at_sign_picks_context(cx: &mut TestAppContext)
#[gpui::test]
fn a_busy_composer_holds_its_message(cx: &mut TestAppContext) {
let (compose, cx) = open(true, cx);
let (compose, cx) = open(true, summarize(), cx);
cx.simulate_input("wait");
settle(cx);
cx.simulate_keystrokes("enter");
assert_eq!(compose.read_with(cx, |compose, _| compose.sent), 0);
assert_eq!(text(&compose, cx), "wait");
}
#[gpui::test]
fn a_file_name_keeps_its_suggestions_past_a_dot(cx: &mut TestAppContext) {
let (compose, cx) = open(false, summarize(), cx);
cx.simulate_input("see @lift.");
settle(cx);
cx.simulate_keystrokes("enter");
settle(cx);
assert_eq!(text(&compose, cx), "see ");
assert_eq!(picked(&compose, cx), ["lift"]);
assert_eq!(compose.read_with(cx, |compose, _| compose.sent), 0);
}
#[gpui::test]
fn arrows_pass_disabled_rows_and_keep_the_cursor_in_view(cx: &mut TestAppContext) {
let (compose, cx) = open(false, numbered(&[1]), cx);
cx.simulate_input("/");
frames(cx);
for _ in 0..16 {
cx.simulate_keystrokes("down");
frames(cx);
}
let (anchor, height) = cx.update(|window, cx| {
let anchor = compose.read(cx).field.read(cx).bounds_for(0);
let height = cx.theme().list_max_height().to_pixels(window.rem_size());
(anchor.expect("the slash is laid out"), height)
});
let foot = point(
anchor.left() + px(24.0),
anchor.bottom() + motion::NUDGE + height - px(8.0),
);
cx.simulate_mouse_move(foot, None, Modifiers::none());
cx.simulate_click(foot, Modifiers::none());
assert_eq!(
picked(&compose, cx),
["c17"],
"sixteen steps past one disabled row land on the eighteenth, at the list's foot"
);
}
#[gpui::test]
fn the_cursor_starts_on_the_first_enabled_row(cx: &mut TestAppContext) {
let (compose, cx) = open(false, numbered(&[0]), cx);
cx.simulate_input("/");
settle(cx);
cx.simulate_keystrokes("enter");
settle(cx);
assert_eq!(picked(&compose, cx), ["c01"]);
}
#[gpui::test]
fn enter_on_only_disabled_rows_picks_and_sends_nothing(cx: &mut TestAppContext) {
let (compose, cx) = open(false, vec![Choice::new("c00", "/c00").disabled()], cx);
cx.simulate_input("/");
settle(cx);
cx.simulate_keystrokes("enter");
settle(cx);
assert_eq!(picked(&compose, cx), Vec::<SharedString>::new());
assert_eq!(compose.read_with(cx, |compose, _| compose.sent), 0);
assert_eq!(text(&compose, cx), "/");
}
#[gpui::test]
fn files_over_the_composer_raise_the_veil(cx: &mut TestAppContext) {
let (_, cx) = open(false, summarize(), cx);
assert_eq!(
cx.debug_bounds("drag-drop-overlay"),
None,
"no veil at rest"
);
cx.simulate_event(FileDropEvent::Entered {
position: point(px(8.0), px(8.0)),
paths: ExternalPaths::default(),
});
settle(cx);
assert!(
cx.debug_bounds("drag-drop-overlay").is_some(),
"files over the composer raise the veil"
);
}