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
+2
View File
@@ -724,6 +724,8 @@ export interface SettingsPayload {
queueModeEnabled?: boolean;
gitmojiEnabled?: boolean;
inputSpellcheckEnabled?: boolean;
enterToSend?: boolean;
enterToSendConfigured?: boolean;
showOpenCodeUpdateNotifications?: boolean;
openCodeUpdateToastDismissedVersion?: string;
showToolFileIcons?: boolean;
+2
View File
@@ -159,6 +159,8 @@ export type DesktopSettings = {
desktopWindowControlsPosition?: DesktopWindowControlsPosition;
desktopWindowControlsStyle?: DesktopWindowControlsStyle;
inputSpellcheckEnabled?: boolean;
enterToSend?: boolean;
enterToSendConfigured?: boolean;
showOpenCodeUpdateNotifications?: boolean;
agentControlToolEnabled?: boolean;
agentWebToolEnabled?: boolean;
+2
View File
@@ -2105,6 +2105,8 @@ export const dict = {
'chat.chatInput.actions.linkGithubPr': 'GitHub-PR verknüpfen',
'chat.chatInput.actions.modelAgentSettings': 'Modell- und Agenteneinstellungen',
'chat.chatInput.actions.sendMessageAria': 'Nachricht senden',
'chat.chatInput.actions.enterToSend': 'Enter sendet',
'chat.chatInput.actions.enterToSendHint': 'Nach der Änderung steuern Enter und Shift+Enter das Verhalten auf jeder Oberfläche. Bis dahin behält jede Oberfläche ihr bestehendes Verhalten bei.',
'chat.chatInput.actions.queueMessageAria': 'Nachricht in die Warteschlange stellen',
'chat.chatInput.actions.stopGeneratingAria': 'Generierung stoppen',
'chat.chatInput.focusMode.toggleAria': 'Fokusmodus umschalten',
+2
View File
@@ -2322,6 +2322,8 @@ export const dict = {
'chat.chatInput.actions.linkGithubPr': 'Link GitHub PR',
'chat.chatInput.actions.modelAgentSettings': 'Model and agent settings',
'chat.chatInput.actions.sendMessageAria': 'Send message',
'chat.chatInput.actions.enterToSend': 'Enter sends',
'chat.chatInput.actions.enterToSendHint': 'Once changed, this controls Enter and Shift+Enter on every surface. Until then, each surface keeps its existing behavior.',
'chat.chatInput.actions.queueMessageAria': 'Queue message',
'chat.chatInput.actions.stopGeneratingAria': 'Stop generating',
'chat.chatInput.focusMode.toggleAria': 'Toggle focus mode',
+2
View File
@@ -2300,6 +2300,8 @@ export const dict: Record<I18nKey, string> = {
"chat.chatInput.actions.linkGithubPr": "Vincular PR de GitHub",
"chat.chatInput.actions.modelAgentSettings": "Configuración del modelo y agente",
"chat.chatInput.actions.sendMessageAria": "Enviar mensaje",
'chat.chatInput.actions.enterToSend': 'Enter envía',
'chat.chatInput.actions.enterToSendHint': 'Después de cambiarlo, controla Enter y Shift+Enter en todas las superficies. Hasta entonces, cada superficie mantiene su comportamiento actual.',
"chat.chatInput.actions.queueMessageAria": "Poner mensaje en cola",
"chat.chatInput.actions.stopGeneratingAria": "Detener generación",
"chat.chatInput.focusMode.toggleAria": "Activar o desactivar modo de enfoque",
+2
View File
@@ -2041,6 +2041,8 @@ export const dict = {
'chat.chatInput.actions.linkGithubPr': 'Lien GitHub PR',
'chat.chatInput.actions.modelAgentSettings': 'Paramètres du modèle et de l\'agent',
'chat.chatInput.actions.sendMessageAria': 'Envoyer un message',
'chat.chatInput.actions.enterToSend': 'Entrée envoie',
'chat.chatInput.actions.enterToSendHint': 'Après modification, ce réglage contrôle Entrée et Maj+Entrée sur toutes les surfaces. En attendant, chaque surface conserve son comportement actuel.',
'chat.chatInput.actions.queueMessageAria': 'Message de file d\'attente',
'chat.chatInput.actions.stopGeneratingAria': 'Arrêter de générer',
'chat.chatInput.focusMode.toggleAria': 'Basculer le mode de mise au point',
+2
View File
@@ -2318,6 +2318,8 @@ export const dict: Record<I18nKey, string> = {
'chat.chatInput.actions.linkGithubPr': 'GitHub PRをリンク',
'chat.chatInput.actions.modelAgentSettings': 'モデルとエージェント設定',
'chat.chatInput.actions.sendMessageAria': 'メッセージを送信',
'chat.chatInput.actions.enterToSend': 'Enterで送信',
'chat.chatInput.actions.enterToSendHint': '変更すると、すべての環境でEnterとShift+Enterの動作を制御します。変更するまでは、各環境の既存の動作が維持されます。',
'chat.chatInput.actions.queueMessageAria': 'メッセージをキュー',
'chat.chatInput.actions.stopGeneratingAria': '生成を停止',
'chat.chatInput.focusMode.toggleAria': 'フォーカスモードの切り替え',
+2
View File
@@ -2322,6 +2322,8 @@ export const dict: Record<I18nKey, string> = {
'chat.chatInput.actions.linkGithubPr': 'GitHub PR 연결',
'chat.chatInput.actions.modelAgentSettings': '모델 및 에이전트 설정',
'chat.chatInput.actions.sendMessageAria': '보내기 메시지',
'chat.chatInput.actions.enterToSend': 'Enter로 전송',
'chat.chatInput.actions.enterToSendHint': '변경하면 모든 환경에서 Enter와 Shift+Enter의 동작을 제어합니다. 변경하기 전에는 각 환경의 기존 동작이 유지됩니다.',
'chat.chatInput.actions.queueMessageAria': '메시지 대기열에 추가',
'chat.chatInput.actions.stopGeneratingAria': '생성 중지',
'chat.chatInput.focusMode.toggleAria': '집중 모드 전환',
+2
View File
@@ -1255,6 +1255,8 @@ export const dict: Record<I18nKey, string> = {
'chat.chatInput.actions.modelAgentSettings': 'Model and agent settings',
'chat.chatInput.actions.queueMessageAria': 'Queue message',
'chat.chatInput.actions.sendMessageAria': 'Send message',
'chat.chatInput.actions.enterToSend': 'Enter wysyła',
'chat.chatInput.actions.enterToSendHint': 'Po zmianie ustawienie steruje działaniem klawiszy Enter i Shift+Enter na każdej powierzchni. Do tego czasu każda powierzchnia zachowuje dotychczasowe działanie.',
'chat.chatInput.actions.stopGeneratingAria': 'Stop generating',
'chat.chatInput.branch': 'Gałąź',
'chat.chatInput.draftPicker.projectTitle': 'Projekt',
@@ -2300,6 +2300,8 @@ export const dict: Record<I18nKey, string> = {
"chat.chatInput.actions.linkGithubPr": "Vincular PR de GitHub",
"chat.chatInput.actions.modelAgentSettings": "Configurações de modelo e agente",
"chat.chatInput.actions.sendMessageAria": "Enviar mensagem",
'chat.chatInput.actions.enterToSend': 'Enter envia',
'chat.chatInput.actions.enterToSendHint': 'Depois de alterada, esta opção controla Enter e Shift+Enter em todas as superfícies. Até lá, cada superfície mantém seu comportamento atual.',
"chat.chatInput.actions.queueMessageAria": "Colocar mensagem na fila",
"chat.chatInput.actions.stopGeneratingAria": "Parar geração",
"chat.chatInput.focusMode.toggleAria": "Ativar ou desativar modo de foco",
+2
View File
@@ -2263,6 +2263,8 @@ export const dict = {
'chat.chatInput.actions.linkGithubPr': 'GitHub PR\'yi bağla',
'chat.chatInput.actions.modelAgentSettings': 'Model ve agent ayarları',
'chat.chatInput.actions.sendMessageAria': 'Mesaj gönder',
'chat.chatInput.actions.enterToSend': 'Enter gönderir',
'chat.chatInput.actions.enterToSendHint': 'Değiştirildikten sonra Enter ve Shift+Enter davranışını tüm yüzeylerde kontrol eder. O zamana kadar her yüzey mevcut davranışını korur.',
'chat.chatInput.actions.queueMessageAria': 'Mesajı kuyruğa ekle',
'chat.chatInput.actions.stopGeneratingAria': 'Üretmeyi durdur',
'chat.chatInput.focusMode.toggleAria': 'Odak modunu aç/kapat',
+2
View File
@@ -2300,6 +2300,8 @@ export const dict: Record<I18nKey, string> = {
"chat.chatInput.actions.linkGithubPr": "Пов’язати GitHub PR",
"chat.chatInput.actions.modelAgentSettings": "Параметри моделі та агента",
"chat.chatInput.actions.sendMessageAria": "Надіслати повідомлення",
'chat.chatInput.actions.enterToSend': 'Enter надсилає',
'chat.chatInput.actions.enterToSendHint': 'Після зміни цей параметр керує поведінкою Enter і Shift+Enter на всіх поверхнях. До цього кожна поверхня зберігає свою поточну поведінку.',
"chat.chatInput.actions.queueMessageAria": "Поставити повідомлення в чергу",
"chat.chatInput.actions.stopGeneratingAria": "Припинити генерацію",
"chat.chatInput.focusMode.toggleAria": "Перемкнути режим фокусування",
@@ -2288,6 +2288,8 @@ export const dict: Record<I18nKey, string> = {
'chat.chatInput.actions.linkGithubPr': '关联 GitHub PR',
'chat.chatInput.actions.modelAgentSettings': '模型与智能体设置',
'chat.chatInput.actions.sendMessageAria': '发送消息',
'chat.chatInput.actions.enterToSend': 'Enter 发送',
'chat.chatInput.actions.enterToSendHint': '更改后,此设置会控制所有界面中的 Enter 和 Shift+Enter。更改前,各界面保持现有行为。',
'chat.chatInput.actions.queueMessageAria': '将消息加入队列',
'chat.chatInput.actions.stopGeneratingAria': '停止生成',
'chat.chatInput.focusMode.toggleAria': '切换专注模式',
@@ -2292,6 +2292,8 @@ export const dict: Record<I18nKey, string> = {
'chat.chatInput.actions.linkGithubPr': '關聯 GitHub PR',
'chat.chatInput.actions.modelAgentSettings': '模型與 Agent 設定',
'chat.chatInput.actions.sendMessageAria': '傳送訊息',
'chat.chatInput.actions.enterToSend': 'Enter 傳送',
'chat.chatInput.actions.enterToSendHint': '變更後,此設定會控制所有介面中的 Enter 與 Shift+Enter。變更前,各介面會維持現有行為。',
'chat.chatInput.actions.queueMessageAria': '將訊息加入佇列',
'chat.chatInput.actions.stopGeneratingAria': '停止生成',
'chat.chatInput.focusMode.toggleAria': '切換專注模式',
+10
View File
@@ -261,6 +261,16 @@ describe('updateDesktopSettings', () => {
}
});
test('applies enterToSendConfigured when it arrives without enterToSend', async () => {
useUIStore.setState({ enterToSend: false, enterToSendConfigured: false });
registerSettingsSave(async () => ({ enterToSendConfigured: true }));
await updateDesktopSettings({ enterToSendConfigured: true });
expect(useUIStore.getState().enterToSend).toBe(false);
expect(useUIStore.getState().enterToSendConfigured).toBe(true);
});
test('reports an error without applying a malformed fallback settings response', async () => {
const previousFetch = globalThis.fetch;
const fallbackFetch: typeof fetch = async () => new Response(JSON.stringify('ok'), {
+18
View File
@@ -574,6 +574,8 @@ const materializeAuthoritativeUiSettings = (settings: DesktopSettings): DesktopS
summaryLength: defaults.summaryLength,
maxLastMessageLength: defaults.maxLastMessageLength,
inputSpellcheckEnabled: defaults.inputSpellcheckEnabled,
enterToSend: defaults.enterToSend,
enterToSendConfigured: defaults.enterToSendConfigured,
showOpenCodeUpdateNotifications: defaults.showOpenCodeUpdateNotifications,
agentControlToolEnabled: defaults.agentControlToolEnabled,
agentWebToolEnabled: defaults.agentWebToolEnabled,
@@ -745,6 +747,16 @@ const applyDesktopUiPreferences = (settings: DesktopSettings) => {
if (typeof settings.inputSpellcheckEnabled === 'boolean' && settings.inputSpellcheckEnabled !== store.inputSpellcheckEnabled) {
store.setInputSpellcheckEnabled(settings.inputSpellcheckEnabled);
}
if (settings.enterToSend === true || settings.enterToSend === false) {
if (settings.enterToSend !== store.enterToSend) {
store.setEnterToSend(settings.enterToSend);
}
}
if (settings.enterToSendConfigured === true || settings.enterToSendConfigured === false) {
if (settings.enterToSendConfigured !== store.enterToSendConfigured) {
store.setEnterToSendConfigured(settings.enterToSendConfigured);
}
}
if (
typeof settings.showOpenCodeUpdateNotifications === 'boolean'
&& settings.showOpenCodeUpdateNotifications !== store.showOpenCodeUpdateNotifications
@@ -1457,6 +1469,12 @@ const sanitizeWebSettings = (payload: unknown): DesktopSettings | null => {
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;
}
+6
View File
@@ -367,6 +367,12 @@ const SETTINGS_SEARCH_ITEMS: readonly SettingsSearchItem[] = [
descriptionKey: 'settings.openchamber.visual.field.largeTextPasteHint',
keywords: ['paste', 'clipboard', 'attachment', 'large', 'text', 'file'],
},
{
id: 'chat.enter-to-send',
page: 'chat',
titleKey: 'chat.chatInput.actions.enterToSend',
keywords: ['enter', 'shift enter', 'send', 'newline'],
},
{
id: 'sessions.default-model',
page: 'sessions',