Skip to content

Commit 8e5dc40

Browse files
authored
fix(chat): render floating chat messages in insertion order instead of timestamp sort (#5647)
1 parent de934b9 commit 8e5dc40

3 files changed

Lines changed: 86 additions & 10 deletions

File tree

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/chat.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -401,9 +401,7 @@ export function Chat() {
401401

402402
const workflowMessages = useMemo(() => {
403403
if (!activeWorkflowId) return []
404-
return messages
405-
.filter((msg) => msg.workflowId === activeWorkflowId)
406-
.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime())
404+
return messages.filter((msg) => msg.workflowId === activeWorkflowId)
407405
}, [messages, activeWorkflowId])
408406

409407
const isStreaming = useMemo(() => {

apps/sim/stores/chat/store.test.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/**
2+
* @vitest-environment jsdom
3+
*/
4+
import { beforeEach, describe, expect, it, vi } from 'vitest'
5+
6+
vi.hoisted(() => {
7+
const legacyNewestFirst = {
8+
state: {
9+
messages: [
10+
{
11+
id: 'msg-2',
12+
content: 'response',
13+
workflowId: 'wf-1',
14+
type: 'workflow',
15+
timestamp: '2026-07-13T00:00:00.000Z',
16+
},
17+
{
18+
id: 'msg-1',
19+
content: 'hi',
20+
workflowId: 'wf-1',
21+
type: 'user',
22+
timestamp: '2026-07-13T00:00:00.000Z',
23+
},
24+
],
25+
},
26+
version: 0,
27+
}
28+
window.localStorage.setItem('chat-store', JSON.stringify(legacyNewestFirst))
29+
})
30+
31+
import { useChatStore } from '@/stores/chat/store'
32+
33+
describe('chat store message ordering', () => {
34+
it('migrates v0 persisted messages from newest-first to insertion order', () => {
35+
const messages = useChatStore.getState().messages
36+
expect(messages.map((m) => m.id)).toEqual(['msg-1', 'msg-2'])
37+
})
38+
39+
describe('addMessage', () => {
40+
beforeEach(() => {
41+
useChatStore.setState({ messages: [] })
42+
})
43+
44+
it('appends messages so insertion order is conversation order, even with identical timestamps', () => {
45+
const { addMessage } = useChatStore.getState()
46+
const timestamp = new Date().toISOString()
47+
48+
addMessage({ content: 'hi', workflowId: 'wf-1', type: 'user', timestamp } as any)
49+
addMessage({ content: '', workflowId: 'wf-1', type: 'workflow', timestamp } as any)
50+
51+
const types = useChatStore.getState().messages.map((m) => m.type)
52+
expect(types).toEqual(['user', 'workflow'])
53+
})
54+
55+
it('keeps only the most recent messages when trimming to the cap', () => {
56+
const { addMessage } = useChatStore.getState()
57+
58+
for (let i = 0; i < 55; i++) {
59+
addMessage({ content: `m${i}`, workflowId: 'wf-1', type: 'user' })
60+
}
61+
62+
const messages = useChatStore.getState().messages
63+
expect(messages).toHaveLength(50)
64+
expect(messages[0].content).toBe('m5')
65+
expect(messages[messages.length - 1].content).toBe('m54')
66+
})
67+
})
68+
})

apps/sim/stores/chat/store.ts

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ export const useChatStore = create<ChatState>()(
6363
timestamp: (message as any).timestamp ?? new Date().toISOString(),
6464
}
6565

66-
const newMessages = [newMessage, ...state.messages].slice(0, MAX_MESSAGES)
66+
const newMessages = [...state.messages, newMessage].slice(-MAX_MESSAGES)
6767

6868
return { messages: newMessages }
6969
})
@@ -126,14 +126,9 @@ export const useChatStore = create<ChatState>()(
126126

127127
const headers = ['timestamp', 'type', 'content']
128128

129-
const sortedMessages = [...messages].sort(
130-
(a: ChatMessage, b: ChatMessage) =>
131-
new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()
132-
)
133-
134129
const csvRows = [
135130
headers.join(','),
136-
...sortedMessages.map((message: ChatMessage) =>
131+
...messages.map((message: ChatMessage) =>
137132
[
138133
formatCSVValue(message.timestamp),
139134
formatCSVValue(message.type),
@@ -253,6 +248,21 @@ export const useChatStore = create<ChatState>()(
253248
}),
254249
{
255250
name: 'chat-store',
251+
version: 1,
252+
/**
253+
* v0 stored messages newest-first; v1 stores them in insertion
254+
* (chronological) order, which consumers render without sorting.
255+
*/
256+
migrate: (persistedState, version) => {
257+
if ((version ?? 0) < 1) {
258+
const state = persistedState as { messages?: ChatMessage[] } | null
259+
return {
260+
...state,
261+
messages: [...(state?.messages ?? [])].reverse(),
262+
}
263+
}
264+
return persistedState
265+
},
256266
/**
257267
* Persist only the durable chat state — message history (with transient
258268
* blob `previewUrl`s stripped since they are not valid across reloads),

0 commit comments

Comments
 (0)