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:
@@ -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"),
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user