You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Found while diagnosing #3019. That issue is a test flake; this is the application behaviour underneath it, which outlives the test fix in #3021.
The mechanism
Two components render a MessageComposer, and which one owns it depends on whether the message list is empty:
frontend/src/components/chat/Footer.tsx:20 — if (!hasMessage(messages) && !showIfEmptyThread) return null;
frontend/src/components/chat/WelcomeScreen.tsx:80,91 — if (hasMessage(messages)) return null; … otherwise renders its own<MessageComposer {...props} />
hasMessage() (frontend/src/lib/utils.ts:10) is true when any step is of type user_message, assistant_message, or tool.
So every transition of hasMessage() unmounts one composer instance and mounts a different one. Because MessageComposer holds its draft in local state, the remount discards whatever the user had typed, and the send button then reads as disabled because the new instance's value is empty (MessageComposer/index.tsx:339-341).
Why this is user-facing, not just a test artifact
The transition fires on ordinary app events, not only in tests:
First turn of a chat.@cl.on_chat_start sends its message asynchronously. A user who starts typing before that message lands can have their input silently dropped.
Thread resume.connection_successful (backend/chainlit/socket.py:208-240) emits the @cl.on_chat_resume message and then resume_thread; the client handler (libs/react-client/src/useChatSession.ts:258+) rebuilds the list from thread.steps and replaces it wholesale. If the persisted history is empty, the list goes [] → [resume message] → [], remounting the composer twice in quick succession.
The faster the backend, the narrower the window — which is exactly why this reads as intermittent.
Suggested direction
Render the composer once, above the WelcomeScreen/Footer branch, so its identity is stable across the transition — or lift the draft value into shared state so a remount preserves it. Either removes a class of "my message vanished" reports that would be near-impossible to reproduce from a bug report.
Related, likely the same root cause: the on_chat_resume message is always erased by the resume_thread replay that follows it, so @cl.on_chat_resume output is only visible for a few frames unless it happens to be in the persisted steps. That may be worth its own issue if it is not intended.
Found while diagnosing #3019. That issue is a test flake; this is the application behaviour underneath it, which outlives the test fix in #3021.
The mechanism
Two components render a
MessageComposer, and which one owns it depends on whether the message list is empty:frontend/src/components/chat/Footer.tsx:20—if (!hasMessage(messages) && !showIfEmptyThread) return null;frontend/src/components/chat/WelcomeScreen.tsx:80,91—if (hasMessage(messages)) return null;… otherwise renders its own<MessageComposer {...props} />hasMessage()(frontend/src/lib/utils.ts:10) is true when any step is of typeuser_message,assistant_message, ortool.So every transition of
hasMessage()unmounts one composer instance and mounts a different one. BecauseMessageComposerholds its draft in local state, the remount discards whatever the user had typed, and the send button then reads as disabled because the new instance's value is empty (MessageComposer/index.tsx:339-341).Why this is user-facing, not just a test artifact
The transition fires on ordinary app events, not only in tests:
@cl.on_chat_startsends its message asynchronously. A user who starts typing before that message lands can have their input silently dropped.connection_successful(backend/chainlit/socket.py:208-240) emits the@cl.on_chat_resumemessage and thenresume_thread; the client handler (libs/react-client/src/useChatSession.ts:258+) rebuilds the list fromthread.stepsand replaces it wholesale. If the persisted history is empty, the list goes[] → [resume message] → [], remounting the composer twice in quick succession.The faster the backend, the narrower the window — which is exactly why this reads as intermittent.
Suggested direction
Render the composer once, above the
WelcomeScreen/Footerbranch, so its identity is stable across the transition — or lift the draft value into shared state so a remount preserves it. Either removes a class of "my message vanished" reports that would be near-impossible to reproduce from a bug report.Notes
cypress/e2e/thread_resume/spec.cy.tsby waiting for the transitions to settle before interacting. That makes the test honest, but it does not address the behaviour.on_chat_resumemessage is always erased by theresume_threadreplay that follows it, so@cl.on_chat_resumeoutput is only visible for a few frames unless it happens to be in the persisted steps. That may be worth its own issue if it is not intended.