From abe6f5004d2f837cc7b3ea02aa1363f6c6cdfd23 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Mon, 13 Jul 2026 15:33:58 -0700 Subject: [PATCH] fix(chat): render floating chat messages in insertion order instead of timestamp sort --- .../w/[workflowId]/components/chat/chat.tsx | 4 +- apps/sim/stores/chat/store.test.ts | 68 +++++++++++++++++++ apps/sim/stores/chat/store.ts | 24 +++++-- 3 files changed, 86 insertions(+), 10 deletions(-) create mode 100644 apps/sim/stores/chat/store.test.ts diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/chat.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/chat.tsx index b9a4e815578..f82386f7e72 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/chat.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/chat.tsx @@ -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(() => { diff --git a/apps/sim/stores/chat/store.test.ts b/apps/sim/stores/chat/store.test.ts new file mode 100644 index 00000000000..1529cff9582 --- /dev/null +++ b/apps/sim/stores/chat/store.test.ts @@ -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') + }) + }) +}) diff --git a/apps/sim/stores/chat/store.ts b/apps/sim/stores/chat/store.ts index 23086295b97..f2e2a9c1cfd 100644 --- a/apps/sim/stores/chat/store.ts +++ b/apps/sim/stores/chat/store.ts @@ -63,7 +63,7 @@ export const useChatStore = create()( 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()( 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()( }), { 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),