Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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(() => {
Expand Down
68 changes: 68 additions & 0 deletions apps/sim/stores/chat/store.test.ts
Original file line number Diff line number Diff line change
@@ -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')
})
})
})
24 changes: 17 additions & 7 deletions apps/sim/stores/chat/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
})
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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),
Expand Down
Loading