Skip to content

[Bug]: Native Chat structured send can resurrect a just-sent message into the next one #17359

Closed

Description

@nwparker

Found reviewing #17169, with a runnable reproduction. Not a blocker for that PR — it replaces a worse bug — but it is a real regression that PR's ownership model creates, and it should not stay implicit.

Sequence

  1. User commits a syllable and presses Enter. The structured send starts.
  2. Before the RPC resolves, the user opens the next composition.
  3. setDraft('') lands while a composition is active, so the layout effect skips the DOM write and the clear is silently dropped.
  4. onImeSettled then adopts element.value as authoritative — and the already-sent text is still sitting in it.

Reproduced against the real NativeChatComposerField and the real settle contract:

dom = "안녕하"   sent = ["안녕"]

The composer ends up holding the message it already sent, which then rides along on the next send.

Why it happens

NativeChatComposer.tsx:255 clears the draft asynchronously, inside dispatchNativeChatStructuredComposerText(...).then(...). The PTY path (use-native-chat-pty-composer-send.ts:105) clears synchronously and is unaffected.

The window is one RPC round trip, and it widens on remote/SSH structured sessions.

This is the direct cost of "the browser always wins at settlement," which #17169 adopts deliberately and for good reason. On main the same sequence loses the new preedit instead (issue #16911) rather than duplicating — so this is a different failure, not a strictly worse one.

Two fixes, and the obvious one is not free

Clear optimistically before awaiting, matching what the PTY path already does. But the structured path guards on if (!accepted) return and only clears on acceptance — so clearing early trades draft-resurrection for draft loss when a send is rejected. That is worse, not better.

Have the settle path not blindly adopt element.value when a programmatic clear was skipped. More surgical: record that a clear was dropped mid-composition and apply it at settlement, so the browser still wins on composition content while a programmatic clear is not silently lost. This looks like the right shape but is the larger change.

Related, from the same review

use-native-chat-composer-attachments.test.tsx:211-212 asserts the deferred flush never steals focus, but the focus call is inside requestAnimationFrame and the test never advances a frame. Mutating the flush to applyResolvedPaths(paths, true, ...) steals focus with all 50 tests still passing. A three-line rAF flush before those assertions kills the mutant and makes that PR bullet real.

Activity

  1. pullfrog commented on Aug 30, 2026

    @pullfrog
    Contributor
  2. added a commit that references this issue on Aug 30, 2026
    d607a63
  3. added a commit that references this issue on Oct 15, 2026
    4ab2207
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions