Skip to content

fix(react-ui): don't submit on Enter while an IME composition is open - #1068

Open
iambharathpadhu wants to merge 1 commit into
thesysdev:mainfrom
iambharathpadhu:fix/composer-ime-enter-guard
Open

fix(react-ui): don't submit on Enter while an IME composition is open#1068
iambharathpadhu wants to merge 1 commit into
thesysdev:mainfrom
iambharathpadhu:fix/composer-ime-enter-guard

Conversation

@iambharathpadhu

Copy link
Copy Markdown

What

Fixes #1045.

Both built-in composers in @openuidev/react-ui submit on any Enter keydown, with no awareness of IME composition state — there is currently no composition guard anywhere in the package (grep -rn "isComposing" packages/react-ui/src returns nothing).

Windows Voice Typing holds an IME composition session open across dictation. A physical Enter therefore submits and clears the textarea, the still-open composition finalizes a moment later and fires one more onChange, and the dictated text reappears in the box the user just emptied. That is the race in the issue.

The blast radius is wider than the report suggests: every IME-based input hits the same path. For Japanese, Chinese and Korean input, Enter is the key that confirms a conversion candidate — today that keypress sends a half-converted phrase instead of committing it.

The issue reporter worked around this in their own app via AgentInterface.Composer's Mode C, patching one composer. This fixes it in the library, for both.

Changes

  • Add shouldSubmitOnEnter() in AgentInterface/_shared/utils/composerKeyboard.ts — a pure predicate that returns false while a composition is open.
    • Checks nativeEvent.isComposing, plus the keyCode === 229 sentinel, since Safari and older Chromium dispatch the composition-consumed keydown before isComposing is set.
    • Takes a narrow structural ComposerKeyDownEvent that React's KeyboardEvent<HTMLTextAreaElement> satisfies, so call sites pass the synthetic event straight through and tests build a plain object — no casts, no DOM.
  • Route both composers through it, so the condition has one source of truth instead of two copies:
    • AgentInterface/components/Composer.tsx
    • AgentInterface/components/DesktopWelcomeComposer.tsx
  • Add __tests__/composerKeyboard.test.ts covering plain Enter, Shift+Enter, non-Enter keys, isComposing, the 229 sentinel, and the Enter that follows a finished composition.

Behavior change

While a composition is open, Enter no longer sends — it closes the composition, and the next Enter sends. This is standard IME behavior, matches what assistant-ui's own composer does, and is precisely what removes the race. Non-IME typing is unaffected: Enter sends, Shift+Enter inserts a newline, exactly as before.

On the approach

I went with a shared pure helper rather than a compositionstart/compositionend ref inside each component for two reasons: it keeps the two composers from drifting apart again, and it stays testable under the package's existing setup. react-ui has no jsdom today — every test here is SSR or pure — so a component-level interaction test would mean adding a DOM environment, a larger diff than the fix itself. Happy to inline the guard in both components instead if you'd prefer that.

Test Plan

  • Verified locally
pnpm --filter @openuidev/react-ui run ci
  • lint:check — 0 errors (27 pre-existing react-hooks/exhaustive-deps warnings in genui-lib/hooks, none from these files)
  • format:check — clean
  • test40 passed across 8 files (6 new)

pnpm --filter @openuidev/react-ui typecheck reports 73 errors, but the count is byte-identical on a clean checkout of main — all in genui-lib, none in AgentInterface. It is also not part of the package's ci script, so I left it alone rather than widening this PR.

Checklist

  • I linked a related issue, if applicable
  • I updated docs/README when needed — no public API change; the helper is internal to AgentInterface
  • I considered backwards compatibility — no exported surface changes; the only behavioral difference is the IME case described above

Both built-in composers submitted on any Enter keydown, with no awareness
of IME composition state. Windows Voice Typing holds a composition session
open across dictation, so a physical Enter sent and cleared the draft, the
composition then finalized and fired one more onChange, and the dictated
text reappeared in the textarea the user had just emptied. Every CJK IME
hits the same path and sends a half-converted phrase instead of committing
it.

Extract the decision into a pure shouldSubmitOnEnter() helper that bails
while a composition is open — checking nativeEvent.isComposing plus the
keyCode 229 sentinel that Safari and older Chromium report before
isComposing is set — and route both composers through it so the condition
has a single source of truth.

The Enter that closes a composition no longer sends; the next one does.
That is standard IME behavior and is what removes the race.

Fixes thesysdev#1045

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented Aug 25, 2026

Copy link
Copy Markdown

@iambharathpadhu is attempting to deploy a commit to the thesys-devs Team on Vercel.

A member of the Team first needs to authorize it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Windows voice to text leaves text in the prompt composer

1 participant