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
@@ -210,6 +210,7 @@ const ChatSectionContent: React.FC = () => {
|
||||
'persistDraft',
|
||||
'inputSpellcheck',
|
||||
'largeTextPaste',
|
||||
'enterToSend',
|
||||
]}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -283,7 +283,7 @@ const normalizeUserMessageRenderingMode = (mode: unknown): 'markdown' | 'plain'
|
||||
return mode === 'markdown' ? 'markdown' : 'plain';
|
||||
};
|
||||
|
||||
type VisibleSetting = 'sessionAssist' | 'sessionGoal' | 'theme' | 'windowControlsPosition' | 'pwaInstallName' | 'pwaOrientation' | 'mobileKeyboardMode' | 'timeFormat' | 'weekStart' | 'fontSize' | 'terminalFontSize' | 'terminalShell' | 'terminalLoginShell' | 'editorFontSize' | 'spacing' | 'inputBarOffset' | 'mermaidRendering' | 'userMessageRendering' | 'chatRenderMode' | 'messageTransport' | 'activityRenderMode' | 'collapsibleUserMessages' | 'stickyUserHeader' | 'promptNavigatorEnabled' | 'wideChatLayout' | 'codeBlockLineWrap' | 'splitAssistantMessageActions' | 'subagentReadOnlyBanner' | 'diffLayout' | 'mobileStatusBar' | 'dotfiles' | 'fileViewerPreview' | 'reasoning' | 'showToolFileIcons' | 'showTurnChangedFiles' | 'expandedTools' | 'followUpBehavior' | 'terminalQuickKeys' | 'fileEditorKeymap' | 'persistDraft' | 'inputSpellcheck' | 'largeTextPaste' | 'reportUsage' | 'autoSaveEnabled' | 'sessionTabs';
|
||||
type VisibleSetting = 'sessionAssist' | 'sessionGoal' | 'theme' | 'windowControlsPosition' | 'pwaInstallName' | 'pwaOrientation' | 'mobileKeyboardMode' | 'timeFormat' | 'weekStart' | 'fontSize' | 'terminalFontSize' | 'terminalShell' | 'terminalLoginShell' | 'editorFontSize' | 'spacing' | 'inputBarOffset' | 'mermaidRendering' | 'userMessageRendering' | 'chatRenderMode' | 'messageTransport' | 'activityRenderMode' | 'collapsibleUserMessages' | 'stickyUserHeader' | 'promptNavigatorEnabled' | 'wideChatLayout' | 'codeBlockLineWrap' | 'splitAssistantMessageActions' | 'subagentReadOnlyBanner' | 'diffLayout' | 'mobileStatusBar' | 'dotfiles' | 'fileViewerPreview' | 'reasoning' | 'showToolFileIcons' | 'showTurnChangedFiles' | 'expandedTools' | 'followUpBehavior' | 'terminalQuickKeys' | 'fileEditorKeymap' | 'persistDraft' | 'inputSpellcheck' | 'largeTextPaste' | 'enterToSend' | 'reportUsage' | 'autoSaveEnabled' | 'sessionTabs';
|
||||
|
||||
const WINDOW_CONTROLS_POSITION_OPTIONS: Array<{ id: DesktopWindowControlsPosition; labelKey: string }> = [
|
||||
{ id: 'left', labelKey: 'settings.openchamber.desktopNetwork.option.windowControlsLeft' },
|
||||
@@ -380,6 +380,11 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
|
||||
const setInputSpellcheckEnabled = useUIStore(state => state.setInputSpellcheckEnabled);
|
||||
const largeTextPasteBehavior = useUIStore(state => state.largeTextPasteBehavior);
|
||||
const setLargeTextPasteBehavior = useUIStore(state => state.setLargeTextPasteBehavior);
|
||||
const enterToSend = useUIStore(state => state.enterToSend);
|
||||
const setEnterToSend = useUIStore(state => state.setEnterToSend);
|
||||
const enterToSendConfigured = useUIStore(state => state.enterToSendConfigured);
|
||||
const setEnterToSendConfigured = useUIStore(state => state.setEnterToSendConfigured);
|
||||
const isExpandedInput = useUIStore(state => state.isExpandedInput);
|
||||
const showToolFileIcons = useUIStore(state => state.showToolFileIcons);
|
||||
const setShowToolFileIcons = useUIStore(state => state.setShowToolFileIcons);
|
||||
const showTurnChangedFiles = useUIStore(state => state.showTurnChangedFiles);
|
||||
@@ -539,6 +544,12 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
|
||||
void updateDesktopSettings({ inputSpellcheckEnabled: enabled });
|
||||
}, [setInputSpellcheckEnabled]);
|
||||
|
||||
const handleEnterToSendChange = React.useCallback((enabled: boolean) => {
|
||||
setEnterToSend(enabled);
|
||||
setEnterToSendConfigured(true);
|
||||
void updateDesktopSettings({ enterToSend: enabled, enterToSendConfigured: true });
|
||||
}, [setEnterToSend, setEnterToSendConfigured]);
|
||||
|
||||
const handleChatRenderModeChange = React.useCallback((mode: 'sorted' | 'live') => {
|
||||
setChatRenderMode(mode);
|
||||
void updateDesktopSettings({ chatRenderMode: mode });
|
||||
@@ -660,7 +671,8 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
|
||||
|| shouldShow('largeTextPaste')
|
||||
|| shouldShow('showToolFileIcons')
|
||||
|| shouldShow('expandedTools')
|
||||
|| (!isMobile && shouldShow('inputSpellcheck'));
|
||||
|| (!isMobile && shouldShow('inputSpellcheck'))
|
||||
|| shouldShow('enterToSend');
|
||||
const showBehaviorDisplaySettings = shouldShow('chatRenderMode')
|
||||
|| (shouldShow('activityRenderMode') && chatRenderMode === 'sorted');
|
||||
const showTransportSection = shouldShow('messageTransport');
|
||||
@@ -684,6 +696,7 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
|
||||
|| shouldShow('showToolFileIcons')
|
||||
|| shouldShow('showTurnChangedFiles')
|
||||
|| (!isMobile && shouldShow('inputSpellcheck'))
|
||||
|| shouldShow('enterToSend')
|
||||
|| shouldShow('reasoning')
|
||||
|| shouldShow('expandedTools');
|
||||
// First behavior section under the page header should not draw a top border on Chat-only;
|
||||
@@ -1457,7 +1470,7 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
|
||||
/>
|
||||
))}
|
||||
</SettingsRadioGroup>
|
||||
</SettingsControlGroup>
|
||||
</SettingsControlGroup>
|
||||
)}
|
||||
<div className={SETTINGS_OPTION_STACK_CLASS}>
|
||||
{shouldShow('autoSaveEnabled') && (
|
||||
@@ -2001,7 +2014,7 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
|
||||
</SettingsSection>
|
||||
)}
|
||||
|
||||
{(shouldShow('persistDraft') || shouldShow('largeTextPaste') || (!isMobile && shouldShow('inputSpellcheck'))) && (
|
||||
{(shouldShow('persistDraft') || shouldShow('largeTextPaste') || (!isMobile && shouldShow('inputSpellcheck')) || shouldShow('enterToSend')) && (
|
||||
<SettingsSection
|
||||
title={t('settings.openchamber.visual.section.composer')}
|
||||
settingsItem="chat.composer"
|
||||
@@ -2044,7 +2057,17 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
|
||||
/>
|
||||
))}
|
||||
</SettingsRadioGroup>
|
||||
</SettingsControlGroup>
|
||||
</SettingsControlGroup>
|
||||
)}
|
||||
{shouldShow('enterToSend') && (
|
||||
<SettingsCheckboxRow
|
||||
checked={enterToSendConfigured ? enterToSend : !isMobile && !isExpandedInput}
|
||||
onChange={handleEnterToSendChange}
|
||||
label={t('chat.chatInput.actions.enterToSend')}
|
||||
info={t('chat.chatInput.actions.enterToSendHint')}
|
||||
ariaLabel={t('chat.chatInput.actions.enterToSend')}
|
||||
settingsItem="chat.enter-to-send"
|
||||
/>
|
||||
)}
|
||||
</SettingsSection>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user