Add configurable send behavior
This commit is contained in:
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user