fix(chat): render floating chat messages in insertion order instead of timestamp sort (#5647)

This commit is contained in:
Waleed
2026-07-13 15:45:06 -07:00
committed by GitHub
parent de934b9979
commit 8e5dc40e72
3 changed files with 86 additions and 10 deletions
@@ -401,9 +401,7 @@ export function Chat() {
const workflowMessages = useMemo(() => {
if (!activeWorkflowId) return []
return messages
.filter((msg) => msg.workflowId === activeWorkflowId)
.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime())
return messages.filter((msg) => msg.workflowId === activeWorkflowId)
}, [messages, activeWorkflowId])
const isStreaming = useMemo(() => {
+68
View File
@@ -0,0 +1,68 @@
/**
* @vitest-environment jsdom
*/
import { beforeEach, describe, expect, it, vi } from 'vitest'
vi.hoisted(() => {
const legacyNewestFirst = {
state: {
messages: [
{
id: 'msg-2',
content: 'response',
workflowId: 'wf-1',
type: 'workflow',
timestamp: '2026-07-13T00:00:00.000Z',
},
{
id: 'msg-1',
content: 'hi',
workflowId: 'wf-1',
type: 'user',
timestamp: '2026-07-13T00:00:00.000Z',
},
],
},
version: 0,
}
window.localStorage.setItem('chat-store', JSON.stringify(legacyNewestFirst))
})
import { useChatStore } from '@/stores/chat/store'
describe('chat store message ordering', () => {
it('migrates v0 persisted messages from newest-first to insertion order', () => {
const messages = useChatStore.getState().messages
expect(messages.map((m) => m.id)).toEqual(['msg-1', 'msg-2'])
})
describe('addMessage', () => {
beforeEach(() => {
useChatStore.setState({ messages: [] })
})
it('appends messages so insertion order is conversation order, even with identical timestamps', () => {
const { addMessage } = useChatStore.getState()
const timestamp = new Date().toISOString()
addMessage({ content: 'hi', workflowId: 'wf-1', type: 'user', timestamp } as any)
addMessage({ content: '', workflowId: 'wf-1', type: 'workflow', timestamp } as any)
const types = useChatStore.getState().messages.map((m) => m.type)
expect(types).toEqual(['user', 'workflow'])
})
it('keeps only the most recent messages when trimming to the cap', () => {
const { addMessage } = useChatStore.getState()
for (let i = 0; i < 55; i++) {
addMessage({ content: `m${i}`, workflowId: 'wf-1', type: 'user' })
}
const messages = useChatStore.getState().messages
expect(messages).toHaveLength(50)
expect(messages[0].content).toBe('m5')
expect(messages[messages.length - 1].content).toBe('m54')
})
})
})
+17 -7
View File
@@ -63,7 +63,7 @@ export const useChatStore = create<ChatState>()(
timestamp: (message as any).timestamp ?? new Date().toISOString(),
}
const newMessages = [newMessage, ...state.messages].slice(0, MAX_MESSAGES)
const newMessages = [...state.messages, newMessage].slice(-MAX_MESSAGES)
return { messages: newMessages }
})
@@ -126,14 +126,9 @@ export const useChatStore = create<ChatState>()(
const headers = ['timestamp', 'type', 'content']
const sortedMessages = [...messages].sort(
(a: ChatMessage, b: ChatMessage) =>
new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()
)
const csvRows = [
headers.join(','),
...sortedMessages.map((message: ChatMessage) =>
...messages.map((message: ChatMessage) =>
[
formatCSVValue(message.timestamp),
formatCSVValue(message.type),
@@ -253,6 +248,21 @@ export const useChatStore = create<ChatState>()(
}),
{
name: 'chat-store',
version: 1,
/**
* v0 stored messages newest-first; v1 stores them in insertion
* (chronological) order, which consumers render without sorting.
*/
migrate: (persistedState, version) => {
if ((version ?? 0) < 1) {
const state = persistedState as { messages?: ChatMessage[] } | null
return {
...state,
messages: [...(state?.messages ?? [])].reverse(),
}
}
return persistedState
},
/**
* Persist only the durable chat state — message history (with transient
* blob `previewUrl`s stripped since they are not valid across reloads),