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>) =>
|
const createIcon = (name: string) => (props: Record<string, unknown>) =>
|
||||||
React.createElement("span", { ...props, "data-icon": name });
|
React.createElement("span", { ...props, "data-icon": name });
|
||||||
return {
|
return {
|
||||||
|
ArrowUp: createIcon("ArrowUp"),
|
||||||
CornerDownLeft: createIcon("CornerDownLeft"),
|
CornerDownLeft: createIcon("CornerDownLeft"),
|
||||||
Square: createIcon("Square"),
|
Square: createIcon("Square"),
|
||||||
Pencil: createIcon("Pencil"),
|
Pencil: createIcon("Pencil"),
|
||||||
|
|||||||
@@ -100,6 +100,7 @@ interface ComposerProps {
|
|||||||
allowEmptySubmit?: boolean;
|
allowEmptySubmit?: boolean;
|
||||||
/** Optional accessibility label for the primary submit button. */
|
/** Optional accessibility label for the primary submit button. */
|
||||||
submitButtonAccessibilityLabel?: string;
|
submitButtonAccessibilityLabel?: string;
|
||||||
|
submitIcon?: "arrow" | "return";
|
||||||
/** Externally controlled loading state. When true, disables the submit button. */
|
/** Externally controlled loading state. When true, disables the submit button. */
|
||||||
isSubmitLoading?: boolean;
|
isSubmitLoading?: boolean;
|
||||||
submitBehavior?: "clear" | "preserve-and-lock";
|
submitBehavior?: "clear" | "preserve-and-lock";
|
||||||
@@ -142,6 +143,7 @@ export function Composer({
|
|||||||
hasExternalContent = false,
|
hasExternalContent = false,
|
||||||
allowEmptySubmit = false,
|
allowEmptySubmit = false,
|
||||||
submitButtonAccessibilityLabel,
|
submitButtonAccessibilityLabel,
|
||||||
|
submitIcon = "arrow",
|
||||||
isSubmitLoading = false,
|
isSubmitLoading = false,
|
||||||
submitBehavior = "clear",
|
submitBehavior = "clear",
|
||||||
blurOnSubmit = false,
|
blurOnSubmit = false,
|
||||||
@@ -930,6 +932,7 @@ export function Composer({
|
|||||||
hasExternalContent={hasExternalContent}
|
hasExternalContent={hasExternalContent}
|
||||||
allowEmptySubmit={allowEmptySubmit}
|
allowEmptySubmit={allowEmptySubmit}
|
||||||
submitButtonAccessibilityLabel={submitButtonAccessibilityLabel}
|
submitButtonAccessibilityLabel={submitButtonAccessibilityLabel}
|
||||||
|
submitIcon={submitIcon}
|
||||||
isSubmitDisabled={isProcessing || isSubmitLoading}
|
isSubmitDisabled={isProcessing || isSubmitLoading}
|
||||||
isSubmitLoading={isProcessing || isSubmitLoading}
|
isSubmitLoading={isProcessing || isSubmitLoading}
|
||||||
attachments={selectedAttachments}
|
attachments={selectedAttachments}
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ vi.mock("lucide-react-native", () => {
|
|||||||
const createIcon = (name: string) => (props: Record<string, unknown>) =>
|
const createIcon = (name: string) => (props: Record<string, unknown>) =>
|
||||||
React.createElement("span", { ...props, "data-icon": name });
|
React.createElement("span", { ...props, "data-icon": name });
|
||||||
return {
|
return {
|
||||||
|
ArrowUp: createIcon("ArrowUp"),
|
||||||
Mic: createIcon("Mic"),
|
Mic: createIcon("Mic"),
|
||||||
MicOff: createIcon("MicOff"),
|
MicOff: createIcon("MicOff"),
|
||||||
CornerDownLeft: createIcon("CornerDownLeft"),
|
CornerDownLeft: createIcon("CornerDownLeft"),
|
||||||
@@ -226,18 +227,27 @@ afterEach(() => {
|
|||||||
vi.unstubAllGlobals();
|
vi.unstubAllGlobals();
|
||||||
});
|
});
|
||||||
|
|
||||||
function renderMessageInput(menuItems: AttachmentMenuItem[]) {
|
interface RenderMessageInputOptions {
|
||||||
|
value?: string;
|
||||||
|
submitIcon?: "arrow" | "return";
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderMessageInput(
|
||||||
|
menuItems: AttachmentMenuItem[],
|
||||||
|
{ value = "", submitIcon }: RenderMessageInputOptions = {},
|
||||||
|
) {
|
||||||
act(() => {
|
act(() => {
|
||||||
root?.render(
|
root?.render(
|
||||||
<MessageInput
|
<MessageInput
|
||||||
value=""
|
value={value}
|
||||||
onChangeText={vi.fn()}
|
onChangeText={vi.fn()}
|
||||||
onSubmit={vi.fn()}
|
onSubmit={vi.fn()}
|
||||||
attachments={[]}
|
attachments={[]}
|
||||||
cwd="/repo"
|
cwd="/repo"
|
||||||
attachmentMenuItems={menuItems}
|
attachmentMenuItems={menuItems}
|
||||||
client={{ isConnected: true } as never}
|
client={{ isConnected: true } as never}
|
||||||
isAgentRunning
|
isAgentRunning={false}
|
||||||
|
submitIcon={submitIcon}
|
||||||
onQueue={vi.fn()}
|
onQueue={vi.fn()}
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
@@ -302,4 +312,16 @@ describe("MessageInput attachments", () => {
|
|||||||
|
|
||||||
expect(queryAllByAriaLabel("Queue message")).toHaveLength(0);
|
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,
|
forwardRef,
|
||||||
} from "react";
|
} from "react";
|
||||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
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 Animated, { useSharedValue, useAnimatedStyle, withTiming } from "react-native-reanimated";
|
||||||
import { useDictation } from "@/hooks/use-dictation";
|
import { useDictation } from "@/hooks/use-dictation";
|
||||||
import { DictationOverlay } from "./dictation-controls";
|
import { DictationOverlay } from "./dictation-controls";
|
||||||
@@ -85,6 +85,7 @@ export interface MessageInputProps {
|
|||||||
allowEmptySubmit?: boolean;
|
allowEmptySubmit?: boolean;
|
||||||
/** Optional accessibility label for the primary submit button. */
|
/** Optional accessibility label for the primary submit button. */
|
||||||
submitButtonAccessibilityLabel?: string;
|
submitButtonAccessibilityLabel?: string;
|
||||||
|
submitIcon?: "arrow" | "return";
|
||||||
isSubmitDisabled?: boolean;
|
isSubmitDisabled?: boolean;
|
||||||
isSubmitLoading?: boolean;
|
isSubmitLoading?: boolean;
|
||||||
attachments: ComposerAttachment[];
|
attachments: ComposerAttachment[];
|
||||||
@@ -217,6 +218,7 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(funct
|
|||||||
hasExternalContent = false,
|
hasExternalContent = false,
|
||||||
allowEmptySubmit = false,
|
allowEmptySubmit = false,
|
||||||
submitButtonAccessibilityLabel,
|
submitButtonAccessibilityLabel,
|
||||||
|
submitIcon = "arrow",
|
||||||
isSubmitDisabled = false,
|
isSubmitDisabled = false,
|
||||||
isSubmitLoading = false,
|
isSubmitLoading = false,
|
||||||
attachments,
|
attachments,
|
||||||
@@ -1180,8 +1182,10 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(funct
|
|||||||
>
|
>
|
||||||
{isSubmitLoading ? (
|
{isSubmitLoading ? (
|
||||||
<ActivityIndicator size="small" color="white" />
|
<ActivityIndicator size="small" color="white" />
|
||||||
) : (
|
) : submitIcon === "return" ? (
|
||||||
<CornerDownLeft size={buttonIconSize} color="white" />
|
<CornerDownLeft size={buttonIconSize} color="white" />
|
||||||
|
) : (
|
||||||
|
<ArrowUp size={buttonIconSize} color="white" />
|
||||||
)}
|
)}
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent side="top" align="center" offset={8}>
|
<TooltipContent side="top" align="center" offset={8}>
|
||||||
|
|||||||
@@ -222,6 +222,7 @@ vi.mock("@/components/composer", () => ({
|
|||||||
Composer: ({
|
Composer: ({
|
||||||
onSubmitMessage,
|
onSubmitMessage,
|
||||||
submitBehavior,
|
submitBehavior,
|
||||||
|
submitIcon,
|
||||||
isSubmitLoading,
|
isSubmitLoading,
|
||||||
value,
|
value,
|
||||||
onChangeText,
|
onChangeText,
|
||||||
@@ -234,13 +235,18 @@ vi.mock("@/components/composer", () => ({
|
|||||||
cwd: string;
|
cwd: string;
|
||||||
}) => void;
|
}) => void;
|
||||||
submitBehavior?: "clear" | "preserve-and-lock";
|
submitBehavior?: "clear" | "preserve-and-lock";
|
||||||
|
submitIcon?: "arrow" | "return";
|
||||||
isSubmitLoading?: boolean;
|
isSubmitLoading?: boolean;
|
||||||
value: string;
|
value: string;
|
||||||
onChangeText: (text: string) => void;
|
onChangeText: (text: string) => void;
|
||||||
attachments: ComposerAttachment[];
|
attachments: ComposerAttachment[];
|
||||||
onChangeAttachments: (attachments: ComposerAttachment[]) => void;
|
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
|
<textarea
|
||||||
aria-label="Message agent..."
|
aria-label="Message agent..."
|
||||||
disabled={submitBehavior === "preserve-and-lock" && isSubmitLoading}
|
disabled={submitBehavior === "preserve-and-lock" && isSubmitLoading}
|
||||||
@@ -759,6 +765,7 @@ describe("NewWorkspaceScreen picker payload", () => {
|
|||||||
const textInput = document.querySelector('[aria-label="Message agent..."]');
|
const textInput = document.querySelector('[aria-label="Message agent..."]');
|
||||||
const removeButton = document.querySelector(`[aria-label="Remove PR #${prItem.number}"]`);
|
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.submitBehavior).toBe("preserve-and-lock");
|
||||||
|
expect(queryByTestId("test-composer")?.dataset.submitIcon).toBe("return");
|
||||||
expect(textInput).toHaveProperty("value", "please review this change");
|
expect(textInput).toHaveProperty("value", "please review this change");
|
||||||
expect(textInput).toHaveProperty("disabled", true);
|
expect(textInput).toHaveProperty("disabled", true);
|
||||||
expect(queryByTestId("composer-github-attachment-pill")).not.toBeNull();
|
expect(queryByTestId("composer-github-attachment-pill")).not.toBeNull();
|
||||||
|
|||||||
@@ -462,6 +462,7 @@ export function NewWorkspaceScreen({
|
|||||||
onSubmitMessage={handleCreateChatAgent}
|
onSubmitMessage={handleCreateChatAgent}
|
||||||
allowEmptySubmit={true}
|
allowEmptySubmit={true}
|
||||||
submitButtonAccessibilityLabel="Create"
|
submitButtonAccessibilityLabel="Create"
|
||||||
|
submitIcon="return"
|
||||||
isSubmitLoading={pendingAction === "chat"}
|
isSubmitLoading={pendingAction === "chat"}
|
||||||
submitBehavior="preserve-and-lock"
|
submitBehavior="preserve-and-lock"
|
||||||
blurOnSubmit={true}
|
blurOnSubmit={true}
|
||||||
|
|||||||
Reference in New Issue
Block a user