Add configurable send behavior

This commit is contained in:
Mohamed Boudra
2026-04-09 18:56:09 +07:00
parent ce77947654
commit 74f7b9e416
4 changed files with 111 additions and 30 deletions
+4
View File
@@ -48,6 +48,7 @@ import { useKeyboardShiftStyle } from "@/hooks/use-keyboard-shift-style";
import { useKeyboardActionHandler } from "@/hooks/use-keyboard-action-handler";
import type { KeyboardActionDefinition } from "@/keyboard/keyboard-action-dispatcher";
import { submitAgentInput } from "@/components/agent-input-submit";
import { useAppSettings } from "@/hooks/use-settings";
type QueuedMessage = {
id: string;
@@ -139,6 +140,8 @@ export function Composer({
agentDirectoryStatus === "revalidating" ||
agentDirectoryStatus === "error_after_ready");
const { settings: appSettings } = useAppSettings();
const agentState = useSessionStore(
useShallow((state) => {
const agent = state.sessions[serverId]?.agents?.get(agentId) ?? null;
@@ -760,6 +763,7 @@ export function Composer({
voiceServerId={serverId}
voiceAgentId={agentId}
isAgentRunning={isAgentRunning}
defaultSendBehavior={appSettings.sendBehavior}
onQueue={handleQueue}
onSubmitLoadingPress={isAgentRunning ? handleCancelAgent : undefined}
onKeyPress={handleCommandKeyPress}
+56 -17
View File
@@ -90,6 +90,9 @@ export interface MessageInputProps {
voiceAgentId?: string;
/** When true and there's sendable content, calls onQueue instead of onSubmit */
isAgentRunning?: boolean;
/** Controls what the default send action (Enter, send button, dictation) does
* when the agent is running. "interrupt" sends immediately, "queue" queues. */
defaultSendBehavior?: "interrupt" | "queue";
/** Callback for queue button when agent is running */
onQueue?: (payload: MessagePayload) => void;
/** Optional handler used when submit button is in loading state. */
@@ -217,6 +220,7 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(funct
voiceServerId,
voiceAgentId,
isAgentRunning = false,
defaultSendBehavior = "interrupt",
onQueue,
onSubmitLoadingPress,
onKeyPress: onKeyPressCallback,
@@ -362,11 +366,17 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(funct
if (shouldAutoSend) {
const imageAttachments = images.length > 0 ? images : undefined;
onSubmit({
text: nextValue,
images: imageAttachments,
forceSend: isAgentRunning || undefined,
});
// Respect send behavior setting: when "queue", dictation queues too.
if (defaultSendBehavior === "queue" && isAgentRunning && onQueue) {
onQueue({ text: nextValue, images: imageAttachments });
onChangeText("");
} else {
onSubmit({
text: nextValue,
images: imageAttachments,
forceSend: isAgentRunning || undefined,
});
}
} else {
onChangeText(nextValue);
}
@@ -377,7 +387,7 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(funct
});
}
},
[onChangeText, onSubmit, images, isAgentRunning],
[onChangeText, onSubmit, onQueue, images, isAgentRunning, defaultSendBehavior],
);
const handleDictationError = useCallback(
@@ -588,6 +598,26 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(funct
onHeightChange?.(MIN_INPUT_HEIGHT);
}, [value, images, onQueue, onChangeText, onHeightChange]);
// Default send action: respects the sendBehavior setting.
// When "interrupt" (default), primary action sends immediately (interrupts).
// When "queue", primary action queues when agent is running.
const handleDefaultSendAction = useCallback(() => {
if (defaultSendBehavior === "queue" && isAgentRunning && onQueue) {
handleQueueMessage();
} else {
handleSendMessage();
}
}, [defaultSendBehavior, isAgentRunning, onQueue, handleQueueMessage, handleSendMessage]);
// Alternate send action: always the opposite of the default.
const handleAlternateSendAction = useCallback(() => {
if (defaultSendBehavior === "queue") {
handleSendMessage(); // interrupt
} else if (onQueue) {
handleQueueMessage(); // queue
}
}, [defaultSendBehavior, handleSendMessage, handleQueueMessage, onQueue]);
// Web input height measurement
function isTextAreaLike(v: unknown): v is TextAreaHandle {
return typeof v === "object" && v !== null && "scrollHeight" in v;
@@ -882,18 +912,18 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(funct
// Shift+Enter: add newline (default behavior, don't intercept)
if (shiftKey) return;
// Cmd+Enter (Mac) or Ctrl+Enter (Windows/Linux): queue when agent is running
// Cmd+Enter (Mac) or Ctrl+Enter (Windows/Linux): alternate action
if ((metaKey || ctrlKey) && isAgentRunning && onQueue) {
if (isSubmitDisabled || isSubmitLoading || disabled) return;
event.preventDefault();
handleQueueMessage();
handleAlternateSendAction();
return;
}
// Enter: send (interrupts agent if running)
// Enter: default send action (interrupt or queue, based on setting)
if (isSubmitDisabled || isSubmitLoading || disabled) return;
event.preventDefault();
handleSendMessage();
handleDefaultSendAction();
}
const hasImages = images.length > 0;
@@ -904,11 +934,14 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(funct
const canPressLoadingButton = isSubmitLoading && typeof onSubmitLoadingPress === "function";
const isSendButtonDisabled =
disabled || (!canPressLoadingButton && (isSubmitDisabled || isSubmitLoading));
const defaultActionQueues = defaultSendBehavior === "queue" && isAgentRunning;
const submitAccessibilityLabel = canPressLoadingButton
? "Interrupt agent"
: isAgentRunning
? "Send and interrupt"
: "Send message";
: defaultActionQueues
? "Queue message"
: isAgentRunning
? "Send and interrupt"
: "Send message";
const handleInputChange = useCallback(
(nextValue: string) => {
@@ -1087,10 +1120,10 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(funct
</TooltipContent>
</Tooltip>
{rightContent}
{hasSendableContent && isAgentRunning && onQueue && (
{hasSendableContent && isAgentRunning && onQueue && !defaultActionQueues && (
<Tooltip delayDuration={0} enabledOnDesktop enabledOnMobile={false}>
<TooltipTrigger
onPress={handleQueueMessage}
onPress={handleAlternateSendAction}
disabled={!isConnected || disabled}
accessibilityLabel="Queue message"
accessibilityRole="button"
@@ -1115,7 +1148,7 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(funct
{shouldShowSendButton && (
<Tooltip delayDuration={0} enabledOnDesktop enabledOnMobile={false}>
<TooltipTrigger
onPress={canPressLoadingButton ? onSubmitLoadingPress : handleSendMessage}
onPress={canPressLoadingButton ? onSubmitLoadingPress : handleDefaultSendAction}
disabled={isSendButtonDisabled}
accessibilityLabel={showEmptySubmitLabel ? emptySubmitLabel : submitAccessibilityLabel}
accessibilityRole="button"
@@ -1134,7 +1167,13 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(funct
</TooltipTrigger>
<TooltipContent side="top" align="center" offset={8}>
<View style={styles.tooltipRow}>
<Text style={styles.tooltipText}>{showEmptySubmitLabel ? emptySubmitLabel : "Send"}</Text>
<Text style={styles.tooltipText}>
{showEmptySubmitLabel
? emptySubmitLabel
: defaultActionQueues
? "Queue"
: "Send"}
</Text>
{sendKeys ? <Shortcut chord={sendKeys} style={styles.tooltipShortcut} /> : null}
</View>
</TooltipContent>