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
@@ -724,6 +724,8 @@ export interface SettingsPayload {
|
||||
queueModeEnabled?: boolean;
|
||||
gitmojiEnabled?: boolean;
|
||||
inputSpellcheckEnabled?: boolean;
|
||||
enterToSend?: boolean;
|
||||
enterToSendConfigured?: boolean;
|
||||
showOpenCodeUpdateNotifications?: boolean;
|
||||
openCodeUpdateToastDismissedVersion?: string;
|
||||
showToolFileIcons?: boolean;
|
||||
|
||||
@@ -159,6 +159,8 @@ export type DesktopSettings = {
|
||||
desktopWindowControlsPosition?: DesktopWindowControlsPosition;
|
||||
desktopWindowControlsStyle?: DesktopWindowControlsStyle;
|
||||
inputSpellcheckEnabled?: boolean;
|
||||
enterToSend?: boolean;
|
||||
enterToSendConfigured?: boolean;
|
||||
showOpenCodeUpdateNotifications?: boolean;
|
||||
agentControlToolEnabled?: boolean;
|
||||
agentWebToolEnabled?: boolean;
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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': 'フォーカスモードの切り替え',
|
||||
|
||||
@@ -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': '집중 모드 전환',
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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': '切換專注模式',
|
||||
|
||||
@@ -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'), {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user