Shxiao 363b0fc1b4
fix(composer): keep IME-composition Enter out of the slash-skill catalog (#5741)
* fix(composer): keep IME-composition Enter out of the slash-skill catalog

`handlePromptTextareaKeyDown` called `handleSkillSuggestionKeyDown` with no
composition guard, unlike its two siblings: the inline-skill editor wraps the
same call in `isIMEComposing(...)` with a comment naming this exact hazard, and
`handlePromptHistoryKeyDown` checks it too. `PromptInputTextarea` calls the
prop's `onKeyDown` *before* its own `isIMEComposing` check, so the suggestion
handler won the event.

With an IME active and the `/` catalog open, pressing Enter to commit the
composition instead ran `event.preventDefault()` and applied the highlighted
suggestion — discarding the text still being composed and selecting a skill the
user never chose. The catalog opens from the live `textInput.value`, and React
`onChange` fires during composition, so a CJK user typing `/res` to reach a
skill hits it directly.

Guard the main composer's call site the same way the inline-skill editor does,
and pin it with a happy-dom regression test: `keyCode: 229` (the keydown a
browser reports for a key an active IME consumed) must leave both the catalog
and the draft untouched, while a plain Enter still selects the highlighted
skill.

Verified:
- new test `tests/unit/components/workspace/input-box-skill-suggestion-ime.dom.test.tsx`
  fails on main (the IME case applies the skill) and passes here; the plain-Enter
  control case passes on both
- `rstest run --pool threads tests/unit/components/workspace` -> 69 files / 448
  tests passed
- `tsc --noEmit`, `eslint`, `prettier --check` clean on the touched files

* fix(composer): ignore the Enter that follows IME compositionend

Safari emits the confirming Enter after compositionend, with neither
isComposing nor keyCode 229. Drop that Enter in PromptInputTextarea for
a short window, before the skill catalog or prompt history can treat it
as a plain key.

---------

Co-authored-by: Shxiao101 <Shxiao101@users.noreply.github.com>
2026-09-24 17:56:48 +08:00

28 lines
863 B
TypeScript

import type { KeyboardEvent } from "react";
type IMEKeyboardEvent = KeyboardEvent<HTMLElement>;
export function isIMEComposing(
event: IMEKeyboardEvent,
isComposing = false,
): boolean {
return isComposing || event.nativeEvent.isComposing || event.keyCode === 229;
}
// Safari emits the Enter that confirms a composition after compositionend,
// with isComposing false and keyCode 13. React has already flushed the
// composing state by the time that keydown runs.
export const COMPOSITION_CONFIRM_ENTER_MS = 50;
export function isCompositionConfirmEnter(
event: IMEKeyboardEvent,
compositionEndedAt: number,
now = Date.now(),
): boolean {
if (event.key !== "Enter" || event.shiftKey || isIMEComposing(event)) {
return false;
}
const elapsed = now - compositionEndedAt;
return elapsed >= 0 && elapsed < COMPOSITION_CONFIRM_ENTER_MS;
}