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:
Muhammad Zaim
2026-09-05 19:07:28 +03:00
committed by GitHub
co-authored by Bohdan Triapitsyn
parent 7ea24e3c50
commit 2bdd9af90a
31 changed files with 368 additions and 39 deletions
@@ -519,6 +519,12 @@ export const createSettingsHelpers = (dependencies) => {
if (typeof candidate.inputSpellcheckEnabled === 'boolean') {
result.inputSpellcheckEnabled = candidate.inputSpellcheckEnabled;
}
if (candidate.enterToSend === true || candidate.enterToSend === false) {
result.enterToSend = candidate.enterToSend;
}
if (candidate.enterToSendConfigured === true || candidate.enterToSendConfigured === false) {
result.enterToSendConfigured = candidate.enterToSendConfigured;
}
if (typeof candidate.showOpenCodeUpdateNotifications === 'boolean') {
result.showOpenCodeUpdateNotifications = candidate.showOpenCodeUpdateNotifications;
}
@@ -66,6 +66,14 @@ describe('settings helpers', () => {
expect(helpers.sanitizeSettingsUpdate({ draftStartersVisible: 'false' })).toEqual({});
});
it('sanitizes both Enter settings independently', () => {
const helpers = createTestHelpers();
expect(helpers.sanitizeSettingsUpdate({ enterToSend: true })).toEqual({ enterToSend: true });
expect(helpers.sanitizeSettingsUpdate({ enterToSendConfigured: false })).toEqual({ enterToSendConfigured: false });
expect(helpers.sanitizeSettingsUpdate({ enterToSend: 'true', enterToSendConfigured: 1 })).toEqual({});
});
it('sanitizes shared sidebar display preferences', () => {
const helpers = createTestHelpers();
@@ -325,6 +333,27 @@ describe('settings helpers', () => {
});
});
it('accepts and rejects invalid Enter-to-send preference values', () => {
const helpers = createTestHelpers();
expect(helpers.sanitizeSettingsUpdate({ enterToSend: true, enterToSendConfigured: true })).toEqual({
enterToSend: true,
enterToSendConfigured: true,
});
expect(helpers.sanitizeSettingsUpdate({ enterToSend: false, enterToSendConfigured: true })).toEqual({
enterToSend: false,
enterToSendConfigured: true,
});
expect(helpers.sanitizeSettingsUpdate({ enterToSend: false, enterToSendConfigured: false })).toEqual({
enterToSend: false,
enterToSendConfigured: false,
});
expect(helpers.sanitizeSettingsUpdate({ enterToSend: 'true' })).toEqual({});
expect(helpers.sanitizeSettingsUpdate({ enterToSend: 1 })).toEqual({});
expect(helpers.sanitizeSettingsUpdate({ enterToSendConfigured: 'true' })).toEqual({});
expect(helpers.sanitizeSettingsUpdate({ enterToSendConfigured: 1 })).toEqual({});
});
it('accepts dismissed OpenCode update toast version as a persisted shared setting', () => {
const helpers = createTestHelpers();