T23c: AI chat, input

- PromptInput: attachments and context above the field, the owner's tools and
  send below; Enter sends, Shift-Enter breaks the line; / offers commands and
  @ offers context over forms::Suggestions; files dropped on it attach behind a
  drop veil
- AttachmentButton, AttachmentChip with thumbnails and uploads, ContextChips
- VoiceInputButton and VoiceWaveform with a red dot and the time
- ParameterPanel, SystemPromptEditor, TokenCounter, CostEstimator
- forms: a Newline action on Shift-Enter keeps one-line fields to one line;
  replace_trigger moves beside Suggestions; choose and dropped shared
- gallery: composer, pickers and tuning
This commit is contained in:
ZacharyZhang-NY committed 2026-09-26 06:29:20 -04:00
1 parent fce38dec00
commit d362f08d41
17 files changed
+1562 -38

No files matched your search

+118
View File
@@ -0,0 +1,118 @@
use gpui::{
AppContext as _, Context, Entity, Focusable, IntoElement, ParentElement, Render, SharedString,
Styled, TestAppContext, VisualTestContext, Window, div,
};
use super::settle;
use crate::{
chat::PromptInput,
forms::{self, Choice, TextInput},
theme::Theme,
};
/// A composer over a field, and what it asked: sends, commands and context.
struct Compose {
field: Entity<TextInput>,
busy: bool,
sent: usize,
picked: Vec<SharedString>,
}
impl Render for Compose {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let (send, command, mention) = (cx.entity(), cx.entity(), cx.entity());
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())),
)
.context([Choice::new("lift", "lift.rs")], move |value, _, cx| {
mention.update(cx, |compose, _| compose.picked.push(value.clone()))
});
let input = if self.busy {
input.busy(|_, _| {})
} else {
input
};
div().size_full().child(input)
}
}
fn open(busy: bool, cx: &mut TestAppContext) -> (Entity<Compose>, &mut VisualTestContext) {
cx.update(|cx| {
Theme::init(cx);
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,
sent: 0,
picked: Vec::new(),
});
cx.update(|window, cx| {
window.activate_window();
let field = compose.read(cx).field.focus_handle(cx);
window.focus(&field);
});
settle(cx);
(compose, cx)
}
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);
cx.simulate_keystrokes("enter");
assert_eq!(
compose.read_with(cx, |compose, _| compose.sent),
0,
"nothing to send"
);
cx.simulate_input("lift");
cx.simulate_keystrokes("shift-enter");
settle(cx);
assert_eq!(text(&compose, cx), "lift\n");
cx.simulate_input("tone");
settle(cx);
cx.simulate_keystrokes("enter");
assert_eq!(compose.read_with(cx, |compose, _| compose.sent), 1);
assert_eq!(
text(&compose, cx),
"lift\ntone",
"Enter sends without a new line"
);
}
#[gpui::test]
fn a_slash_picks_a_command_and_an_at_sign_picks_context(cx: &mut TestAppContext) {
let (compose, cx) = open(false, cx);
cx.simulate_input("/sum");
settle(cx);
cx.simulate_keystrokes("enter");
settle(cx);
assert_eq!(text(&compose, cx), "", "the command's words go");
cx.simulate_input("see @li");
settle(cx);
cx.simulate_keystrokes("enter");
settle(cx);
assert_eq!(text(&compose, cx), "see ");
compose.read_with(cx, |compose, _| {
assert_eq!(compose.picked, ["summarize", "lift"]);
assert_eq!(compose.sent, 0, "a pick is not a send");
});
}
#[gpui::test]
fn a_busy_composer_holds_its_message(cx: &mut TestAppContext) {
let (compose, cx) = open(true, 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");
}
+1
View File
@@ -8,6 +8,7 @@ use gpui::{
};
mod cite;
mod compose;
use super::{
ImageMessage, MessageAvatar, MessageBubble, MessageEditor, MessageList, Role, ThinkingBlock,