From bac571ef4ea6c7435209c8661a8d483f87712084 Mon Sep 17 00:00:00 2001 From: beetle0915 <120192315+beetle0915@users.noreply.github.com> Date: Tue, 7 Jul 2026 08:24:34 +0800 Subject: [PATCH] Fix Enter handling during IME composition --- .../canvas/canvas-agent-chat-ui.tsx | 3 +- .../canvas-resource-mention-textarea.tsx | 7 ++++- web/src/lib/keyboard-event.ts | 22 +++++++++++++ web/tests/ime-keyboard.test.ts | 31 +++++++++++++++++++ 4 files changed, 61 insertions(+), 2 deletions(-) create mode 100644 web/src/lib/keyboard-event.ts create mode 100644 web/tests/ime-keyboard.test.ts diff --git a/web/src/components/canvas/canvas-agent-chat-ui.tsx b/web/src/components/canvas/canvas-agent-chat-ui.tsx index b8f622e..739a902 100644 --- a/web/src/components/canvas/canvas-agent-chat-ui.tsx +++ b/web/src/components/canvas/canvas-agent-chat-ui.tsx @@ -2,6 +2,7 @@ import { useEffect, useRef, useState, type ReactNode } from "react"; import { Button, Tooltip } from "antd"; import { ArrowUp, CheckCircle2, CircleAlert, ImagePlus, LoaderCircle, UserRound, Wrench, X, XCircle } from "lucide-react"; +import { isPlainEnterKey } from "@/lib/keyboard-event"; import { canvasThemes } from "@/lib/canvas-theme"; import type { LocalUser } from "@/stores/use-user-store"; @@ -198,7 +199,7 @@ export function AgentChatComposer({ void onAddFiles(images); }} onKeyDown={(event) => { - if (event.key !== "Enter" || event.shiftKey || event.ctrlKey || event.metaKey) return; + if (!isPlainEnterKey(event)) return; event.preventDefault(); void onSubmit(); }} diff --git a/web/src/components/canvas/canvas-resource-mention-textarea.tsx b/web/src/components/canvas/canvas-resource-mention-textarea.tsx index 4e4d801..5315750 100644 --- a/web/src/components/canvas/canvas-resource-mention-textarea.tsx +++ b/web/src/components/canvas/canvas-resource-mention-textarea.tsx @@ -4,6 +4,7 @@ import { createPortal } from "react-dom"; import { FileText, Image as ImageIcon, Music2, Video } from "lucide-react"; import { canvasThemes } from "@/lib/canvas-theme"; +import { isImeComposing, isPlainEnterKey } from "@/lib/keyboard-event"; import { useThemeStore } from "@/stores/use-theme-store"; import type { CanvasResourceReference } from "@/lib/canvas/canvas-resource-references"; @@ -131,6 +132,10 @@ export const CanvasResourceMentionTextarea = forwardRef { + if (isImeComposing(event)) { + onKeyDown?.(event); + return; + } if (mention && candidates.length) { if (event.key === "ArrowDown") { event.preventDefault(); @@ -153,7 +158,7 @@ export const CanvasResourceMentionTextarea = forwardRef[0]; + +function enterEvent(overrides: Partial = {}): KeyboardEventLike { + return { + key: "Enter", + shiftKey: false, + ctrlKey: false, + metaKey: false, + ...overrides, + }; +} + +assert.equal(isPlainEnterKey(enterEvent()), true, "plain Enter submits"); +assert.equal(isPlainEnterKey(enterEvent({ shiftKey: true })), false, "Shift+Enter does not submit"); +assert.equal(isPlainEnterKey(enterEvent({ ctrlKey: true })), false, "Ctrl+Enter does not submit"); +assert.equal(isPlainEnterKey(enterEvent({ metaKey: true })), false, "Meta+Enter does not submit"); +assert.equal(isPlainEnterKey(enterEvent({ nativeEvent: { isComposing: true } })), false, "IME composition Enter does not submit"); +assert.equal(isPlainEnterKey(enterEvent({ nativeEvent: { keyCode: 229 } })), false, "legacy IME Enter does not submit"); +assert.equal(isPlainEnterKey(enterEvent({ key: "a" })), false, "non-Enter keys do not submit"); + +assert.equal(isImeComposing(enterEvent({ isComposing: true })), true, "direct composition flag is detected"); +assert.equal(isImeComposing(enterEvent({ nativeEvent: { isComposing: true } })), true, "native composition flag is detected"); +assert.equal(isImeComposing(enterEvent({ keyCode: 229 })), true, "direct legacy keyCode composition is detected"); +assert.equal(isImeComposing(enterEvent({ nativeEvent: { keyCode: 229 } })), true, "native legacy keyCode composition is detected"); +assert.equal(isImeComposing(enterEvent()), false, "plain Enter is not composition"); + +console.log("ime keyboard tests passed");