fix(desktop): send on Enter from live editor text, not stale composer state#39639
Conversation
… state Pressing Enter often did nothing (~90% with IME / fast typing); adding a trailing space "fixed" it. The composer's submit path read the draft from the AUI composer state (`useAuiState(s => s.composer.text)`) and the derived `hasComposerPayload`, both of which lag the contentEditable DOM by a render. On fast typing or IME composition the final keystroke(s) weren't in state yet, so `submitDraft()` saw an empty draft and dropped the message. A trailing space only worked around it by forcing an extra input event that flushed the state. submitDraft() now refreshes draftRef from the editor node and submits/queues based on the live DOM text, and the Enter handler decides the queue-drain vs submit branch from the DOM too. draftRef is already synced on every input event, so this just closes the in-flight-keystroke gap. Fixes NousResearch#39630. Also addresses the "typing + Enter does nothing" reports in NousResearch#39623.
f000a53 to
22ed461
Compare
…#39630) Pin the contract that the composer's Enter path reads the live DOM editor text, not the render-lagged composer state: a just-typed message sends even when state hasn't synced; while busy it queues (never drains the queue or cancels); an empty Enter while busy is a no-op; and an empty idle Enter drains the next queued prompt. Faithful DOM-event repro mirroring handleEditorKeyDown + submitDraft.
OutThisLife
left a comment
There was a problem hiding this comment.
LGTM — approving. This is the right layer for the "Enter does nothing / drops the message" bug (#39630, and the #39623 "type + Enter, no response" reports).
Root cause is accurate: the submit path read render-lagged AUI state (draft / hasComposerPayload), so a keystroke (or IME composition) that hadn't committed to React state yet was invisible at Enter time — the message dropped, and a trailing space only "worked around" it because the extra input event flushed state. The fix reads the live DOM text in both the keydown branch (hasLivePayload) and at the top of submitDraft (re-syncs draftRef + setText), then routes every branch — queue-drain, busy no-op, queue, send — off the live payload. Consistent and correct.
The regression test reproduces the race deterministically (mutates textContent without firing input, the exact IME/fast-type window) and covers all four branches.
Note for whoever merges: the red nix (macos/ubuntu) checks are a transient cache.nixos.org outage (HTTP2 framing / connection-reset / broken-pipe on narinfo+nar downloads), not this diff — every other check (test shards, e2e, builds, ruff/ty, supply-chain) is green. Re-running the nix jobs now.
… state (NousResearch#39639) * fix(desktop): send on Enter from live editor text, not stale composer state Pressing Enter often did nothing (~90% with IME / fast typing); adding a trailing space "fixed" it. The composer's submit path read the draft from the AUI composer state (`useAuiState(s => s.composer.text)`) and the derived `hasComposerPayload`, both of which lag the contentEditable DOM by a render. On fast typing or IME composition the final keystroke(s) weren't in state yet, so `submitDraft()` saw an empty draft and dropped the message. A trailing space only worked around it by forcing an extra input event that flushed the state. submitDraft() now refreshes draftRef from the editor node and submits/queues based on the live DOM text, and the Enter handler decides the queue-drain vs submit branch from the DOM too. draftRef is already synced on every input event, so this just closes the in-flight-keystroke gap. Fixes NousResearch#39630. Also addresses the "typing + Enter does nothing" reports in NousResearch#39623. * test(desktop): cover Enter-submit from live editor text (NousResearch#39630) Pin the contract that the composer's Enter path reads the live DOM editor text, not the render-lagged composer state: a just-typed message sends even when state hasn't synced; while busy it queues (never drains the queue or cancels); an empty Enter while busy is a no-op; and an empty idle Enter drains the next queued prompt. Faithful DOM-event repro mirroring handleEditorKeyDown + submitDraft.
… state (NousResearch#39639) * fix(desktop): send on Enter from live editor text, not stale composer state Pressing Enter often did nothing (~90% with IME / fast typing); adding a trailing space "fixed" it. The composer's submit path read the draft from the AUI composer state (`useAuiState(s => s.composer.text)`) and the derived `hasComposerPayload`, both of which lag the contentEditable DOM by a render. On fast typing or IME composition the final keystroke(s) weren't in state yet, so `submitDraft()` saw an empty draft and dropped the message. A trailing space only worked around it by forcing an extra input event that flushed the state. submitDraft() now refreshes draftRef from the editor node and submits/queues based on the live DOM text, and the Enter handler decides the queue-drain vs submit branch from the DOM too. draftRef is already synced on every input event, so this just closes the in-flight-keystroke gap. Fixes NousResearch#39630. Also addresses the "typing + Enter does nothing" reports in NousResearch#39623. * test(desktop): cover Enter-submit from live editor text (NousResearch#39630) Pin the contract that the composer's Enter path reads the live DOM editor text, not the render-lagged composer state: a just-typed message sends even when state hasn't synced; while busy it queues (never drains the queue or cancels); an empty Enter while busy is a no-op; and an empty idle Enter drains the next queued prompt. Faithful DOM-event repro mirroring handleEditorKeyDown + submitDraft.
Fixes #39630
Also addresses #39623 ("type + Enter, no response").
Symptom
In the desktop app, pressing Enter after typing frequently does nothing — the message isn't sent. Adding a trailing space before Enter works around it 100%. IME / Chinese users (e.g. typing
你好) and fast typers are hit disproportionately (~90% of attempts); occasionally the message sends but is missing the last few characters.Root cause
The chat input is a custom
contentEditablediv bound to@assistant-ui/react. The submit path read the draft from React/AUI state rather than the DOM:submitDraft()useddraft(useAuiState(s => s.composer.text)) and the derivedhasComposerPayload.handleEditorKeyDown's Enter branch usedhasComposerPayloadtoo.Both lag the DOM by a render. Typing fires
onInput→aui.composer().setText(...)(a scheduled state update). If the user presses Enter before React commits that update,draftis still the previous (often empty) value, so:submitDraft()falls through itstext.trim()check and drops the message, andA trailing space "fixes" it only because the extra
onInputevent flushes the pending state in time.Note:
draftRef(auseRef) is already synced from the DOM on every input event (handleEditorInput), but the submit path wasn't using it.Fix (
apps/desktop/src/app/chat/composer/index.tsx)submitDraft()now refreshesdraftReffrom the editor node (composerPlainText(editorRef.current)) and makes all decisions — slash-command detection, queue-vs-cancel while busy, and the send payload — from that live DOM text instead of the staledraft/hasComposerPayload.This closes the in-flight-keystroke gap (incl. the IME case, which already waits for
compositionendbefore Enter submits). No behavior change when state and DOM agree.Test plan
tsc -bclean你好, press Enter → sends with full text, no dropped trailing chars