From 5d76cc8840124b3e774db9ee9cd5fbcb5e0d410c Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Sat, 18 Apr 2026 16:03:08 +0700 Subject: [PATCH] feat(app): gate return-key submit icon to new workspace composer The new-workspace composer is a one-shot "create" submit, not a chat send, so a return-key glyph reads more accurately there than an arrow. Add a submitIcon prop on Composer/MessageInput (defaulting to "arrow") and opt NewWorkspace into "return". --- packages/app/src/components/composer.test.tsx | 1 + packages/app/src/components/composer.tsx | 3 ++ .../app/src/components/message-input.test.tsx | 28 +++++++++++++++++-- packages/app/src/components/message-input.tsx | 8 ++++-- .../src/screens/new-workspace-screen.test.tsx | 9 +++++- .../app/src/screens/new-workspace-screen.tsx | 1 + 6 files changed, 44 insertions(+), 6 deletions(-) diff --git a/packages/app/src/components/composer.test.tsx b/packages/app/src/components/composer.test.tsx index 8316ebc17..d148d2af0 100644 --- a/packages/app/src/components/composer.test.tsx +++ b/packages/app/src/components/composer.test.tsx @@ -176,6 +176,7 @@ vi.mock("lucide-react-native", () => { const createIcon = (name: string) => (props: Record) => React.createElement("span", { ...props, "data-icon": name }); return { + ArrowUp: createIcon("ArrowUp"), CornerDownLeft: createIcon("CornerDownLeft"), Square: createIcon("Square"), Pencil: createIcon("Pencil"), diff --git a/packages/app/src/components/composer.tsx b/packages/app/src/components/composer.tsx index f0b8f6269..24fe17f3f 100644 --- a/packages/app/src/components/composer.tsx +++ b/packages/app/src/components/composer.tsx @@ -100,6 +100,7 @@ interface ComposerProps { allowEmptySubmit?: boolean; /** Optional accessibility label for the primary submit button. */ submitButtonAccessibilityLabel?: string; + submitIcon?: "arrow" | "return"; /** Externally controlled loading state. When true, disables the submit button. */ isSubmitLoading?: boolean; submitBehavior?: "clear" | "preserve-and-lock"; @@ -142,6 +143,7 @@ export function Composer({ hasExternalContent = false, allowEmptySubmit = false, submitButtonAccessibilityLabel, + submitIcon = "arrow", isSubmitLoading = false, submitBehavior = "clear", blurOnSubmit = false, @@ -930,6 +932,7 @@ export function Composer({ hasExternalContent={hasExternalContent} allowEmptySubmit={allowEmptySubmit} submitButtonAccessibilityLabel={submitButtonAccessibilityLabel} + submitIcon={submitIcon} isSubmitDisabled={isProcessing || isSubmitLoading} isSubmitLoading={isProcessing || isSubmitLoading} attachments={selectedAttachments} diff --git a/packages/app/src/components/message-input.test.tsx b/packages/app/src/components/message-input.test.tsx index ebe016187..fdf329e36 100644 --- a/packages/app/src/components/message-input.test.tsx +++ b/packages/app/src/components/message-input.test.tsx @@ -49,6 +49,7 @@ vi.mock("lucide-react-native", () => { const createIcon = (name: string) => (props: Record) => React.createElement("span", { ...props, "data-icon": name }); return { + ArrowUp: createIcon("ArrowUp"), Mic: createIcon("Mic"), MicOff: createIcon("MicOff"), CornerDownLeft: createIcon("CornerDownLeft"), @@ -226,18 +227,27 @@ afterEach(() => { vi.unstubAllGlobals(); }); -function renderMessageInput(menuItems: AttachmentMenuItem[]) { +interface RenderMessageInputOptions { + value?: string; + submitIcon?: "arrow" | "return"; +} + +function renderMessageInput( + menuItems: AttachmentMenuItem[], + { value = "", submitIcon }: RenderMessageInputOptions = {}, +) { act(() => { root?.render( , ); @@ -302,4 +312,16 @@ describe("MessageInput attachments", () => { expect(queryAllByAriaLabel("Queue message")).toHaveLength(0); }); + + it("uses ArrowUp by default and CornerDownLeft when return submit icon is requested", () => { + renderMessageInput([], { value: "Send this" }); + + expect(document.querySelectorAll('[data-icon="ArrowUp"]')).toHaveLength(1); + expect(document.querySelectorAll('[data-icon="CornerDownLeft"]')).toHaveLength(0); + + renderMessageInput([], { value: "Create this", submitIcon: "return" }); + + expect(document.querySelectorAll('[data-icon="ArrowUp"]')).toHaveLength(0); + expect(document.querySelectorAll('[data-icon="CornerDownLeft"]')).toHaveLength(1); + }); }); diff --git a/packages/app/src/components/message-input.tsx b/packages/app/src/components/message-input.tsx index 34074155a..28b855c93 100644 --- a/packages/app/src/components/message-input.tsx +++ b/packages/app/src/components/message-input.tsx @@ -20,7 +20,7 @@ import { forwardRef, } from "react"; import { StyleSheet, useUnistyles } from "react-native-unistyles"; -import { Mic, MicOff, CornerDownLeft, Plus, Square } from "lucide-react-native"; +import { ArrowUp, Mic, MicOff, CornerDownLeft, Plus, Square } from "lucide-react-native"; import Animated, { useSharedValue, useAnimatedStyle, withTiming } from "react-native-reanimated"; import { useDictation } from "@/hooks/use-dictation"; import { DictationOverlay } from "./dictation-controls"; @@ -85,6 +85,7 @@ export interface MessageInputProps { allowEmptySubmit?: boolean; /** Optional accessibility label for the primary submit button. */ submitButtonAccessibilityLabel?: string; + submitIcon?: "arrow" | "return"; isSubmitDisabled?: boolean; isSubmitLoading?: boolean; attachments: ComposerAttachment[]; @@ -217,6 +218,7 @@ export const MessageInput = forwardRef(funct hasExternalContent = false, allowEmptySubmit = false, submitButtonAccessibilityLabel, + submitIcon = "arrow", isSubmitDisabled = false, isSubmitLoading = false, attachments, @@ -1180,8 +1182,10 @@ export const MessageInput = forwardRef(funct > {isSubmitLoading ? ( - ) : ( + ) : submitIcon === "return" ? ( + ) : ( + )} diff --git a/packages/app/src/screens/new-workspace-screen.test.tsx b/packages/app/src/screens/new-workspace-screen.test.tsx index 2e9191bcc..ffe4968d8 100644 --- a/packages/app/src/screens/new-workspace-screen.test.tsx +++ b/packages/app/src/screens/new-workspace-screen.test.tsx @@ -222,6 +222,7 @@ vi.mock("@/components/composer", () => ({ Composer: ({ onSubmitMessage, submitBehavior, + submitIcon, isSubmitLoading, value, onChangeText, @@ -234,13 +235,18 @@ vi.mock("@/components/composer", () => ({ cwd: string; }) => void; submitBehavior?: "clear" | "preserve-and-lock"; + submitIcon?: "arrow" | "return"; isSubmitLoading?: boolean; value: string; onChangeText: (text: string) => void; attachments: ComposerAttachment[]; onChangeAttachments: (attachments: ComposerAttachment[]) => void; }) => ( -
+