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".
This commit is contained in:
Mohamed Boudra
2026-04-18 16:03:08 +07:00
parent b0e0786419
commit 5d76cc8840
6 changed files with 44 additions and 6 deletions
@@ -176,6 +176,7 @@ vi.mock("lucide-react-native", () => {
const createIcon = (name: string) => (props: Record<string, unknown>) =>
React.createElement("span", { ...props, "data-icon": name });
return {
ArrowUp: createIcon("ArrowUp"),
CornerDownLeft: createIcon("CornerDownLeft"),
Square: createIcon("Square"),
Pencil: createIcon("Pencil"),
+3
View File
@@ -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}
@@ -49,6 +49,7 @@ vi.mock("lucide-react-native", () => {
const createIcon = (name: string) => (props: Record<string, unknown>) =>
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(
<MessageInput
value=""
value={value}
onChangeText={vi.fn()}
onSubmit={vi.fn()}
attachments={[]}
cwd="/repo"
attachmentMenuItems={menuItems}
client={{ isConnected: true } as never}
isAgentRunning
isAgentRunning={false}
submitIcon={submitIcon}
onQueue={vi.fn()}
/>,
);
@@ -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);
});
});
@@ -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<MessageInputRef, MessageInputProps>(funct
hasExternalContent = false,
allowEmptySubmit = false,
submitButtonAccessibilityLabel,
submitIcon = "arrow",
isSubmitDisabled = false,
isSubmitLoading = false,
attachments,
@@ -1180,8 +1182,10 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(funct
>
{isSubmitLoading ? (
<ActivityIndicator size="small" color="white" />
) : (
) : submitIcon === "return" ? (
<CornerDownLeft size={buttonIconSize} color="white" />
) : (
<ArrowUp size={buttonIconSize} color="white" />
)}
</TooltipTrigger>
<TooltipContent side="top" align="center" offset={8}>
@@ -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;
}) => (
<div data-testid="test-composer" data-submit-behavior={submitBehavior}>
<div
data-testid="test-composer"
data-submit-behavior={submitBehavior}
data-submit-icon={submitIcon}
>
<textarea
aria-label="Message agent..."
disabled={submitBehavior === "preserve-and-lock" && isSubmitLoading}
@@ -759,6 +765,7 @@ describe("NewWorkspaceScreen picker payload", () => {
const textInput = document.querySelector('[aria-label="Message agent..."]');
const removeButton = document.querySelector(`[aria-label="Remove PR #${prItem.number}"]`);
expect(queryByTestId("test-composer")?.dataset.submitBehavior).toBe("preserve-and-lock");
expect(queryByTestId("test-composer")?.dataset.submitIcon).toBe("return");
expect(textInput).toHaveProperty("value", "please review this change");
expect(textInput).toHaveProperty("disabled", true);
expect(queryByTestId("composer-github-attachment-pill")).not.toBeNull();
@@ -462,6 +462,7 @@ export function NewWorkspaceScreen({
onSubmitMessage={handleCreateChatAgent}
allowEmptySubmit={true}
submitButtonAccessibilityLabel="Create"
submitIcon="return"
isSubmitLoading={pendingAction === "chat"}
submitBehavior="preserve-and-lock"
blurOnSubmit={true}