diff --git a/packages/views/chat/components/chat-message-list.tsx b/packages/views/chat/components/chat-message-list.tsx index 5652b1972..4667c0142 100644 --- a/packages/views/chat/components/chat-message-list.tsx +++ b/packages/views/chat/components/chat-message-list.tsx @@ -35,23 +35,26 @@ export function ChatMessageList({ const fadeStyle = useScrollFade(scrollRef); useAutoScroll(scrollRef); - // Once the assistant message for this pending task has landed in the - // messages list, AssistantMessage owns its rendering — suppress the live - // timeline to avoid rendering the same content in two places during the - // invalidate → refetch window. + // While a task is in flight and its assistant message hasn't landed yet, + // inject a synthetic placeholder carrying the same task_id. AssistantMessage + // reads the live timeline from the shared taskMessagesOptions cache (fed by + // WS), so the synthetic bubble renders exactly what the persisted bubble + // will. Because MessageBubble is keyed by task_id when present, the DOM + // element is preserved when the real message arrives — no unmount/remount, + // no double scroll-to-bottom, no visible jump. const pendingAlreadyPersisted = !!pendingTaskId && messages.some( (m) => m.role === "assistant" && m.task_id === pendingTaskId, ); - - // Live timeline for the in-flight task. useRealtimeSync keeps this cache - // current via setQueryData on task:message events. - const showLiveTimeline = !!pendingTaskId && !pendingAlreadyPersisted; - const { data: liveTaskMessages } = useQuery({ - ...taskMessagesOptions(pendingTaskId ?? ""), - enabled: showLiveTimeline, - }); - const liveTimeline: ChatTimelineItem[] = (liveTaskMessages ?? []).map(toTimelineItem); - const hasLive = showLiveTimeline && liveTimeline.length > 0; + const displayMessages: ChatMessage[] = pendingTaskId && !pendingAlreadyPersisted + ? [...messages, { + id: `pending-${pendingTaskId}`, + chat_session_id: messages[messages.length - 1]?.chat_session_id ?? "", + role: "assistant", + content: "", + task_id: pendingTaskId, + created_at: new Date().toISOString(), + }] + : messages; return (