Skip to content

fix(web): guard Korean IME Enter composition in text inputs - #350

Draft
Hwang, Gunn Gu (kuil09) wants to merge 1 commit into
JetBrains:mainfrom
kuil09:pr-2
Draft

fix(web): guard Korean IME Enter composition in text inputs#350
Hwang, Gunn Gu (kuil09) wants to merge 1 commit into
JetBrains:mainfrom
kuil09:pr-2

Conversation

@kuil09

@kuil09 Hwang, Gunn Gu (kuil09) commented Aug 29, 2026

Copy link
Copy Markdown

Summary

Fix Korean IME Enter composition bug where pressing Enter during Korean character composition triggers premature submission and duplicate/leftover character insertion.

Background: How Korean IME Works

Unlike Western alphabets, Korean uses an Input Method Editor (IME) where characters are composed syllable-by-syllable:

  1. Jamo (자모) - Individual consonants (ㄱ, ㄴ, ㄷ...) and vowels (ㅏ, ㅑ, ㅓ...) are typed
  2. Composition - The IME combines them in real-time into a syllable block (e.g., ㄱ + ㅏ + ㄴ → "간")
  3. Commit - When the user presses Space or Enter, the composed syllable is "committed" (finalized)

The critical difference: In Korean IME, pressing Enter while a syllable is still being composed (e.g., after typing just "ㄱ" or "가" but before Space) is a valid commit action — it finalizes the current syllable. This is expected Korean IME behavior.

The Bug

Our keydown handlers checked e.key === "Enter" without checking e.nativeEvent.isComposing. This caused:

  1. Premature submission — The app treated the Enter press as "send message" before the IME had a chance to commit the syllable
  2. Duplicate/leftover character — React's controlled input was cleared on submit, but the browser's subsequent compositionend event re-inserted the committed character into the now-empty input
  3. Unintended menu activation — Enter during composition also triggered slash commands, mentions, history recall, etc.

Changes

Added isComposing guards to all keydown handlers across 12 text input components:

  • Composer.tsx - main chat composer
  • SlashCommandCompletion.tsx - slash command autocomplete
  • TodoList.tsx - todo add input
  • HistoryOverlay.tsx - history search input
  • NewWorkspaceDialog.tsx - workspace creation prompt
  • LoginDialog.tsx - login prompt
  • ExtUiDialog.tsx - extension UI input
  • PreviewCommenting.tsx - review comment composer
  • ReviewThreadCard.tsx - review thread edit
  • reviewWidgets.ts - Monaco-based review widgets
  • LayoutSettings.tsx - layout preset rename
  • useGlobalHotkeys.ts - global hotkeys

Documentation: Added IME composition safety invariant to apps/web/src/chat/SPEC.md

Tests: Added unit test in SlashCommandCompletion.test.ts for IME guard

Bonus fix: Fixed scripts/check-catalog.ts Bun JSONC parsing issue

Testing

All verification gates pass:

  • bun run check:deps - OK (18 catalog entries enforced)
  • bun run check:boundaries - OK (8 module boundaries enforced)
  • bun run check:seams - OK (4 known opaque imports, all handled)
  • bun run lint - OK (6 pre-existing biome warnings, no new ones)
  • bun run typecheck - OK (14 packages, 0 errors)
  • bun --filter @thinkrail/web test - 812 tests pass
  • All other package unit tests pass (272 tests across pi-* packages)

@rsolmano Rinat S (rsolmano) left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One additional affected path is outside this diff: AskUserQuestionCard.tsx intercepts unmodified ArrowUp/ArrowDown in the Other-answer input before checking composition, so those keys can move questionnaire focus instead of the IME candidate selection. Its Enter branch also relies only on isComposing.

};

const onKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => {
if (e.nativeEvent.isComposing) return;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This still misses the composition-confirming Enter emitted by older Safari/WKWebView. In that event order, isComposing is already false while keyCode remains 229. In a real-host browser probe with that event shape, the draft cleared and the agent started, so the core premature-submit bug remains for those runtimes.

if (!open) return null;

const onKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
if (e.nativeEvent.isComposing) return;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This input-level guard cannot protect composition Escape: the window capture listener above runs first and closes the overlay. I reproduced this with isComposing=true; History closed and focus returned to the composer.

send.addEventListener("click", () => submit(callbacks.onSend));
cancel.addEventListener("click", closeComposer);
textarea.addEventListener("keydown", (e) => {
if (e.isComposing) return;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Before this change every keydown reached stopPropagation(). A composing event now returns first and bubbles into Monaco, creating a new path where the editor behind this embedded textarea can process the IME keystroke.

expect(slashCompletionKeyAction("Enter", false, 1, 3)).toEqual({ type: "none" });
});

it("ignores keyboard actions when IME is actively composing", () => {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test never invokes handleKeyDown or any other production behavior. prevented and stopped remain false only because their callbacks are never called, so the test still passes if the production composition guard is deleted.

Comment thread scripts/check-catalog.ts
}

const lock = Bun.JSONC.parse(readFileSync(join(root, "bun.lock"), "utf8")) as BunLock;
const lockModule = await import(join(root, "bun.lock"));

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This changes the repository-wide dependency gate but is unrelated to IME handling. Under the project-pinned Bun 1.3.14, the existing Bun.JSONC.parse path works and the catalog tests pass, so this PR currently mixes an unmotivated tooling behavior change into the UI bug fix.

Add  checks to all keydown handlers across text inputs
(Composer, SlashCommandCompletion, TodoList, HistoryOverlay,
NewWorkspaceDialog, LoginDialog, ExtUiDialog, PreviewCommenting,
ReviewThreadCard, reviewWidgets, LayoutSettings, useGlobalHotkeys)
to prevent premature submission and duplicate character insertion
when pressing Enter during Korean/CJK composition.

Update SPEC.md with IME composition safety invariant.
Add unit test for SlashCommandCompletion IME guard.
@kuil09

Copy link
Copy Markdown
Author

Rinat S (@rsolmano)
Thanks for the detailed review. I'll address all the points raised and update the PR shortly. 🙇

@kuil09
Hwang, Gunn Gu (kuil09) marked this pull request as draft September 1, 2026 01:48
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.

2 participants