feat(ui): task-chat composer posts on Cmd/Ctrl+Enter instead of Shift+Enter

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
scotttongandClaude Fable 5 committed 2026-08-07 16:55:54 -07:00
1 parent ced262a0da
commit 563969612f
2 files changed
+28 -13

No files matched your search

@@ -36,16 +36,19 @@ function setTextareaValue(textarea: HTMLTextAreaElement, value: string) {
});
}
function pressEnter(textarea: HTMLTextAreaElement, shiftKey = false) {
function pressEnter(
textarea: HTMLTextAreaElement,
modifiers: { metaKey?: boolean; ctrlKey?: boolean; shiftKey?: boolean } = {},
) {
flushSync(() => {
textarea.dispatchEvent(
new KeyboardEvent("keydown", { key: "Enter", shiftKey, bubbles: true, cancelable: true }),
new KeyboardEvent("keydown", { key: "Enter", ...modifiers, bubbles: true, cancelable: true }),
);
});
}
function pressShiftEnter(textarea: HTMLTextAreaElement) {
pressEnter(textarea, true);
function pressCmdEnter(textarea: HTMLTextAreaElement) {
pressEnter(textarea, { metaKey: true });
}
async function flushAsync() {
@@ -61,7 +64,7 @@ function sendButton() {
}
describe("TaskChatComposer", () => {
it("submits the trimmed body on Shift+Enter and clears the draft", async () => {
it("submits the trimmed body on Cmd+Enter and clears the draft", async () => {
const onAdd = vi.fn().mockResolvedValue(undefined);
render(<TaskChatComposer onAdd={onAdd} workMode="standard" />);
@@ -69,19 +72,31 @@ describe("TaskChatComposer", () => {
setTextareaValue(input(), " hello there ");
expect(sendButton().disabled).toBe(false);
pressShiftEnter(input());
pressCmdEnter(input());
await flushAsync();
expect(onAdd).toHaveBeenCalledWith("hello there", undefined, undefined);
expect(input().value).toBe("");
});
it("does not submit on plain Enter (newline stays in the draft)", async () => {
it("submits on Ctrl+Enter", async () => {
const onAdd = vi.fn().mockResolvedValue(undefined);
render(<TaskChatComposer onAdd={onAdd} workMode="standard" />);
setTextareaValue(input(), "hello");
pressEnter(input(), { ctrlKey: true });
await flushAsync();
expect(onAdd).toHaveBeenCalledWith("hello", undefined, undefined);
});
it("does not submit on plain Enter or Shift+Enter (newline stays in the draft)", async () => {
const onAdd = vi.fn().mockResolvedValue(undefined);
render(<TaskChatComposer onAdd={onAdd} workMode="standard" />);
setTextareaValue(input(), "line one");
pressEnter(input());
pressEnter(input(), { shiftKey: true });
await flushAsync();
expect(onAdd).not.toHaveBeenCalled();
@@ -108,7 +123,7 @@ describe("TaskChatComposer", () => {
expect(chip.textContent).toContain("Plan");
setTextareaValue(input(), "do the plan");
pressShiftEnter(input());
pressCmdEnter(input());
await flushAsync();
expect(onWorkModeChange).toHaveBeenCalledWith("planning");
@@ -127,7 +142,7 @@ describe("TaskChatComposer", () => {
);
setTextareaValue(input(), "wake up");
pressShiftEnter(input());
pressCmdEnter(input());
await flushAsync();
expect(onAdd).toHaveBeenCalledWith("wake up", true, undefined);
@@ -272,9 +272,9 @@ export function TaskChatComposer({
value={body}
onChange={(e) => setBody(e.target.value)}
onKeyDown={(e) => {
// Shift+Enter posts; plain Enter inserts a newline (Enter alone was
// too easy to trip while drafting).
if (e.key === "Enter" && e.shiftKey) {
// Cmd/Ctrl+Enter posts; plain Enter inserts a newline (Enter alone
// was too easy to trip while drafting).
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
e.preventDefault();
void submit();
return;
@@ -419,7 +419,7 @@ export function TaskChatComposer({
type="button"
onClick={() => void submit()}
disabled={disabled || submitting || body.trim().length === 0}
title="Send (Shift+Enter)"
title="Send (⌘+Enter)"
aria-label="Send"
className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-primary text-primary-foreground transition-transform hover:scale-105 disabled:scale-100 disabled:bg-muted disabled:text-muted-foreground"
data-testid="task-chat-composer-send"