Skip to content

studio/chat: release stuck IME flag when compositionend never fires#5551

Merged
danielhanchen merged 8 commits into
unslothai:mainfrom
wtfashwin:fix/5546-ime-composition-watchdog
May 18, 2026
Merged

studio/chat: release stuck IME flag when compositionend never fires#5551
danielhanchen merged 8 commits into
unslothai:mainfrom
wtfashwin:fix/5546-ime-composition-watchdog

Conversation

@wtfashwin

Copy link
Copy Markdown
Contributor

Summary

Root cause

The earlier IME hardening (#5327) cleared the stuck-composition flag on the next non-composing input event. On Chrome talking to a WSL-hosted Studio, the OS-level IME pipeline fires compositionstart + compositionupdate but never emits compositionend or a follow-up non-composing input event after the commit. composingRef.current stays true, canSend stays false, and the committed Chinese text just sits in the textarea with no way to send it.

Changes

  • studio/frontend/src/components/assistant-ui/thread.tsx: introduce IME_STUCK_TIMEOUT_MS (2500 ms) and rework useImeComposerInputHandlers so setCompositionState(true) arms a watchdog timer; onCompositionUpdate and the existing onChange rearm it; expiry resets both composingRef and isComposing. Cleans up on unmount.
  • studio/frontend/src/features/chat/shared-composer.tsx: mirror the same watchdog into the compare-mode composer.
  • tests/studio/playwright_chat_ime_i18n.py: new section 6b that dispatches the WSL/Chrome event sequence (start → update → commit input event, no compositionend) and asserts the Send button comes back enabled while the committed text is still in the textarea.
  • tests/studio/test_composer_rtl_bidi_attribute.py: two new static guards so the watchdog and its onCompositionUpdate wiring can't be silently dropped in a future refactor.

Validation

  • npm run typecheck (clean)
  • npm run build (clean)
  • npx biome check on the two changed .tsx files — same diagnostic count as main, no new errors/warnings introduced
  • python3 -m pytest tests/studio/test_composer_rtl_bidi_attribute.py -v → 8 passed (6 existing + 2 new guards)

Chrome on Windows talking to a WSL-hosted Studio (issue unslothai#5546) fires
compositionstart + compositionupdate but no compositionend after the
IME commits. The earlier hardening in unslothai#5327 cleared the stale flag on
the next non-composing input event, which never arrives in this
sequence, so composingRef stays true forever and the Send button stays
disabled even though the committed CJK text is already in the textarea.

Add a watchdog in both useImeComposerInputHandlers (main + edit
composer) and SharedComposer (compare mode) that runs the same reset
the missing compositionend would have done. The timer is rearmed on
every compositionupdate and on every non-composing input so it only
fires when the IME pipeline has actually gone quiet — normal candidate
selection keeps it alive, the WSL stuck case lets it expire.

Extends the existing IME Playwright smoke with a stuck-compositionend
repro and adds a static guard so the watchdog can't be removed without
the regression tests catching it.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a watchdog timer to resolve an issue where the compositionend event fails to fire in specific environments, such as Chrome on Windows over WSL, which previously caused the message composer to remain in a "composing" state indefinitely. The implementation adds a 2500ms timeout that resets during compositionupdate events and automatically clears the composition flag if the timer expires. This fix is applied to both the assistant-ui thread and the shared composer components. Furthermore, the PR includes new Playwright regression tests and source-level assertions to ensure the watchdog is correctly wired and functional. I have no feedback to provide.

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

Copy link
Copy Markdown

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: 357402cf29

ℹ️ 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 +383 to +388
if (next) {
stuckTimerRef.current = setTimeout(() => {
stuckTimerRef.current = null;
composingRef.current = false;
setIsComposing(false);
}, IME_STUCK_TIMEOUT_MS);

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 Avoid clearing IME state solely on silence

In an active IME session where the user pauses for more than 2.5s while choosing a candidate, this timer flips composingRef/isComposing to false even though no compositionend or commit signal has arrived. Because the Send/Edit guards rely on those flags, the UI can re-enable submission during a still-active composition and allow the current preedit/candidate text to be sent or saved; the watchdog should only be armed after a commit-like input or otherwise verify composition has actually ended. The same silence-based timeout is mirrored in SharedComposer.

Useful? React with 👍 / 👎.

danielhanchen and others added 3 commits May 18, 2026 13:03
…5546 watchdog gap

The stuck-compositionend watchdog (PR unslothai#5551) releases composingRef after
2500 ms of IME silence so Send unwedges in the WSL+Chrome case. The same
release also fires during a long candidate-window pause in healthy IMEs,
which lets a subsequent IME-confirm Enter slip preedit text through
handleSubmit (main composer) or click-Send through send() (compare composer).

Add a keydown gate to both composers: when the browser still reports
nativeEvent.isComposing or keyCode 229, re-pin composingRef and cancel
any pending watchdog so the next form-submit / send() guard refuses.
The Send button stays visually enabled (avoids re-introducing the
stuck-UI bug) but the submit path is blocked until a real compositionend
or non-composing input arrives. Mirrors the existing isComposing guard
shape in shared-composer.onKeyDown.

Tests:
- tests/studio/test_composer_rtl_bidi_attribute.py: two new static
  guards asserting the keydown gate wiring in both composer files.
- tests/studio/playwright_chat_ime_i18n.py: new section 6c repro that
  fires the IME-confirm keydown after the watchdog has cleared, then
  triggers form.requestSubmit() and asserts the preedit text is not
  cleared (would indicate a leaked submit).

Verified across Chromium / Firefox / WebKit via a side-by-side pre-PR
vs post-PR simulation (54 scenarios, zero pageerror or console.error).
The unslothai#5546 stuck-end repro still passes (Send re-enables 2.5-3 s after
the silent commit) and the new keydown-repin probe confirms the submit
gate refuses on all three engines.

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

Copy link
Copy Markdown

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: 597af0d0fd

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +453 to +456
if (e.nativeEvent.isComposing || e.keyCode === 229) {
composingRef.current = true;
clearStuckTimer();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Re-arm IME watchdog after re-pinning composition

When the watchdog has already cleared composingRef during a long IME pause, this keydown path sets composingRef back to true but only clears the timeout; it never schedules a new one. On the same Windows+WSL Chrome path this patch targets (missing compositionend), if no subsequent compositionupdate/input arrives after that IME keydown, composingRef stays true indefinitely and Send remains blocked again (the same pattern is mirrored in shared-composer.tsx).

Useful? React with 👍 / 👎.

wtfashwin and others added 2 commits May 18, 2026 18:42
The keydown re-pin added in 2c3c979 closed the watchdog-race for
healthy IMEs, but on the same WSL+Chrome no-compositionend path this
PR targets it would re-lock Send permanently: setting composingRef=true
and only *clearing* the watchdog leaves the flag pinned forever if no
follow-up compositionend or non-composing input ever arrives.

Swap clearStuckTimer/clearStuckImeTimer for refreshStuckTimer/
refreshStuckImeTimer in both composer keydown gates so the watchdog
fires once more after every IME keypress. Same visual contract — Send
stays enabled — the submit gate just keeps a 2.5s window before
re-releasing instead of staying locked.

Extends the playwright IME smoke with section 6d: clears composing via
the watchdog, fires an IME keydown, then waits past the re-armed
watchdog window and asserts the form submit actually flushes the
textarea. Two new static guards in test_composer_rtl_bidi_attribute
lock the refresh call into both keydown handlers.

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

Copy link
Copy Markdown

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: 8d9b73d96f

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +387 to +389
composingRef.current = false;
setIsComposing(false);
}, IME_STUCK_TIMEOUT_MS);

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 Avoid unlocking IME solely on watchdog silence

The watchdog callback clears composingRef/isComposing after a fixed 2.5s gap even if no commit signal (compositionend or non-composing input) has arrived, which can re-enable Send during an active candidate session when users pause to choose text and then click Send/Edit. In that case preedit/candidate text can be submitted as if composition had finished. Fresh evidence in this commit: the new Playwright section 6b codifies timeout-only unlock behavior with no compositionend, so this path is still present (and mirrored in shared-composer.tsx).

Useful? React with 👍 / 👎.

@danielhanchen danielhanchen merged commit 361f9f9 into unslothai:main May 18, 2026
29 checks passed
@wtfashwin wtfashwin deleted the fix/5546-ime-composition-watchdog branch May 18, 2026 17:11
rsd-darshan pushed a commit to rsd-darshan/unsloth that referenced this pull request Jun 3, 2026
…nslothai#5551)

* studio/chat: release stuck IME flag when compositionend never fires

Chrome on Windows talking to a WSL-hosted Studio (issue unslothai#5546) fires
compositionstart + compositionupdate but no compositionend after the
IME commits. The earlier hardening in unslothai#5327 cleared the stale flag on
the next non-composing input event, which never arrives in this
sequence, so composingRef stays true forever and the Send button stays
disabled even though the committed CJK text is already in the textarea.

Add a watchdog in both useImeComposerInputHandlers (main + edit
composer) and SharedComposer (compare mode) that runs the same reset
the missing compositionend would have done. The timer is rearmed on
every compositionupdate and on every non-composing input so it only
fires when the IME pipeline has actually gone quiet — normal candidate
selection keeps it alive, the WSL stuck case lets it expire.

Extends the existing IME Playwright smoke with a stuck-compositionend
repro and adds a static guard so the watchdog can't be removed without
the regression tests catching it.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* studio/chat: re-pin composing flag on IME keydown to close unslothai#5546 watchdog gap

The stuck-compositionend watchdog (PR unslothai#5551) releases composingRef after
2500 ms of IME silence so Send unwedges in the WSL+Chrome case. The same
release also fires during a long candidate-window pause in healthy IMEs,
which lets a subsequent IME-confirm Enter slip preedit text through
handleSubmit (main composer) or click-Send through send() (compare composer).

Add a keydown gate to both composers: when the browser still reports
nativeEvent.isComposing or keyCode 229, re-pin composingRef and cancel
any pending watchdog so the next form-submit / send() guard refuses.
The Send button stays visually enabled (avoids re-introducing the
stuck-UI bug) but the submit path is blocked until a real compositionend
or non-composing input arrives. Mirrors the existing isComposing guard
shape in shared-composer.onKeyDown.

Tests:
- tests/studio/test_composer_rtl_bidi_attribute.py: two new static
  guards asserting the keydown gate wiring in both composer files.
- tests/studio/playwright_chat_ime_i18n.py: new section 6c repro that
  fires the IME-confirm keydown after the watchdog has cleared, then
  triggers form.requestSubmit() and asserts the preedit text is not
  cleared (would indicate a leaked submit).

Verified across Chromium / Firefox / WebKit via a side-by-side pre-PR
vs post-PR simulation (54 scenarios, zero pageerror or console.error).
The unslothai#5546 stuck-end repro still passes (Send re-enables 2.5-3 s after
the silent commit) and the new keydown-repin probe confirms the submit
gate refuses on all three engines.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* studio/chat: re-arm IME watchdog after keydown re-pin (Codex P1)

The keydown re-pin added in 2c3c979 closed the watchdog-race for
healthy IMEs, but on the same WSL+Chrome no-compositionend path this
PR targets it would re-lock Send permanently: setting composingRef=true
and only *clearing* the watchdog leaves the flag pinned forever if no
follow-up compositionend or non-composing input ever arrives.

Swap clearStuckTimer/clearStuckImeTimer for refreshStuckTimer/
refreshStuckImeTimer in both composer keydown gates so the watchdog
fires once more after every IME keypress. Same visual contract — Send
stays enabled — the submit gate just keeps a 2.5s window before
re-releasing instead of staying locked.

Extends the playwright IME smoke with section 6d: clears composing via
the watchdog, fires an IME keydown, then waits past the re-armed
watchdog window and asserts the form submit actually flushes the
textarea. Two new static guards in test_composer_rtl_bidi_attribute
lock the refresh call into both keydown handlers.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Daniel Han <danielhanchen@gmail.com>
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.

Chinese IME input not working in Unsloth Studio (WSL + Windows Chrome)

2 participants