From 08106b67efbbbbb7e1149b5b2dad4e5fea1f2a66 Mon Sep 17 00:00:00 2001 From: Matt Toohey Date: Wed, 8 Apr 2026 15:06:39 +1000 Subject: [PATCH] fix: treat Option+Return as newline in multiline text inputs (#118) * fix: treat Option+Return as newline in multiline text inputs Currently only Shift+Return inserts a newline in multiline text areas (like the chat input). Option+Return (Alt+Return) was treated the same as plain Return, triggering the send action. Add altKey checks alongside the existing shiftKey checks in both: - ChatInput: skip send when altKey is held - PromptInputTextarea: early-return (allow newline) when altKey is held * test: add alt-enter multiline regression coverage --- ui/goose2/src/features/chat/ui/ChatInput.tsx | 2 +- .../chat/ui/__tests__/ChatInput.test.tsx | 19 ++++++++++- .../ui/ai-elements/prompt-input.test.tsx | 33 +++++++++++++++++++ .../shared/ui/ai-elements/prompt-input.tsx | 2 +- 4 files changed, 53 insertions(+), 3 deletions(-) create mode 100644 ui/goose2/src/shared/ui/ai-elements/prompt-input.test.tsx diff --git a/ui/goose2/src/features/chat/ui/ChatInput.tsx b/ui/goose2/src/features/chat/ui/ChatInput.tsx index b51dea1d04..af5ee937a7 100644 --- a/ui/goose2/src/features/chat/ui/ChatInput.tsx +++ b/ui/goose2/src/features/chat/ui/ChatInput.tsx @@ -279,7 +279,7 @@ export function ChatInput({ } } } - if (e.key === "Enter" && !e.shiftKey) { + if (e.key === "Enter" && !e.shiftKey && !e.altKey) { e.preventDefault(); handleSend(); } diff --git a/ui/goose2/src/features/chat/ui/__tests__/ChatInput.test.tsx b/ui/goose2/src/features/chat/ui/__tests__/ChatInput.test.tsx index 46b51d04bb..8e7942331c 100644 --- a/ui/goose2/src/features/chat/ui/__tests__/ChatInput.test.tsx +++ b/ui/goose2/src/features/chat/ui/__tests__/ChatInput.test.tsx @@ -1,5 +1,5 @@ import { describe, it, expect, vi } from "vitest"; -import { render, screen } from "@testing-library/react"; +import { fireEvent, render, screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { useState } from "react"; import { ChatInput } from "../ChatInput"; @@ -75,6 +75,23 @@ describe("ChatInput", () => { expect(onSend).not.toHaveBeenCalled(); }); + it("does not call onSend on Alt+Enter (newline)", async () => { + const onSend = vi.fn(); + const user = userEvent.setup(); + render(); + + const input = screen.getByRole("textbox"); + await user.type(input, "hello"); + const wasNotPrevented = fireEvent.keyDown(input, { + altKey: true, + key: "Enter", + }); + + expect(wasNotPrevented).toBe(true); + expect(onSend).not.toHaveBeenCalled(); + expect(input).toHaveValue("hello"); + }); + it("shows current model name in model picker", () => { render( { + it("does not submit on Alt+Enter and inserts a newline", async () => { + const onSubmit = vi.fn(); + const user = userEvent.setup(); + + render( + + + + , + ); + + const input = screen.getByRole("textbox"); + await user.type(input, "hello"); + const wasNotPrevented = fireEvent.keyDown(input, { + altKey: true, + key: "Enter", + }); + + expect(wasNotPrevented).toBe(true); + expect(onSubmit).not.toHaveBeenCalled(); + expect(input).toHaveValue("hello"); + }); +}); diff --git a/ui/goose2/src/shared/ui/ai-elements/prompt-input.tsx b/ui/goose2/src/shared/ui/ai-elements/prompt-input.tsx index ba9299ecb2..2f5496497c 100644 --- a/ui/goose2/src/shared/ui/ai-elements/prompt-input.tsx +++ b/ui/goose2/src/shared/ui/ai-elements/prompt-input.tsx @@ -972,7 +972,7 @@ export const PromptInputTextarea = ({ if (isComposing || e.nativeEvent.isComposing) { return; } - if (e.shiftKey) { + if (e.shiftKey || e.altKey) { return; } e.preventDefault();