Skip to content

fix(frontend): don't send on Enter while an IME is composing#247

Merged
haowei2000 merged 1 commit into
developfrom
claude/ime-composition-enter
Jul 17, 2026
Merged

fix(frontend): don't send on Enter while an IME is composing#247
haowei2000 merged 1 commit into
developfrom
claude/ime-composition-enter

Conversation

@haowei2000

Copy link
Copy Markdown
Collaborator

Change description

Pressing Enter to pick a candidate from a Chinese IME sent the message instead of selecting the word — so you got the raw pinyin posted to the channel.

Cause. While an IME is composing, the browser still fires keydown for Enter; it just marks it isComposing: true, because that keystroke belongs to the candidate popup rather than to the app. MessageComposer only checked !e.shiftKey, and nothing in the codebase looked at that flag, so "confirm this word" was read as "send".

Fix. A shared isComposing() helper in frontend/src/lib/ime.ts (nativeEvent.isComposing, plus the legacy keyCode === 229 some browsers still emit instead), guarded at every Enter-to-submit text input.

In the composer the guard sits at the top of handleKeyDown rather than on the send branch alone — the @// picker also claims Enter, Arrow, and Escape, and all of those navigate the IME popup while it's open. Guarding only the send path would have left the picker stealing candidate navigation.

The same bug existed in six other Enter-to-submit inputs, all fixed here: new channel / workspace / session dialogs, the workbench file-name input, the kanban task input, and friend lookup. Password fields are deliberately left alone — browsers disable IME there, so a guard would be dead code.

Change type

  • Bug fix (fix)

Test

  • Unit tests added/updated
  • Local pytest passed in full
  • Front-end npm run build no error reported
  • When it comes to npm packages, the npm run lint / npm test / npm run build of the corresponding package has passed

Verified end-to-end against the in-cluster gateway (kind + Helm) by driving the real composer with a keydown carrying isComposing: true — exactly what the browser emits mid-composition — and A/B'd by stashing the fix:

composing Enter normal Enter
Before draft cleared — sent (the bug) sent
After draft kept — no send sent

The final send round-tripped through the gateway and rendered in the channel, and exactly one message was persisted despite dispatching both a composing Enter and a normal Enter — confirming the composing one was swallowed, not merely delayed.

No unit test added: isComposing() is a two-term predicate whose only real behaviour is the browser flag it reads, so a test would restate the implementation. The repo's vitest suite covers domain logic (yaml, registry) rather than helpers of this shape, and the behaviour that matters here was verified in a real browser instead.

Database migration

  • Does not involve database changes

Security and Release Impact

  • Does not contain .env, logs, databases, uploaded files, private keys, tokens or production configurations
  • Updated documentation when it comes to configuration, deployment, permissions, file uploads, or Agent Bridge

Reviewer notes

  • Pre-existing, unrelated: tsc --noEmit reports 2 errors in frontend/src/features/chat/PdfViewer.tsx from pdfjs API drift (RenderParameters.canvas, PDFDocumentProxy.destroy). Confirmed present on a clean tree, untouched by this PR, and they don't fail npm run build (vite build doesn't typecheck). Worth a separate fix.
  • My local node_modules was stale and missing declared deps (yaml, @codemirror/*); npm ci resolved it. The lockfile is untouched by this PR.

Related Issues

Closes #

🤖 Generated with Claude Code

Pressing Enter to pick a candidate from a Chinese (or Japanese/Korean)
IME sent the message instead, committing the raw pinyin.

While an IME is composing, the browser still fires keydown for Enter and
marks it isComposing — the keystroke belongs to the candidate popup, not
to us. Nothing checked that flag, so the composer read "confirm this
word" as "send".

Add a shared isComposing() helper (nativeEvent.isComposing, plus the
legacy keyCode 229 some browsers still emit) and guard on it. In the
composer the guard sits at the top of handleKeyDown rather than on the
send branch alone: the @// picker also claims Enter/Arrow/Escape, and
those keys navigate the IME popup while it is open.

Apply the same guard to the other Enter-to-submit text inputs that had
the identical bug: new channel/workspace/session dialogs, the workbench
file-name input, the kanban task input, and friend lookup. Password
fields are left alone — browsers disable IME there.

Verified against the in-cluster gateway by driving the real composer
with a keydown carrying isComposing: true, A/B'd by stashing the fix.
Before: draft cleared (sent). After: draft kept; a normal Enter still
sends, and exactly one message was persisted across both dispatches.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@haowei2000
haowei2000 merged commit f390da9 into develop Jul 17, 2026
5 checks passed
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.

1 participant