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
@@ -66,6 +66,7 @@ The webview build emits each worker as one self-contained file. VS Code webviews
|
||||
|
||||
- `bridge-settings-runtime.ts`
|
||||
- Settings read/write and OpenCode skills discovery via API for bridge consumers.
|
||||
- `settings-changes.ts` validates Enter preferences independently before writes; invalid values are omitted without dropping unrelated changes.
|
||||
|
||||
- `bridge-system-runtime.ts`
|
||||
- System/editor/provider/quota/notification/update-check message handlers.
|
||||
|
||||
@@ -5,6 +5,7 @@ import * as path from 'path';
|
||||
import * as vscode from 'vscode';
|
||||
import { BUILT_IN_SKILL_LOCATION, type DiscoveredSkill, type SkillScope, type SkillSource } from './opencodeConfig';
|
||||
import type { BridgeContext } from './bridge';
|
||||
import { enterSettingsSchema } from './settings-changes';
|
||||
|
||||
const SETTINGS_KEY = 'openchamber.settings';
|
||||
const OPENCHAMBER_SHARED_SETTINGS_PATH = path.join(os.homedir(), '.config', 'openchamber', 'settings.json');
|
||||
@@ -292,6 +293,14 @@ export const readSettings = (ctx?: BridgeContext): Record<string, unknown> => {
|
||||
export const persistSettings = async (changes: Record<string, unknown>, ctx?: BridgeContext): Promise<Record<string, unknown>> => {
|
||||
const current = readSettings(ctx);
|
||||
const restChanges = stripDerived({ ...(changes || {}) });
|
||||
const enterSettings = enterSettingsSchema.parse(restChanges);
|
||||
for (const key of ['enterToSend', 'enterToSendConfigured'] as const) {
|
||||
if (enterSettings[key] === undefined) {
|
||||
delete restChanges[key];
|
||||
} else {
|
||||
restChanges[key] = enterSettings[key];
|
||||
}
|
||||
}
|
||||
|
||||
const keysToClear = new Set<string>();
|
||||
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { describe, expect, test } from 'bun:test';
|
||||
import { enterSettingsSchema } from './settings-changes';
|
||||
|
||||
describe('VS Code Enter settings validation', () => {
|
||||
test('preserves both explicit choices', () => {
|
||||
expect(enterSettingsSchema.parse({ enterToSend: true, enterToSendConfigured: false }))
|
||||
.toEqual({ enterToSend: true, enterToSendConfigured: false });
|
||||
expect(enterSettingsSchema.parse({ enterToSend: false, enterToSendConfigured: true }))
|
||||
.toEqual({ enterToSend: false, enterToSendConfigured: true });
|
||||
});
|
||||
|
||||
test('omits invalid fields independently', () => {
|
||||
expect(enterSettingsSchema.parse({ enterToSend: 'true', enterToSendConfigured: true }))
|
||||
.toEqual({ enterToSend: undefined, enterToSendConfigured: true });
|
||||
expect(enterSettingsSchema.parse({ enterToSend: false, enterToSendConfigured: 1 }))
|
||||
.toEqual({ enterToSend: false, enterToSendConfigured: undefined });
|
||||
expect(enterSettingsSchema.parse({})).toEqual({});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,8 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
// Invalid fields are omitted independently so one malformed preference cannot
|
||||
// discard the other preference or any unrelated settings in the same write.
|
||||
export const enterSettingsSchema = z.object({
|
||||
enterToSend: z.boolean().optional().catch(undefined),
|
||||
enterToSendConfigured: z.boolean().optional().catch(undefined),
|
||||
});
|
||||
Reference in New Issue
Block a user