From 09b74dea642782da2bc24ecc3fafd340de8fc982 Mon Sep 17 00:00:00 2001 From: teknium1 <127238744+teknium1@users.noreply.github.com> Date: Sun, 13 Sep 2026 19:53:02 -0700 Subject: [PATCH] fix(desktop): one IME-aware Enter helper for plugins too; guard the sites main added since - The Kanban plugin cannot import `@/lib/ime` (plugin fence), so its `./ime-enter` twin duplicated the predicate. Export `isSubmitEnter` from `@hermes/plugin-sdk` and drop the twin so one helper owns the policy. - `BoardNameField` in kanban/board-switcher.tsx (main's refactor of the create/rename board dialogs) submitted on composition Enter; guarded. - Telegram allowed-ID input and the clarify-card textarea submitted on the post-compositionend keyCode-229 Enter; both now use `isSubmitEnter`. --- .../src/app/messaging/telegram-qr-setup.tsx | 3 ++- .../components/assistant-ui/clarify-tool.tsx | 7 ++----- .../src/plugins/kanban/board-switcher.tsx | 3 ++- apps/desktop/src/plugins/kanban/board.tsx | 4 ++-- apps/desktop/src/plugins/kanban/drawer.tsx | 4 ++-- .../src/plugins/kanban/ime-enter.test.ts | 21 ------------------- apps/desktop/src/plugins/kanban/ime-enter.ts | 16 -------------- apps/desktop/src/sdk/index.ts | 4 ++++ 8 files changed, 14 insertions(+), 48 deletions(-) delete mode 100644 apps/desktop/src/plugins/kanban/ime-enter.test.ts delete mode 100644 apps/desktop/src/plugins/kanban/ime-enter.ts diff --git a/apps/desktop/src/app/messaging/telegram-qr-setup.tsx b/apps/desktop/src/app/messaging/telegram-qr-setup.tsx index 936b16ed56..f426a5db12 100644 --- a/apps/desktop/src/app/messaging/telegram-qr-setup.tsx +++ b/apps/desktop/src/app/messaging/telegram-qr-setup.tsx @@ -16,6 +16,7 @@ import { import { useI18n } from '@/i18n' import { openExternalLink } from '@/lib/external-link' import { Check, ExternalLink, QrCode, Save, X } from '@/lib/icons' +import { isSubmitEnter } from '@/lib/ime' import { cn } from '@/lib/utils' import { CREDENTIAL_CONTROL_CLASS } from '../settings/credential-key-ui' @@ -326,7 +327,7 @@ export function TelegramQrSetup({ onApplied, platform, scopeProfile }: TelegramQ className={CREDENTIAL_CONTROL_CLASS} onChange={event => setNewAllowedId(event.target.value)} onKeyDown={event => { - if (event.key === 'Enter') { + if (isSubmitEnter(event)) { event.preventDefault() addAllowedId() } diff --git a/apps/desktop/src/components/assistant-ui/clarify-tool.tsx b/apps/desktop/src/components/assistant-ui/clarify-tool.tsx index d2e6e63200..0dd7053ed9 100644 --- a/apps/desktop/src/components/assistant-ui/clarify-tool.tsx +++ b/apps/desktop/src/components/assistant-ui/clarify-tool.tsx @@ -24,6 +24,7 @@ import { Tip } from '@/components/ui/tooltip' import { useI18n } from '@/i18n' import { triggerHaptic } from '@/lib/haptics' import { CircleLetterA, Loader2, MessageQuestion } from '@/lib/icons' +import { isSubmitEnter } from '@/lib/ime' import { visibleClarifyCard } from '@/lib/keybinds/composer-focus-keys' import { cn } from '@/lib/utils' import { @@ -592,11 +593,7 @@ function ClarifyToolSinglePending({ const handleTextareaKey = useCallback( (event: KeyboardEvent) => { - if (event.nativeEvent.isComposing) { - return - } - - if (event.key === 'Enter' && !event.shiftKey) { + if (isSubmitEnter(event) && !event.shiftKey) { event.preventDefault() submitAnswer() } diff --git a/apps/desktop/src/plugins/kanban/board-switcher.tsx b/apps/desktop/src/plugins/kanban/board-switcher.tsx index 06a18e15a4..b719628352 100644 --- a/apps/desktop/src/plugins/kanban/board-switcher.tsx +++ b/apps/desktop/src/plugins/kanban/board-switcher.tsx @@ -21,6 +21,7 @@ import { DropdownMenuTrigger, host, Input, + isSubmitEnter, Select, SelectContent, SelectItem, @@ -160,7 +161,7 @@ function BoardNameField({ onChange(event.target.value)} - onKeyDown={event => event.key === 'Enter' && onEnter()} + onKeyDown={event => isSubmitEnter(event) && onEnter()} placeholder={k.boardNamePlaceholder} value={value} /> diff --git a/apps/desktop/src/plugins/kanban/board.tsx b/apps/desktop/src/plugins/kanban/board.tsx index b8eade333b..fb8ce6b605 100644 --- a/apps/desktop/src/plugins/kanban/board.tsx +++ b/apps/desktop/src/plugins/kanban/board.tsx @@ -33,6 +33,7 @@ import { formatModifierToken, host, Input, + isSubmitEnter, Loader, SearchField, Select, @@ -79,7 +80,6 @@ import { } from './api' import { BoardSwitcher } from './board-switcher' import { TaskDrawer } from './drawer' -import { shouldSubmitOnEnter } from './ime-enter' import { EMPTY_OVERRIDE, ModelOverrideField, overrideCreateFields, type TaskModelOverride } from './model-override' import { OrchestrationPanel } from './orchestration' import { columnMeta, type KanbanBoard, type KanbanTask, type TaskEstimate } from './types' @@ -684,7 +684,7 @@ function NewTaskDialog({ autoFocus onChange={event => setTitle(event.target.value)} onKeyDown={event => { - if (shouldSubmitOnEnter(event)) { + if (isSubmitEnter(event)) { event.preventDefault() void submit() } diff --git a/apps/desktop/src/plugins/kanban/drawer.tsx b/apps/desktop/src/plugins/kanban/drawer.tsx index e56151f64c..525df44547 100644 --- a/apps/desktop/src/plugins/kanban/drawer.tsx +++ b/apps/desktop/src/plugins/kanban/drawer.tsx @@ -18,6 +18,7 @@ import { DropdownMenuTrigger, ErrorState, host, + isSubmitEnter, Loader, LogView, Textarea, @@ -45,7 +46,6 @@ import { taskKey, uploadAttachment } from './api' -import { shouldSubmitOnEnter } from './ime-enter' import { ModelOverrideField, overridePatch } from './model-override' import { type Diagnostic, @@ -323,7 +323,7 @@ function CommentComposer({ className={cn('field-sizing-content max-h-40 min-h-0 resize-none', running ? 'pr-[3.5rem]' : 'pr-[5rem]')} onChange={event => setBody(event.target.value)} onKeyDown={event => { - if (shouldSubmitOnEnter(event) && !event.shiftKey) { + if (isSubmitEnter(event) && !event.shiftKey) { event.preventDefault() submit() } diff --git a/apps/desktop/src/plugins/kanban/ime-enter.test.ts b/apps/desktop/src/plugins/kanban/ime-enter.test.ts deleted file mode 100644 index b0180b69e0..0000000000 --- a/apps/desktop/src/plugins/kanban/ime-enter.test.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { describe, expect, it } from 'vitest' - -import { shouldSubmitOnEnter } from './ime-enter' - -describe('shouldSubmitOnEnter', () => { - it('submits a normal Enter keypress', () => { - expect(shouldSubmitOnEnter({ key: 'Enter', nativeEvent: {} })).toBe(true) - }) - - it('does not submit while IME composition is active', () => { - expect(shouldSubmitOnEnter({ key: 'Enter', nativeEvent: { isComposing: true } })).toBe(false) - }) - - it('does not submit Chromium composition-boundary keyCode 229', () => { - expect(shouldSubmitOnEnter({ key: 'Enter', nativeEvent: { keyCode: 229 } })).toBe(false) - }) - - it('does not submit non-Enter keys', () => { - expect(shouldSubmitOnEnter({ key: 'Escape', nativeEvent: {} })).toBe(false) - }) -}) diff --git a/apps/desktop/src/plugins/kanban/ime-enter.ts b/apps/desktop/src/plugins/kanban/ime-enter.ts deleted file mode 100644 index 53941e0f02..0000000000 --- a/apps/desktop/src/plugins/kanban/ime-enter.ts +++ /dev/null @@ -1,16 +0,0 @@ -export interface ImeKeyEvent { - key: string - nativeEvent: { - isComposing?: boolean - keyCode?: number - } -} - -/** - * Enter confirms an IME conversion before it should act as a submit shortcut. - * Chromium can report the legacy 229 keyCode around composition boundaries, - * so keep that fallback in addition to the standard isComposing signal. - */ -export function shouldSubmitOnEnter(event: ImeKeyEvent): boolean { - return event.key === 'Enter' && !event.nativeEvent.isComposing && event.nativeEvent.keyCode !== 229 -} diff --git a/apps/desktop/src/sdk/index.ts b/apps/desktop/src/sdk/index.ts index c9c066a4b3..69a8c2b04c 100644 --- a/apps/desktop/src/sdk/index.ts +++ b/apps/desktop/src/sdk/index.ts @@ -1683,6 +1683,10 @@ export { triggerHaptic as haptic } from '@/lib/haptics' export type { HermesOpenTarget } from '@/lib/hermes-open-target' /** The app's lucide icon set (RefreshCw, LayoutDashboard, Activity, …). */ export * as icons from '@/lib/icons' +/** IME-aware Enter: true only for a real submit Enter, never a CJK composition + * commit (`isComposing` or the legacy keyCode 229). Use it on every plugin + * text field whose bare Enter performs an action. */ +export { isSubmitEnter } from '@/lib/ime' export { type KeybindContribution, KEYBINDS_AREA } from '@/lib/keybinds/actions' export { formatModifierToken } from '@/lib/keybinds/combo' /** A `Map` with a ceiling, for the module-level caches a plugin keeps across