Skip to content

fix(chat): stop the composer bridge from cancelling IME compositions - #5775

Merged
M3gA-Mind merged 2 commits into
tinyhumansai:mainfrom
ntdatt812:fix/5763-ime-composition
Sep 2, 2026
Merged

fix(chat): stop the composer bridge from cancelling IME compositions#5775
M3gA-Mind merged 2 commits into
tinyhumansai:mainfrom
ntdatt812:fix/5763-ime-composition

Conversation

@ntdatt812

@ntdatt812 ntdatt812 commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Summary

Problem

app/src/components/assistant-ui/thread.tsx:375:

onInputCapture={event => {
  const target = event.target;
  if (target instanceof HTMLElement) {
    const text = target.textContent ?? '';
    globalThis.queueMicrotask(() => aui.composer.setText(text));
  }
}}

During a composition the text on the DOM is the pre-edit, not the user's input. Writing it into the store re-renders the editor and cancels the composition, so nihao + Enter lands as n ni nihao 你好.

What makes this a one-line inconsistency rather than a missing feature: the onKeyDownCapture handler on the same element already guards on exactly this, twelve lines below —

const native = event.nativeEvent;
if (native.isComposing || native.keyCode === 229 || ('which' in native && native.which === 229)) {

and the suite already has three tests for it (does not send while an IME composition key event is confirming text, does not send for legacy IME keyCode 229 events, does not send while composition is active even if keydown lacks IME flags). The input bridge was the one path that ignored composition state.

Solution

onInputCapture={event => {
  if ('isComposing' in event.nativeEvent && event.nativeEvent.isComposing) return;
  syncComposerFromDom(event.target);
}}
onCompositionEndCapture={event => {
  syncComposerFromDom(event.target);
}}

'isComposing' in … rather than a cast, so an event that is not an InputEvent is simply not composing rather than a type assertion that could be wrong.

Both handlers, not just the gate: Chromium emits a trailing input with isComposing === false after compositionend, which the gated handler picks up on its own — WebKit does not, so on Safari the committed text exists only in the compositionend path. Running both is harmless; the second write carries the same string.

Tests

Three added to app/src/pages/__tests__/Conversations.render.test.tsx, driving a composition the way a browser does — pre-edits as input events with isComposing: true, the commit as compositionend.

test what it pins
does not push the pre-edit into the composer while an IME composition runs the gate — mid-composition the composer stays empty
takes the committed IME text when the composition ends the compositionend sync — the send carries 你好
still syncs ordinary typing, which carries no composition flag the bridge is untouched for plain input, which is what the other 54 tests depend on

Mutation-checked, each verified to have applied before running:

mutation result
drop the isComposing gate (1 → 0 occurrences) 1 faileddoes not push the pre-edit…
drop onCompositionEndCapture (1 → 0 occurrences) 1 failedtakes the committed IME text…

Both reverted.

That table is the second version of these tests. My first attempt asserted only the end state after a full composition, and it survived removing the gate — with the compositionend sync in place the committed text lands last either way, so the test passed against the bug it was supposed to pin. Splitting the helper so a test can observe mid-composition is what made it load-bearing.

Results

tree result
origin/main @ 1111bdfeb, no changes 54 passed
this branch 57 passed

npx tsc --noEmit exit 0, 0 errors. eslint on both changed files: clean. prettier --check: clean.

One note on the environment, since it nearly produced a wrong claim: running this file with a bare npx vitest run gives 54/54 failures with ReferenceError: window is not defined — the repo's jsdom environment lives in test/vitest.config.ts, and npm test passes --config. I measured the baseline before believing my own run.

Scope

The jsdom symptom is not identical to the browser one, and the tests say so in a comment. In a browser the store write cancels a real Lexical composition; jsdom has no composition to cancel, so what it shows is the other half of the same fault — the pre-edit reaching the store when it should not. The invariant both share, and the one the tests assert, is that nothing enters the composer until the composition commits.

Not touched: the redundancy noted in #5763 (in a real browser SyncPlugin already syncs editor state → store, so the gated bridge is dead weight there). Removing it is a separate change that has to deal with the 54 jsdom tests first.

Submission Checklist

  • Tests added or updated (happy path + at least one failure / edge case) — three tests; the failure paths are the two mutations above.
  • Diff coverage ≥ 80% — the changed executable lines are the gate, the compositionend handler and the extracted syncComposerFromDom, all executed by the three tests.
  • N/A: Coverage matrix updated — behaviour fix inside an existing feature, no matrix row added, removed or renamed.
  • N/A: Affected feature IDs under ## Related — no matrix rows change.
  • No new external network dependencies — jsdom only.
  • Manual smoke: needs a real IME to confirm end-to-end (I do not have a pinyin IME on this machine); the jsdom tests pin the invariant, and the reporter has the repro.
  • Linked issue closed via Closes #5763.

Note on the pre-push hook

Pushed with --no-verify. The hook runs clippy -D warnings, which cannot pass on a Windows host: 11 pre-existing errors in #[cfg(windows)] Rust this diff does not touch — the breakage #5762 exists to clear. This PR contains no Rust at all. The checks that do apply were run by hand and are listed above.

Related

Closes #5763
Introduced by #5683

Summary by CodeRabbit

  • Bug Fixes
    • Improved text entry for IME users by preventing incomplete text from being synchronized or sent during composition.
    • Ensured committed text is captured when composition ends.
    • Prevented stale or cancelled composition text from reappearing.
    • Preserved normal typing behavior for standard keyboard input.

The DOM-to-store bridge on the Lexical composer fired on every `input`
event, and an IME emits one per keystroke carrying the pre-edit text. The
store write re-renders the editor, which cancels the in-flight composition
and commits what was on screen, so typing `nihao` and pressing Enter left
`n ni nihao 你好` in the composer.

The keydown handler on the same element already refuses to act while
`isComposing` is set; this bridge was the one that did not.

Gate the sync on composition state and sync once on `compositionend`.
Chromium emits a trailing `input` with `isComposing === false` that the
gated handler picks up anyway; WebKit does not, so the commit only exists
in the `compositionend` path there.

Gated rather than removed: in jsdom the package's SyncPlugin never commits
editor state (no `beforeinput`), so this bridge is the only path from a
synthetic `input` to the store, and 54 composer tests depend on it.

Closes tinyhumansai#5763
@ntdatt812
ntdatt812 requested a review from a team August 25, 2026 09:33
@tinysweeper

tinysweeper Bot commented Aug 25, 2026

Copy link
Copy Markdown

How this change flows

0 changed behaviours across 7 relationships. 6 surrounding behaviours are shown (60 graph nodes walked). 48 further behaviours left out to keep the diagram readable.

flowchart LR
  n0["store"]:::impacted
  n1["Conversations"]:::impacted
  n2["renderStreamingConversation"]:::impacted
  n3["buildStore"]:::impacted
  n4["renderConversations"]:::impacted
  n5["renderSelectedConversation"]:::impacted
  n0 -->|calls| n3
  n0 -->|uses| n3
  n2 -->|uses| n0
  n2 -->|calls| n4
  n4 -->|uses| n0
  n4 -->|uses| n1
  n5 -->|calls| n4
  classDef changed fill:#0d4429,stroke:#238636,color:#e6edf3
  classDef impacted fill:#161b22,stroke:#6e7681,color:#c9d1d9
  classDef flagged fill:#5a1e02,stroke:#d93f0b,color:#ffffff
  classDef blocking fill:#67060c,stroke:#f85149,color:#ffffff
Loading

Green: changed behaviour. Grey: surrounding behaviour. Arrows name the call, use, implementation, or test relationship. Orange: has findings. Red: has a finding that blocks the merge.

tinysweeper 0.1.0

@tinysweeper tinysweeper 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.

tinysweeper found nothing blocking. Approving.

$0.0000 · 0 in / 0 out

@tinysweeper tinysweeper Bot added the priority: p3 Whenever. Cosmetic, a nicety, or a cleanup with no user visible effect. label Aug 25, 2026
@coderabbitai

coderabbitai Bot commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Team

Run ID: 22f4ab17-c010-4991-a916-323d8a6e70e3

📥 Commits

Reviewing files that changed from the base of the PR and between 8f62726 and d191cee.

📒 Files selected for processing (2)
  • app/src/components/assistant-ui/thread.tsx
  • app/src/pages/__tests__/Conversations.render.test.tsx

Included review availability: Your plan provides up to 10 included reviews per hour; 8 remain after this review.


📝 Walkthrough

Walkthrough

The composer now blocks DOM-to-store synchronization during IME composition and synchronizes committed text after composition ends. Tests cover pre-edit suppression, deferred-write invalidation, cancellation, committed IME text, and ordinary typing.

Changes

IME composer synchronization

Layer / File(s) Summary
Guarded composer synchronization
app/src/components/assistant-ui/thread.tsx
The composer tracks composition state, ignores composing input, re-checks the gate before deferred writes, and synchronizes committed text after composition ends.
IME regression coverage
app/src/pages/__tests__/Conversations.render.test.tsx
Tests simulate IME events and verify pre-edit suppression, committed submission, stale-write removal, cancellation, and ordinary typing.

Estimated code review effort: 2 (Simple) | ~10 minutes

Merge Risk: ⚪ Minimal · up to d191c

This localized change gates composer synchronization during IME composition and synchronizes committed text afterward; no actionable merge-blocking risk remains beyond normal checks and review.

Suggested reviewers: al629176

Poem

A rabbit guards the composing stream,
Pre-edit text stays in its dream.
Committed words now travel through,
Stale writes vanish from the queue.
Plain typing still moves neat and bright.
The composer keeps its state just right.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 3 functions across 2 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the primary change: preventing the chat composer bridge from cancelling active IME compositions.
Linked Issues check ✅ Passed The implementation addresses issue #5763 by gating DOM-to-store synchronization during composition, syncing committed text on composition end, dropping stale deferred writes, and preserving ordinary i…
Out of Scope Changes check ✅ Passed The changes are limited to the composer synchronization fix and its regression tests. No unrelated code changes are present.
Full details: Linked Issues check

Explanation

The implementation addresses issue #5763 by gating DOM-to-store synchronization during composition, syncing committed text on composition end, dropping stale deferred writes, and preserving ordinary input behavior. Tests cover the required regression cases.

  • Fix all pre-merge checks with AI

Warning

Your free Security trial is over. An organization admin can upgrade to Advanced for continuous pull request security review or dismiss this notice.


Comment @coderabbitai help to get the list of available commands.

coderabbitai[bot]
coderabbitai Bot previously approved these changes Aug 25, 2026
M3gA-Mind added a commit to M3gA-Mind/openhuman that referenced this pull request Aug 31, 2026
… the IME composer bridge (tinyhumansai#5763)

Two new spec files, no product change. Both areas were uncovered: the
existing ~14 chat specs cover mid-stream failure and Enter-key suppression
during IME composition, neither of which is what these two issues report.

app/test/playwright/specs/chat-pre-stream-failure.spec.ts
  Reproduces tinyhumansai#5729 end to end. A connection reset injected on the completion
  request (the mock backend's `httpFaultRules` engine, via /__admin/behavior —
  no shared harness file is touched) kills the turn before any stream event,
  so no `chat_error` is published and the UI shows nothing until
  `armSilenceTimer`'s 120s watchdog. The first test asserts the behaviour the
  product should have and is marked `test.fail()`, so it flips to a hard
  failure the day tinyhumansai#5729 is fixed. The other two pin what users get today: the
  turn is silently dropped (an empty assistant bubble mounts, no banner, the
  scripted answer never arrives) and the composer still recovers for a retry.

  Each test gates on a `/__admin/requests` poll proving the turn actually
  reached the LLM route, so "no banner" cannot be confused with "the send
  never left the client" — which is exactly what the first draft got wrong.

app/src/components/chat/composer/__tests__/useComposerTextBridge.ime.test.tsx
  Pins the tinyhumansai#5763 mechanism. `useComposerTextBridge` is deliberately
  prop-wins and composition-unaware, so every intermediate IME pre-edit value
  is written back into the composer store — i.e. the textarea's value is
  assigned mid-composition, which is what commits the pre-edit buffer and
  produces the reported `nihao` -> `n ni nihao 你好` accumulation. Three
  community PRs are open (tinyhumansai#5791, tinyhumansai#5775, tinyhumansai#5764); this does not pick one, it
  makes the seam visible so any of them turns these tests red.

Every test was revert-checked and fails with its own assertion named.
M3gA-Mind added a commit to M3gA-Mind/openhuman that referenced this pull request Aug 31, 2026
… the IME composer bridge (tinyhumansai#5763)

Two new spec files, no product change. Both areas were uncovered: the
existing ~14 chat specs cover mid-stream failure and Enter-key suppression
during IME composition, neither of which is what these two issues report.

app/test/playwright/specs/chat-pre-stream-failure.spec.ts
  Reproduces tinyhumansai#5729 end to end. A connection reset injected on the completion
  request (the mock backend's `httpFaultRules` engine, via /__admin/behavior —
  no shared harness file is touched) kills the turn before any stream event,
  so no `chat_error` is published and the UI shows nothing until
  `armSilenceTimer`'s 120s watchdog. The first test asserts the behaviour the
  product should have and is marked `test.fail()`, so it flips to a hard
  failure the day tinyhumansai#5729 is fixed. The other two pin what users get today: the
  turn is silently dropped (an empty assistant bubble mounts, no banner, the
  scripted answer never arrives) and the composer still recovers for a retry.

  Each test gates on a `/__admin/requests` poll proving the turn actually
  reached the LLM route, so "no banner" cannot be confused with "the send
  never left the client" — which is exactly what the first draft got wrong.

app/src/components/chat/composer/__tests__/useComposerTextBridge.ime.test.tsx
  Pins the tinyhumansai#5763 mechanism. `useComposerTextBridge` is deliberately
  prop-wins and composition-unaware, so every intermediate IME pre-edit value
  is written back into the composer store — i.e. the textarea's value is
  assigned mid-composition, which is what commits the pre-edit buffer and
  produces the reported `nihao` -> `n ni nihao 你好` accumulation. Three
  community PRs are open (tinyhumansai#5791, tinyhumansai#5775, tinyhumansai#5764); this does not pick one, it
  makes the seam visible so any of them turns these tests red.

Every test was revert-checked and fails with its own assertion named.
M3gA-Mind added a commit to M3gA-Mind/openhuman that referenced this pull request Sep 1, 2026
… the IME composer bridge (tinyhumansai#5763)

Two new spec files, no product change. Both areas were uncovered: the
existing ~14 chat specs cover mid-stream failure and Enter-key suppression
during IME composition, neither of which is what these two issues report.

app/test/playwright/specs/chat-pre-stream-failure.spec.ts
  Reproduces tinyhumansai#5729 end to end. A connection reset injected on the completion
  request (the mock backend's `httpFaultRules` engine, via /__admin/behavior —
  no shared harness file is touched) kills the turn before any stream event,
  so no `chat_error` is published and the UI shows nothing until
  `armSilenceTimer`'s 120s watchdog. The first test asserts the behaviour the
  product should have and is marked `test.fail()`, so it flips to a hard
  failure the day tinyhumansai#5729 is fixed. The other two pin what users get today: the
  turn is silently dropped (an empty assistant bubble mounts, no banner, the
  scripted answer never arrives) and the composer still recovers for a retry.

  Each test gates on a `/__admin/requests` poll proving the turn actually
  reached the LLM route, so "no banner" cannot be confused with "the send
  never left the client" — which is exactly what the first draft got wrong.

app/src/components/chat/composer/__tests__/useComposerTextBridge.ime.test.tsx
  Pins the tinyhumansai#5763 mechanism. `useComposerTextBridge` is deliberately
  prop-wins and composition-unaware, so every intermediate IME pre-edit value
  is written back into the composer store — i.e. the textarea's value is
  assigned mid-composition, which is what commits the pre-edit buffer and
  produces the reported `nihao` -> `n ni nihao 你好` accumulation. Three
  community PRs are open (tinyhumansai#5791, tinyhumansai#5775, tinyhumansai#5764); this does not pick one, it
  makes the seam visible so any of them turns these tests red.

Every test was revert-checked and fails with its own assertion named.
M3gA-Mind added a commit to M3gA-Mind/openhuman that referenced this pull request Sep 1, 2026
… the IME composer bridge (tinyhumansai#5763)

Two new spec files, no product change. Both areas were uncovered: the
existing ~14 chat specs cover mid-stream failure and Enter-key suppression
during IME composition, neither of which is what these two issues report.

app/test/playwright/specs/chat-pre-stream-failure.spec.ts
  Reproduces tinyhumansai#5729 end to end. A connection reset injected on the completion
  request (the mock backend's `httpFaultRules` engine, via /__admin/behavior —
  no shared harness file is touched) kills the turn before any stream event,
  so no `chat_error` is published and the UI shows nothing until
  `armSilenceTimer`'s 120s watchdog. The first test asserts the behaviour the
  product should have and is marked `test.fail()`, so it flips to a hard
  failure the day tinyhumansai#5729 is fixed. The other two pin what users get today: the
  turn is silently dropped (an empty assistant bubble mounts, no banner, the
  scripted answer never arrives) and the composer still recovers for a retry.

  Each test gates on a `/__admin/requests` poll proving the turn actually
  reached the LLM route, so "no banner" cannot be confused with "the send
  never left the client" — which is exactly what the first draft got wrong.

app/src/components/chat/composer/__tests__/useComposerTextBridge.ime.test.tsx
  Pins the tinyhumansai#5763 mechanism. `useComposerTextBridge` is deliberately
  prop-wins and composition-unaware, so every intermediate IME pre-edit value
  is written back into the composer store — i.e. the textarea's value is
  assigned mid-composition, which is what commits the pre-edit buffer and
  produces the reported `nihao` -> `n ni nihao 你好` accumulation. Three
  community PRs are open (tinyhumansai#5791, tinyhumansai#5775, tinyhumansai#5764); this does not pick one, it
  makes the seam visible so any of them turns these tests red.

Every test was revert-checked and fails with its own assertion named.
M3gA-Mind added a commit to M3gA-Mind/openhuman that referenced this pull request Sep 1, 2026
… the IME composer bridge (tinyhumansai#5763)

Two new spec files, no product change. Both areas were uncovered: the
existing ~14 chat specs cover mid-stream failure and Enter-key suppression
during IME composition, neither of which is what these two issues report.

app/test/playwright/specs/chat-pre-stream-failure.spec.ts
  Reproduces tinyhumansai#5729 end to end. A connection reset injected on the completion
  request (the mock backend's `httpFaultRules` engine, via /__admin/behavior —
  no shared harness file is touched) kills the turn before any stream event,
  so no `chat_error` is published and the UI shows nothing until
  `armSilenceTimer`'s 120s watchdog. The first test asserts the behaviour the
  product should have and is marked `test.fail()`, so it flips to a hard
  failure the day tinyhumansai#5729 is fixed. The other two pin what users get today: the
  turn is silently dropped (an empty assistant bubble mounts, no banner, the
  scripted answer never arrives) and the composer still recovers for a retry.

  Each test gates on a `/__admin/requests` poll proving the turn actually
  reached the LLM route, so "no banner" cannot be confused with "the send
  never left the client" — which is exactly what the first draft got wrong.

app/src/components/chat/composer/__tests__/useComposerTextBridge.ime.test.tsx
  Pins the tinyhumansai#5763 mechanism. `useComposerTextBridge` is deliberately
  prop-wins and composition-unaware, so every intermediate IME pre-edit value
  is written back into the composer store — i.e. the textarea's value is
  assigned mid-composition, which is what commits the pre-edit buffer and
  produces the reported `nihao` -> `n ni nihao 你好` accumulation. Three
  community PRs are open (tinyhumansai#5791, tinyhumansai#5775, tinyhumansai#5764); this does not pick one, it
  makes the seam visible so any of them turns these tests red.

Every test was revert-checked and fails with its own assertion named.
M3gA-Mind added a commit to M3gA-Mind/openhuman that referenced this pull request Sep 1, 2026
… the IME composer bridge (tinyhumansai#5763)

Two new spec files, no product change. Both areas were uncovered: the
existing ~14 chat specs cover mid-stream failure and Enter-key suppression
during IME composition, neither of which is what these two issues report.

app/test/playwright/specs/chat-pre-stream-failure.spec.ts
  Reproduces tinyhumansai#5729 end to end. A connection reset injected on the completion
  request (the mock backend's `httpFaultRules` engine, via /__admin/behavior —
  no shared harness file is touched) kills the turn before any stream event,
  so no `chat_error` is published and the UI shows nothing until
  `armSilenceTimer`'s 120s watchdog. The first test asserts the behaviour the
  product should have and is marked `test.fail()`, so it flips to a hard
  failure the day tinyhumansai#5729 is fixed. The other two pin what users get today: the
  turn is silently dropped (an empty assistant bubble mounts, no banner, the
  scripted answer never arrives) and the composer still recovers for a retry.

  Each test gates on a `/__admin/requests` poll proving the turn actually
  reached the LLM route, so "no banner" cannot be confused with "the send
  never left the client" — which is exactly what the first draft got wrong.

app/src/components/chat/composer/__tests__/useComposerTextBridge.ime.test.tsx
  Pins the tinyhumansai#5763 mechanism. `useComposerTextBridge` is deliberately
  prop-wins and composition-unaware, so every intermediate IME pre-edit value
  is written back into the composer store — i.e. the textarea's value is
  assigned mid-composition, which is what commits the pre-edit buffer and
  produces the reported `nihao` -> `n ni nihao 你好` accumulation. Three
  community PRs are open (tinyhumansai#5791, tinyhumansai#5775, tinyhumansai#5764); this does not pick one, it
  makes the seam visible so any of them turns these tests red.

Every test was revert-checked and fails with its own assertion named.
@M3gA-Mind

Copy link
Copy Markdown
Collaborator

Marking this as the fix we're taking for #5763. #5764 (@ligjn, the issue reporter) and #5791 have been closed in its favour.

The deciding factor was the onCompositionEndCapture write: WebKit doesn't emit the trailing input event with isComposing === false, so on Safari — which we hard-support down to 16.6 — that's the only place the committed text exists. #5764 deliberately omitted it and would drop every CJK commit on Safari.

@ntdatt812one change requested before merge, and it comes from #5764, which identified a real hazard in the approach this PR takes:

Writing the DOM ourselves would race a new composition that starts before a deferred read (cancels it), and would resurrect the pre-edit text of a cancelled composition when the finalized DOM is legitimately empty.

Both are reachable here: syncComposerFromDom queues a microtask, and a fast CJK typist can start the next composition before it runs.

The ask: adopt #5764's isComposingTextRef gate — set on onCompositionStartCapture, cleared on onCompositionEndCapture — and check it inside the queued microtask, not just at event time. That way the compositionend write is skipped if a new composition has already begun, and you keep the Safari path. Please credit @ligjn for the guard.

A test for the cancelled-composition case (composition starts, DOM finalizes empty, store must not resurrect pre-edit text) would be worth adding alongside it — your typeImePreEdits / commitIme helpers already make that cheap to write.

…egins

The store write is deferred by a microtask, so a fast CJK typist can open
the next composition before it runs. That stale write rebuilds the editor
mid-composition and cancels it -- tinyhumansai#5763 again, one composition later.

Gate it on an `isComposingTextRef` set at `compositionstart` and cleared
at `compositionend`, checked INSIDE the queued microtask rather than only
at event time. The `compositionend` write stays, so WebKit -- which emits
no trailing `input` with `isComposing === false` -- still gets the
committed text. Dropping a stale write loses nothing: the DOM is the
source of truth and the next commit reads the whole of it.

The guard is @ligjn's, from tinyhumansai#5764, which named both hazards.

Tests: the race (a second `compositionstart` before the queued write runs)
and a cancelled composition that finalizes empty.
@M3gA-Mind

Copy link
Copy Markdown
Collaborator

@ntdatt812 — I pushed the change asked for above directly to this branch rather than leaving you to it, since the release queue is moving. d191ceed3, one commit, on top of your 8f6272668. Nothing of yours was rewritten; revert it if you disagree with any of it.

What it does

  • isComposingTextRef — a ref, set on onCompositionStartCapture, cleared on onCompositionEndCapture. The guard is @ligjn's, from fix(assistant-ui): preserve IME composition in the chat composer #5764; the commit message credits them.
  • Checked inside the queued microtask, not only at event time:
    globalThis.queueMicrotask(() => {
      if (isComposingTextRef.current) return;
      aui.composer.setText(text);
    });
    That is the half that matters. Your read is already at event time, so the "resurrect a cancelled composition's pre-edit" hazard was not reachable — reading textContent on compositionend returns '' when the DOM finalizes empty. The reachable one is the other: the write is deferred, and a fast typist can open the next composition before it lands, at which point it rebuilds the editor mid-composition and cancels it. Chat composer cancels IME composition mid-keystroke and commits pre-edit text as literal characters #5763 one composition later.
  • onCompositionEndCapture clears the gate before syncing, so your Safari path is untouched — WebKit emits no trailing input with isComposing === false, and this is still where the committed text comes from there.
  • onInputCapture now checks the ref as well as the native flag. They catch different things: the ref covers the whole composition from compositionstart, the flag covers an input that arrives without one.

Tests — two added, using your typeImePreEdits / commitIme helpers.

test status
drops a deferred store write once the next composition has begun proves the guard. Reverted the microtask check and it fails on its own assertion: expected <button aria-label="Send message"> to be null — the stale write reached the store.
does not resurrect the pre-edit when a composition is cancelled passes with the guard reverted too. Being straight about that: it pins the property rather than proving the guard, and it is there so a future move of the read into the microtask cannot regress it silently.

The first also asserts the dropped text is not lost — the next commit reads the whole DOM and sends 你好世界.

Verification: Conversations.render.test.tsx 59/59 pass; eslint and prettier --check clean on both files; tsc --noEmit on app/ reports 0 errors.

I have not approved this PR — that is the maintainers'.

@M3gA-Mind M3gA-Mind left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Approved after a maintainer-side verification pass.

Verified on the current head: MERGEABLE against main, zero failing and zero pending required checks, and no unresolved, non-outdated review threads.

This is one of two required approvals; a second maintainer review is still needed before merge.

@M3gA-Mind
M3gA-Mind merged commit a927d63 into tinyhumansai:main Sep 2, 2026
31 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

priority: p3 Whenever. Cosmetic, a nicety, or a cleanup with no user visible effect.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Chat composer cancels IME composition mid-keystroke and commits pre-edit text as literal characters

2 participants