diff --git a/surfaces/gui/src/components/Composer.skills.test.tsx b/surfaces/gui/src/components/Composer.skills.test.tsx
index 9d1250be2..fd68a9eea 100644
--- a/surfaces/gui/src/components/Composer.skills.test.tsx
+++ b/surfaces/gui/src/components/Composer.skills.test.tsx
@@ -47,6 +47,31 @@ afterEach(() => {
});
describe("Composer / skills popup", () => {
+ it("does not send when Enter confirms IME composition", async () => {
+ stubFetch();
+ const p = props();
+ render();
+ fireEvent.change(box(), { target: { value: "你好" } });
+
+ fireEvent.keyDown(box(), { key: "Enter", isComposing: true });
+ expect(p.onSend).not.toHaveBeenCalled();
+ expect((box() as HTMLTextAreaElement).value).toBe("你好");
+
+ fireEvent.keyDown(box(), { key: "Enter" });
+ await waitFor(() => expect(p.onSend).toHaveBeenCalledWith("你好", [], undefined));
+ });
+
+ it("does not send for WebKit's legacy IME key code", () => {
+ stubFetch();
+ const p = props();
+ render();
+ fireEvent.change(box(), { target: { value: "你好" } });
+
+ fireEvent.keyDown(box(), { key: "Enter", keyCode: 229 });
+ expect(p.onSend).not.toHaveBeenCalled();
+ expect((box() as HTMLTextAreaElement).value).toBe("你好");
+ });
+
it("opens on a leading '/' and lists only enabled skills from the effective menu", async () => {
stubFetch();
render();
diff --git a/surfaces/gui/src/components/Composer.tsx b/surfaces/gui/src/components/Composer.tsx
index c57d46f98..dbb2b5b81 100644
--- a/surfaces/gui/src/components/Composer.tsx
+++ b/surfaces/gui/src/components/Composer.tsx
@@ -402,6 +402,11 @@ export function Composer(props: Props) {
};
const onKey = (e: React.KeyboardEvent) => {
+ // Enter confirms the active IME candidate before it means "send". WebKit may
+ // report the composition keydown with the legacy 229 key code, so keep that
+ // fallback alongside the standards-based isComposing flag.
+ if (e.nativeEvent.isComposing || e.nativeEvent.keyCode === 229) return;
+
if (slashQuery !== null) {
if (e.key === "ArrowDown") {
e.preventDefault();