Fix composer send shortcut conflicts

This commit is contained in:
Mohamed Boudra
2026-06-12 13:29:51 +07:00
parent 9e9d0ee305
commit 426547bfcc
16 changed files with 152 additions and 101 deletions
+5 -30
View File
@@ -54,7 +54,7 @@ import { useDismissKeyboardOnOpen } from "@/components/ui/keyboard-dismiss";
import { useWebElementScrollbar } from "@/components/use-web-scrollbar";
import { useShortcutKeys } from "@/hooks/use-shortcut-keys";
import { useIosHardwareKeyboardSubmit } from "@/hooks/use-ios-hardware-keyboard-submit";
import { formatShortcut } from "@/utils/format-shortcut";
import { formatShortcut, type ShortcutKey } from "@/utils/format-shortcut";
import { getShortcutOs } from "@/utils/shortcut-platform";
import type { MessageInputKeyboardActionKind } from "@/keyboard/actions";
import { isImeComposingKeyboardEvent } from "@/utils/keyboard-ime";
@@ -67,7 +67,9 @@ import {
resolveVoiceAccessibilityLabel,
resolveVoiceTooltipText,
} from "./labels";
import { computeCanStartDictation } from "./state";
import { computeCanStartDictation, runAlternateSendAction, runDefaultSendAction } from "./state";
const DEFAULT_SEND_KEYS: ShortcutKey[][] = [["Enter"]];
export interface AttachmentMenuItem {
id: string;
@@ -1123,32 +1125,6 @@ function computeSendButtonState(input: SendButtonStateInput): SendButtonStateOut
return { canPressLoadingButton, isSendButtonDisabled, defaultActionQueues };
}
interface DefaultSendActionContext {
defaultSendBehavior: "interrupt" | "queue";
isAgentRunning: boolean;
onQueue: ((payload: MessagePayload) => void) | undefined;
handleSendMessage: () => void;
handleQueueMessage: () => void;
}
function runDefaultSendAction(ctx: DefaultSendActionContext): void {
if (ctx.defaultSendBehavior === "queue" && ctx.isAgentRunning && ctx.onQueue) {
ctx.handleQueueMessage();
return;
}
ctx.handleSendMessage();
}
function runAlternateSendAction(ctx: DefaultSendActionContext): void {
if (ctx.defaultSendBehavior === "queue") {
ctx.handleSendMessage();
return;
}
if (ctx.onQueue) {
ctx.handleQueueMessage();
}
}
interface ResolvedMessageInputProps {
value: string;
onChangeText: (text: string) => void;
@@ -1282,7 +1258,6 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(
const buttonIconSize = isWeb ? ICON_SIZE.md : ICON_SIZE.lg;
const toast = useToast();
const voice = useVoiceOptional();
const sendKeys = useShortcutKeys("message-input-send");
const voiceMuteToggleKeys = useShortcutKeys("voice-mute-toggle");
const dictationToggleKeys = useShortcutKeys("dictation-toggle");
const focusInputKeys = useShortcutKeys("focus-message-input");
@@ -1898,7 +1873,7 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(
isSubmitLoading={isSubmitLoading}
submitIcon={submitIcon}
buttonIconSize={buttonIconSize}
sendKeys={sendKeys}
sendKeys={DEFAULT_SEND_KEYS}
sendTooltipLabel={sendTooltipLabel}
/>
</View>
+59 -1
View File
@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { computeCanStartDictation } from "./state";
import { computeCanStartDictation, runAlternateSendAction, runDefaultSendAction } from "./state";
const connected = { isConnected: true } as never;
const disconnected = { isConnected: false } as never;
@@ -91,3 +91,61 @@ describe("computeCanStartDictation", () => {
).toBe(false);
});
});
describe("composer send behavior", () => {
function actions() {
const calls: string[] = [];
return {
calls,
handleSendMessage: () => calls.push("send"),
handleQueueMessage: () => calls.push("queue"),
onQueue: () => undefined,
};
}
it("uses Enter to interrupt and Mod+Enter to queue when interrupt is selected", () => {
const defaultAction = actions();
runDefaultSendAction({
defaultSendBehavior: "interrupt",
isAgentRunning: true,
onQueue: defaultAction.onQueue,
handleSendMessage: defaultAction.handleSendMessage,
handleQueueMessage: defaultAction.handleQueueMessage,
});
const alternateAction = actions();
runAlternateSendAction({
defaultSendBehavior: "interrupt",
isAgentRunning: true,
onQueue: alternateAction.onQueue,
handleSendMessage: alternateAction.handleSendMessage,
handleQueueMessage: alternateAction.handleQueueMessage,
});
expect(defaultAction.calls).toEqual(["send"]);
expect(alternateAction.calls).toEqual(["queue"]);
});
it("uses Enter to queue and Mod+Enter to submit when queue is selected", () => {
const defaultAction = actions();
runDefaultSendAction({
defaultSendBehavior: "queue",
isAgentRunning: true,
onQueue: defaultAction.onQueue,
handleSendMessage: defaultAction.handleSendMessage,
handleQueueMessage: defaultAction.handleQueueMessage,
});
const alternateAction = actions();
runAlternateSendAction({
defaultSendBehavior: "queue",
isAgentRunning: true,
onQueue: alternateAction.onQueue,
handleSendMessage: alternateAction.handleSendMessage,
handleQueueMessage: alternateAction.handleQueueMessage,
});
expect(defaultAction.calls).toEqual(["queue"]);
expect(alternateAction.calls).toEqual(["send"]);
});
});
+29
View File
@@ -1,4 +1,15 @@
import type { DaemonClient } from "@getpaseo/client/internal/daemon-client";
import type { MessagePayload } from "@/composer/types";
export type SendBehavior = "interrupt" | "queue";
interface SendActionContext {
defaultSendBehavior: SendBehavior;
isAgentRunning: boolean;
onQueue: ((payload: MessagePayload) => void) | undefined;
handleSendMessage: () => void;
handleQueueMessage: () => void;
}
export function computeCanStartDictation(input: {
client: DaemonClient | null;
@@ -12,3 +23,21 @@ export function computeCanStartDictation(input: {
socketConnected && readyForDictation && !input.disabled && !input.dictationUnavailableMessage
);
}
export function runDefaultSendAction(ctx: SendActionContext): void {
if (ctx.defaultSendBehavior === "queue" && ctx.isAgentRunning && ctx.onQueue) {
ctx.handleQueueMessage();
return;
}
ctx.handleSendMessage();
}
export function runAlternateSendAction(ctx: SendActionContext): void {
if (ctx.defaultSendBehavior === "queue") {
ctx.handleSendMessage();
return;
}
if (ctx.isAgentRunning && ctx.onQueue) {
ctx.handleQueueMessage();
}
}