feat(ui): add composer enter-to-send toggle (#3178)
* feat(ui): add composer enter-to-send toggle and native hardware-keyboard detection Replaces the settings-page "Enter sends with a keyboard attached" checkbox with an EnterKeyToggle in the composer footer: plain Enter submits / Shift+Enter inserts a newline when enabled, Shift+Enter submits / Enter inserts a newline when disabled. Ctrl/Cmd+Enter always submits as the soft-keyboard fallback. Persisted as enterToSend. Adds the Android HardwareKeyboardPlugin: scans input devices for an alphabetic physical keyboard (ignoring phantom key/sensor devices), re-answers on config changes/foreground, and confirms attachment from real hardware key events. MainActivity surfaces key events to it before the WebView consumes them. The composer and draft layout start keyboard-aware instead of inferring one focus late; ComposerEditor preserves Enter modifiers through CodeMirror's deferred re-dispatch so the toggle can tell Shift/Ctrl+Enter from plain Enter. Removes the settings search entry and i18n keys for the old checkbox. * refactor(ui): keep enter-to-send branch focused * fix(ui): preserve enter-toggle taps on touch * fix(ui): preserve enter key defaults and move setting * fix(ui): keep enter setting lint-clean * fix(i18n): preserve current Turkish message parity * fix(settings): persist enter-to-send preference * fix(ui): clarify enter-to-send setting * fix(ui): apply enter preference on desktop * fix(ui): match enter setting focus mode default * test(ui): cover enter key policy matrix * fix(ui): harden deferred enter handling * fix(chat): preserve untouched Enter policy and validate settings --------- Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
This commit is contained in:
committed by
GitHub
co-authored by
Bohdan Triapitsyn
parent
7ea24e3c50
commit
2bdd9af90a
@@ -51,6 +51,7 @@ import { ModelControls } from './ModelControls';
|
||||
import { parseAgentMentions } from '@/lib/messages/agentMentions';
|
||||
import { CONTEXT_METADATA_KEY, draftFromContextPayload } from '@/lib/messages/contextParts';
|
||||
import { ComposerStatusBar } from './ComposerStatusBar';
|
||||
import { shouldSubmitEnter } from './composer/keyboardPolicy';
|
||||
import { PendingChangesBar } from './PendingChangesBar';
|
||||
import { useChatColumnSession } from './chatColumnSession';
|
||||
import { useChatSurfaceMode } from './useChatSurfaceMode';
|
||||
@@ -490,6 +491,8 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({
|
||||
const agents = getVisibleAgents();
|
||||
const isMobile = useUIStore((state) => state.isMobile);
|
||||
const hasHardwareKeyboard = useHardwareKeyboard();
|
||||
const enterToSend = useUIStore((state) => state.enterToSend);
|
||||
const enterToSendConfigured = useUIStore((state) => state.enterToSendConfigured);
|
||||
const { enabled: isTabletLayout } = useTabletLayout();
|
||||
const setImagePreviewOpen = useUIStore((state) => state.setImagePreviewOpen);
|
||||
const inputBarOffset = useUIStore((state) => state.inputBarOffset);
|
||||
@@ -1924,16 +1927,20 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle Enter/Ctrl+Enter based on selected follow-up behavior. On
|
||||
// mobile, and in desktop focus mode, plain Enter writes a newline and
|
||||
// only Cmd/Ctrl+Enter sends: both are surfaces for composing long
|
||||
// prompts, where an accidental send costs more than an extra keypress.
|
||||
const requiresModifierToSend = isMobile || isDesktopExpanded;
|
||||
if (e.key === 'Enter' && !e.shiftKey && (!requiresModifierToSend || e.ctrlKey || e.metaKey)) {
|
||||
// Preserve each surface's existing default until the user changes the
|
||||
// setting. Once configured, the choice applies consistently everywhere.
|
||||
const isCtrlEnter = e.ctrlKey || e.metaKey;
|
||||
if (e.key === 'Enter' && shouldSubmitEnter({
|
||||
isMobile,
|
||||
isDesktopExpanded,
|
||||
enterToSend,
|
||||
enterToSendConfigured,
|
||||
shiftKey: e.shiftKey,
|
||||
ctrlKey: e.ctrlKey,
|
||||
metaKey: e.metaKey,
|
||||
})) {
|
||||
e.preventDefault();
|
||||
|
||||
const isCtrlEnter = e.ctrlKey || e.metaKey;
|
||||
|
||||
// Queueing / steering only works when there's an existing busy
|
||||
// session (or an active auto-review run).
|
||||
const canQueue = !isBtwActive && inputMode === 'normal' && hasContent && currentSessionId && (currentSessionPhase !== 'idle' || autoReviewRunning);
|
||||
@@ -3248,6 +3255,7 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({
|
||||
editable={Boolean(currentSessionId || newSessionDraftOpen)}
|
||||
autoCorrect={composerAutoCorrect({ isMobile })}
|
||||
autoCapitalize={isMobile ? 'sentences' : 'none'}
|
||||
preserveDeferredEnterShift={!enterToSendConfigured || !isMobile}
|
||||
spellCheck={isMobile || inputSpellcheckEnabled}
|
||||
fillContainer={isComposerExpanded}
|
||||
maxLines={isMobile ? MAX_MOBILE_COMPOSER_LINES : MAX_VISIBLE_COMPOSER_LINES}
|
||||
|
||||
Reference in New Issue
Block a user