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

64 lines
1.6 KiB
TypeScript

import { describe, expect, it } from "@rstest/core";
import type { KeyboardEvent } from "react";
import {
COMPOSITION_CONFIRM_ENTER_MS,
isCompositionConfirmEnter,
} from "@/lib/ime";
function enter(
overrides: Partial<KeyboardEvent<HTMLElement>> = {},
): KeyboardEvent<HTMLElement> {
return {
key: "Enter",
shiftKey: false,
keyCode: 13,
nativeEvent: { isComposing: false },
...overrides,
} as KeyboardEvent<HTMLElement>;
}
describe("isCompositionConfirmEnter", () => {
const endedAt = 1_000;
it("matches the Enter that arrives just after compositionend", () => {
expect(isCompositionConfirmEnter(enter(), endedAt, endedAt)).toBe(true);
expect(
isCompositionConfirmEnter(
enter(),
endedAt,
endedAt + COMPOSITION_CONFIRM_ENTER_MS - 1,
),
).toBe(true);
});
it("lets a later Enter through", () => {
expect(
isCompositionConfirmEnter(
enter(),
endedAt,
endedAt + COMPOSITION_CONFIRM_ENTER_MS,
),
).toBe(false);
expect(isCompositionConfirmEnter(enter(), 0, endedAt)).toBe(false);
});
it("does not claim Shift+Enter or a keydown that is still composing", () => {
expect(
isCompositionConfirmEnter(enter({ shiftKey: true }), endedAt, endedAt),
).toBe(false);
expect(
isCompositionConfirmEnter(enter({ keyCode: 229 }), endedAt, endedAt),
).toBe(false);
expect(
isCompositionConfirmEnter(
enter({
nativeEvent: { isComposing: true } as KeyboardEvent["nativeEvent"],
}),
endedAt,
endedAt,
),
).toBe(false);
});
});