From a9bd64c106f077f0666457f2a810d85827643392 Mon Sep 17 00:00:00 2001 From: MaheshtheDev <38828053+MaheshtheDev@users.noreply.github.com> Date: Mon, 29 Sep 2025 05:38:32 +0000 Subject: [PATCH] hot-fix: chat conversation reaching max depth with update (#442) --- apps/web/stores/chat.ts | 175 ++++++++++++++++++++++++---------------- 1 file changed, 107 insertions(+), 68 deletions(-) diff --git a/apps/web/stores/chat.ts b/apps/web/stores/chat.ts index 16814bdc..fe27275e 100644 --- a/apps/web/stores/chat.ts +++ b/apps/web/stores/chat.ts @@ -1,43 +1,73 @@ -import type { UIMessage } from "@ai-sdk/react"; -import { create } from "zustand"; -import { persist } from "zustand/middleware"; +import type { UIMessage } from "@ai-sdk/react" +import { create } from "zustand" +import { persist } from "zustand/middleware" + +/** + * Deep equality check for UIMessage arrays to prevent unnecessary state updates + */ +function areUIMessageArraysEqual(a: UIMessage[], b: UIMessage[]): boolean { + if (a === b) return true + if (a.length !== b.length) return false + + for (let i = 0; i < a.length; i++) { + const msgA = a[i] + const msgB = b[i] + + // Both messages should exist at this index + if (!msgA || !msgB) return false + + if (msgA === msgB) continue + + if (msgA.id !== msgB.id || msgA.role !== msgB.role) { + return false + } + + // Compare the entire message using JSON serialization as a fallback + // This handles all properties including parts, toolInvocations, etc. + if (JSON.stringify(msgA) !== JSON.stringify(msgB)) { + return false + } + } + + return true +} export interface ConversationSummary { - id: string; - title?: string; - lastUpdated: string; + id: string + title?: string + lastUpdated: string } interface ConversationRecord { - messages: UIMessage[]; - title?: string; - lastUpdated: string; + messages: UIMessage[] + title?: string + lastUpdated: string } interface ProjectConversationsState { - currentChatId: string | null; - conversations: Record; + currentChatId: string | null + conversations: Record } interface ConversationsStoreState { - byProject: Record; - setCurrentChatId: (projectId: string, chatId: string | null) => void; + byProject: Record + setCurrentChatId: (projectId: string, chatId: string | null) => void setConversation: ( projectId: string, chatId: string, messages: UIMessage[], - ) => void; - deleteConversation: (projectId: string, chatId: string) => void; + ) => void + deleteConversation: (projectId: string, chatId: string) => void setConversationTitle: ( projectId: string, chatId: string, title: string | undefined, - ) => void; + ) => void } export const usePersistentChatStore = create()( persist( - (set, get) => ({ + (set, _get) => ({ byProject: {}, setCurrentChatId(projectId, chatId) { @@ -45,29 +75,38 @@ export const usePersistentChatStore = create()( const project = state.byProject[projectId] ?? { currentChatId: null, conversations: {}, - }; + } return { byProject: { ...state.byProject, [projectId]: { ...project, currentChatId: chatId }, }, - }; - }); + } + }) }, setConversation(projectId, chatId, messages) { - const now = new Date().toISOString(); + const now = new Date().toISOString() set((state) => { const project = state.byProject[projectId] ?? { currentChatId: null, conversations: {}, - }; - const existing = project.conversations[chatId]; + } + const existing = project.conversations[chatId] + + // Check if messages are actually different to prevent unnecessary updates + if ( + existing && + areUIMessageArraysEqual(existing.messages, messages) + ) { + return state // No change needed + } + const shouldTouchLastUpdated = (() => { - if (!existing) return messages.length > 0; - const previousLength = existing.messages?.length ?? 0; - return messages.length > previousLength; - })(); + if (!existing) return messages.length > 0 + const previousLength = existing.messages?.length ?? 0 + return messages.length > previousLength + })() const record: ConversationRecord = { messages, @@ -75,7 +114,7 @@ export const usePersistentChatStore = create()( lastUpdated: shouldTouchLastUpdated ? now : (existing?.lastUpdated ?? now), - }; + } return { byProject: { ...state.byProject, @@ -87,8 +126,8 @@ export const usePersistentChatStore = create()( }, }, }, - }; - }); + } + }) }, deleteConversation(projectId, chatId) { @@ -96,28 +135,28 @@ export const usePersistentChatStore = create()( const project = state.byProject[projectId] ?? { currentChatId: null, conversations: {}, - }; - const { [chatId]: _, ...rest } = project.conversations; + } + const { [chatId]: _, ...rest } = project.conversations const nextCurrent = - project.currentChatId === chatId ? null : project.currentChatId; + project.currentChatId === chatId ? null : project.currentChatId return { byProject: { ...state.byProject, [projectId]: { currentChatId: nextCurrent, conversations: rest }, }, - }; - }); + } + }) }, setConversationTitle(projectId, chatId, title) { - const now = new Date().toISOString(); + const now = new Date().toISOString() set((state) => { const project = state.byProject[projectId] ?? { currentChatId: null, conversations: {}, - }; - const existing = project.conversations[chatId]; - if (!existing) return { byProject: state.byProject }; + } + const existing = project.conversations[chatId] + if (!existing) return { byProject: state.byProject } return { byProject: { ...state.byProject, @@ -129,76 +168,76 @@ export const usePersistentChatStore = create()( }, }, }, - }; - }); + } + }) }, }), { name: "supermemory-chats", }, ), -); +) // Always scoped to the current project via useProject -import { useProject } from "."; +import { useProject } from "." export function usePersistentChat() { - const { selectedProject } = useProject(); - const projectId = selectedProject; + const { selectedProject } = useProject() + const projectId = selectedProject - const projectState = usePersistentChatStore((s) => s.byProject[projectId]); - const setCurrentChatIdRaw = usePersistentChatStore((s) => s.setCurrentChatId); - const setConversationRaw = usePersistentChatStore((s) => s.setConversation); + const projectState = usePersistentChatStore((s) => s.byProject[projectId]) + const setCurrentChatIdRaw = usePersistentChatStore((s) => s.setCurrentChatId) + const setConversationRaw = usePersistentChatStore((s) => s.setConversation) const deleteConversationRaw = usePersistentChatStore( (s) => s.deleteConversation, - ); + ) const setConversationTitleRaw = usePersistentChatStore( (s) => s.setConversationTitle, - ); + ) const conversations: ConversationSummary[] = (() => { - const convs = projectState?.conversations ?? {}; + const convs = projectState?.conversations ?? {} return Object.entries(convs).map(([id, rec]) => ({ id, title: rec.title, lastUpdated: rec.lastUpdated, - })); - })(); + })) + })() - const currentChatId = projectState?.currentChatId ?? null; + const currentChatId = projectState?.currentChatId ?? null function setCurrentChatId(chatId: string | null): void { - setCurrentChatIdRaw(projectId, chatId); + setCurrentChatIdRaw(projectId, chatId) } function setConversation(chatId: string, messages: UIMessage[]): void { - setConversationRaw(projectId, chatId, messages); + setConversationRaw(projectId, chatId, messages) } function deleteConversation(chatId: string): void { - deleteConversationRaw(projectId, chatId); + deleteConversationRaw(projectId, chatId) } function setConversationTitle( chatId: string, title: string | undefined, ): void { - setConversationTitleRaw(projectId, chatId, title); + setConversationTitleRaw(projectId, chatId, title) } function getCurrentConversation(): UIMessage[] | undefined { - const convs = projectState?.conversations ?? {}; - const id = currentChatId; - if (!id) return undefined; - return convs[id]?.messages; + const convs = projectState?.conversations ?? {} + const id = currentChatId + if (!id) return undefined + return convs[id]?.messages } function getCurrentChat(): ConversationSummary | undefined { - const id = currentChatId; - if (!id) return undefined; - const rec = projectState?.conversations?.[id]; - if (!rec) return undefined; - return { id, title: rec.title, lastUpdated: rec.lastUpdated }; + const id = currentChatId + if (!id) return undefined + const rec = projectState?.conversations?.[id] + if (!rec) return undefined + return { id, title: rec.title, lastUpdated: rec.lastUpdated } } return { @@ -210,5 +249,5 @@ export function usePersistentChat() { setConversationTitle, getCurrentConversation, getCurrentChat, - }; + } }