Skip to content

fix(web): prevent Enter from sending message during IME composition on Safari#1815

Open
qianqiuqiu wants to merge 3 commits intoMoonshotAI:mainfrom
qianqiuqiu:fix/safari-ime-enter-submit
Open

fix(web): prevent Enter from sending message during IME composition on Safari#1815
qianqiuqiu wants to merge 3 commits intoMoonshotAI:mainfrom
qianqiuqiu:fix/safari-ime-enter-submit

Conversation

@qianqiuqiu
Copy link
Copy Markdown

@qianqiuqiu qianqiuqiu commented Apr 9, 2026

Related Issue

N/A (new issue — discovered during manual testing on Safari + macOS native Chinese IME)

Description

Bug

On Safari (macOS), pressing Enter to commit raw English letters from the IME candidate bar sends the message immediately instead of committing the text to the input field. Chrome and Firefox are not affected.

Repro steps

  1. Open Kimi Web in Safari on macOS
  2. Keep the macOS native Chinese input method (Pinyin) active — do not switch to an English input source
  3. In a mixed Chinese/English typing flow, type a short English word directly, e.g. ok or hello — the IME candidate bar appears with the raw letters
  4. Press Enter to commit the raw English letters to the textarea (a common habit to avoid switching input sources for occasional English words)

Expected: The English letters (ok / hello) are committed to the textarea as-is; the user continues editing.
Actual: The message is sent immediately.

Root cause

The existing guard in PromptInputTextarea checks two conditions before allowing Enter to submit:

if (isComposing || e.nativeEvent.isComposing) {
  return;
}

This is a thoughtful design — useState may have stale values due to React's asynchronous batching, so e.nativeEvent.isComposing is added as a synchronous fallback. This works perfectly on Chrome and Firefox, where the W3C-specified event order is:

keydown (Enter, isComposing: true)  →  compositionend  →  keyup

However, Safari has a long-standing event ordering bug (WebKit #165004, status: NEW, unfixed) that reverses the order:

compositionend  →  keydown (Enter, isComposing: false)  →  keyup

Because compositionend fires before the Enter keydown:

  • isComposing (React state) → already set to false by the onCompositionEnd handler
  • e.nativeEvent.isComposing → also false (the browser considers composition ended)

Both guards fail, and the Enter keystroke reaches form?.requestSubmit().

Fix

Record the timestamp of compositionend via a useRef. In the Enter keydown handler, treat any Enter arriving within 100 ms of compositionend as part of the composition — not a send action.

Uses event.timeStamp (monotonic, immune to main-thread blocking) instead of Date.now() for reliable timing — adopted from Copilot review feedback.

const compositionEndTimeRef = useRef(0);

// In handleKeyDown:
if (
  isComposing ||
  e.nativeEvent.isComposing ||
  e.timeStamp - compositionEndTimeRef.current < 100  // Safari workaround
) {
  return;
}

// In onCompositionEnd:
onCompositionEnd={(e) => {
  setIsComposing(false);
  compositionEndTimeRef.current = e.timeStamp;
}}

This is the same approach used by ProseMirror and CodeMirror to handle this Safari quirk. The 100 ms window is far shorter than any realistic gap between finishing a composition and intentionally pressing Enter to send, so Chrome/Firefox behavior is completely unaffected.

Scope: 1 file changed, 3 lines of logic added. No behavioral change on Chrome/Firefox.

Review feedback adopted

  • Copilot: Switched from Date.now() to event.timeStamp for monotonic, main-thread-safe timing — thanks for catching this.
  • Codex: Fixed missing e parameter in onCompositionEnd callback that would have caused a ReferenceError at runtime — good catch, especially since this file is excluded from tsc checking.

Checklist

  • I have read the CONTRIBUTING document.
  • I have linked the related issue, if any.
  • I have added tests that prove my fix is effective or that my feature works.
  • I have run make gen-changelog to update the changelog.
  • I have run make gen-docs to update the user documentation. (N/A)

…n Safari

Safari fires compositionend before the Enter keydown (WebKit bug #165004),
making both isComposing and nativeEvent.isComposing false by the time the
handler checks. Add a 100 ms post-compositionend cooldown via useRef to
block premature submission. Chrome/Firefox behavior is unaffected.
Copilot AI review requested due to automatic review settings April 9, 2026 09:02
Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

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

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no potential bugs to report.

View in Devin Review to see 1 additional finding.

Open in Devin Review

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bf60a7f602

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +970 to +974
if (
isComposing ||
e.nativeEvent.isComposing ||
Date.now() - compositionEndTimeRef.current < 100
) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Gate composition cooldown so fast Enter still submits

The new Date.now() - compositionEndTimeRef.current < 100 guard in handleKeyDown runs on all browsers, not just Safari. After any compositionend (for example, IME text committed via Space/candidate click), a user who presses Enter quickly to send will hit this early return before e.preventDefault(), so the textarea inserts a newline instead of submitting. This creates a cross-browser regression for fast IME workflows; the cooldown should be limited to the Safari-specific path (or suppress default Enter behavior when blocking submit).

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Fixes a Safari/macOS IME edge case where pressing Enter to commit IME text can incorrectly submit the chat message due to WebKit’s compositionend/keydown event ordering.

Changes:

  • Add a compositionend timestamp ref and an Enter “cooldown” guard to prevent unintended submission on Safari.
  • Update onCompositionEnd to record the last composition end time.
  • Add an Unreleased changelog entry documenting the Safari IME fix.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
web/src/components/ai-elements/prompt-input.tsx Adds a post-compositionend guard to block Enter submission immediately after composition ends (Safari workaround).
CHANGELOG.md Documents the web IME/Safari Enter submission fix in Unreleased.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 1055 to +1059
onBlur={() => setIsComposing(false)}
onCompositionEnd={() => setIsComposing(false)}
onCompositionEnd={() => {
setIsComposing(false);
compositionEndTimeRef.current = Date.now();
}}
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

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

onBlur/onCompositionEnd are defined here but then can be overridden by {...props} later in the JSX spread. In current usage (ChatPromptComposer) onBlur is passed, so setIsComposing(false) never runs on blur; similarly, any future onCompositionEnd prop would bypass updating compositionEndTimeRef and reintroduce the Safari bug. Consider destructuring onBlur/onCompositionStart/onCompositionEnd from props and invoking them inside internal handlers (or spread props before attaching these internal handlers) so internal composition state and the cooldown ref are always maintained.

Copilot uses AI. Check for mistakes.
Comment on lines +970 to +974
if (
isComposing ||
e.nativeEvent.isComposing ||
Date.now() - compositionEndTimeRef.current < 100
) {
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

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

The cooldown uses Date.now() deltas, which are based on handler execution time and wall-clock time. If the main thread is blocked (or the system clock changes), the computed delta can exceed 100ms (or go negative), defeating the safeguard or suppressing Enter unexpectedly. Consider storing the compositionend event timestamp (e.g., e.timeStamp or performance.now()) and comparing it to the keydown event’s timestamp instead of Date.now().

Copilot uses AI. Check for mistakes.
…cooldown

Addresses Copilot review feedback — event.timeStamp is monotonic and
immune to main-thread blocking or system clock changes, making the
100 ms composition cooldown more reliable than Date.now() deltas.
devin-ai-integration[bot]

This comment was marked as resolved.

The arrow function referenced e.timeStamp but had no parameter,
which would throw ReferenceError at runtime.
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