Skip to content

fix(desktop): commit IME-finalised text on compositionend so Enter submits - #39435

Closed
maxmilian wants to merge 1 commit into
NousResearch:mainfrom
maxmilian:fix/desktop-ime-compositionend-submit
Closed

fix(desktop): commit IME-finalised text on compositionend so Enter submits#39435
maxmilian wants to merge 1 commit into
NousResearch:mainfrom
maxmilian:fix/desktop-ime-compositionend-submit

Conversation

@maxmilian

Copy link
Copy Markdown
Contributor

Summary

Fixes #39025 (dup #39112 / #39107). On the Windows desktop app, after typing with a Chinese/Japanese/Korean IME, pressing Enter can fail to send: the text is visible in the composer but Hermes behaves as if there is no draft.

Root cause

apps/desktop/src/app/chat/composer/index.tsx skips state writes while IME composition is active:

if (composingRef.current) { return }   // handleEditorInput

and relies on a clean input event after compositionend to push the finalised text into draftRef + aui.composer().setText(...). On Windows/Electron that trailing input event isn't always delivered, leaving:

  • visible editor text: non-empty
  • tracked draft (draft / hasComposerPayload): empty/stale

Enter then runs submitDraft(), which checks the stale draft and no-ops. (Thanks to @sunwz1115 for the precise diagnosis and a tested fix shape in the issue.)

Fix

Add syncComposerDraft(editor, prevDraft, setText) (in rich-editor.ts) that commits the live contentEditable text back into app state, and call it from compositionend — not just the flaky input event — so the draft is correct the moment composition ends. handleEditorInput reuses the same helper.

Because compositionend is a reliably-delivered event that fires before the user's submit-Enter (a separate event, with a React flush in between), syncing there makes the draft fresh for every downstream path (submitDraft, queueCurrentDraft, hasComposerPayload) without touching any of them — minimal blast radius.

The existing composingRef/nativeEvent.isComposing keydown guard is unchanged, so Enter during active composition still confirms the IME candidate instead of submitting.

Tests

apps/desktop vitest:

  • rich-editor.test.ts: syncComposerDraft commits a stale draft, no-ops when already in sync, and is null-editor safe.
  • ime-submit-repro.test.tsx: DOM repro — IME-finalised text submits on Enter even when the post-composition input event is dropped; Enter during active composition does not submit. (Mirror-harness style, same as the existing slash-nav-dom-repro.test.tsx, exercising the real syncComposerDraft.)
  • tsc -b + eslint clean; all 4 composer test files pass (15 tests).

(Note: the desktop vitest suite has ~52 pre-existing failures in this local environment from a jsdom/Node localStorage quirk, unrelated to this change.)

Scope

Follow-up

  • If a dropped compositionend is ever observed (not reported), a belt-and-suspenders sync at the top of submitDraft reading live DOM text would cover it.

@maxmilian
maxmilian force-pushed the fix/desktop-ime-compositionend-submit branch from d5294ac to c607e20 Compare June 5, 2026 01:13
@alt-glitch alt-glitch added type/bug Something isn't working P3 Low — cosmetic, nice to have labels Jun 5, 2026
…bmits

On Windows/Electron, the chat composer's contentEditable skips state writes
while `composingRef` is true and relies on a trailing `input` event after
`compositionend` to push the finalised text into `draftRef` + the
assistant-ui composer state. That input event isn't always delivered, so the
visible editor holds Chinese/Japanese/Korean text while the tracked draft
stays empty — and Enter calls `submitDraft()`, sees a stale/empty draft, and
no-ops (NousResearch#39025, dup NousResearch#39112 / NousResearch#39107).

Add a `syncComposerDraft(editor, prevDraft, setText)` helper that commits the
live DOM text back into app state, and:
- call it from `compositionend` (not just the flaky input event) so the draft
  is correct the moment composition ends; `handleEditorInput` reuses it;
- call it at the top of `submitDraft()` and read the synchronously-updated
  `draftRef` (via the helper's return) for the submit/queue decision, so a
  finalised message sent in the same tick as `compositionend` isn't lost to
  React's async state update; `queueCurrentDraft()` likewise reads `draftRef`.

The existing `composingRef`/`isComposing` keydown guard is unchanged, so Enter
during active composition still confirms the IME candidate instead of
submitting.

Tests: unit tests for `syncComposerDraft` (commits stale draft, no-ops when in
sync, null-editor safe) and a DOM repro that submits IME-finalised text on
Enter when the post-composition input event is dropped — asserting against the
same synchronous read path submitDraft uses — and confirms Enter during
composition does not submit.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@maxmilian
maxmilian force-pushed the fix/desktop-ime-compositionend-submit branch from c607e20 to 818a1a4 Compare June 5, 2026 02:07
@maxmilian
maxmilian marked this pull request as ready for review June 5, 2026 02:28
@maxmilian

Copy link
Copy Markdown
Contributor Author

Closing — superseded by the flushEditorToDraft fix (#39614), now on main. The onCompositionEnd handler already flushes finalised IME text into composer state, which is exactly what this PR did via syncDraftFromEditor. No longer needed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

P3 Low — cosmetic, nice to have type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Windows desktop: Enter does not submit Chinese IME text because rich composer draft state can stay stale

2 participants