diff --git a/src/renderer/src/pages/home/Messages/Messages.tsx b/src/renderer/src/pages/home/Messages/Messages.tsx index 7b71097782..23d9424532 100644 --- a/src/renderer/src/pages/home/Messages/Messages.tsx +++ b/src/renderer/src/pages/home/Messages/Messages.tsx @@ -123,6 +123,33 @@ const Messages: React.FC = ({ assistant, topic, setActiveTopic, o return unifiedResult.groupedMessages.filter((group) => !group.some((item) => streamingSet.has(item.message.id))) }, [isDataApiPath, unifiedResult.groupedMessages, sessionIds]) + // NOTE: [v2 Migration] Listen for MESSAGE_CREATED event to refresh data when user sends a message. + // This ensures user messages appear immediately in the list. + useEffect(() => { + if (!isDataApiPath) return + + const handler = async ({ + topicId: eventTopicId + }: { + message: Message + blocks: MessageBlock[] + topicId: string + }) => { + if (eventTopicId !== topic.id) return + + // Refresh DataApi to show the new user message + if (unifiedResult.mutate) { + await unifiedResult.mutate() + } + } + + EventEmitter.on(EVENT_NAMES.MESSAGE_CREATED, handler) + return () => { + EventEmitter.off(EVENT_NAMES.MESSAGE_CREATED, handler) + } + // eslint-disable-next-line react-hooks/exhaustive-deps -- mutate is stable (useCallback wrapped) + }, [isDataApiPath, topic.id, unifiedResult.mutate]) + // NOTE: [v2 Migration] Listen for STREAMING_FINALIZED event to handle DataApi refresh and cache clearing. // TRADEOFF: Event-driven ensures mutate() completes before cache clears, preventing UI flicker. useEffect(() => { diff --git a/src/renderer/src/services/EventService.ts b/src/renderer/src/services/EventService.ts index e22aa08dac..ece8af408d 100644 --- a/src/renderer/src/services/EventService.ts +++ b/src/renderer/src/services/EventService.ts @@ -28,5 +28,8 @@ export const EVENT_NAMES = { CHANGE_TOPIC: 'CHANGE_TOPIC', // NOTE: [v2 Migration] Used for streaming->API transition. // Emitted by StreamingService.finalize() to signal UI hooks to refresh data and clear cache. - STREAMING_FINALIZED: 'streaming:finalized' + STREAMING_FINALIZED: 'streaming:finalized', + // NOTE: [v2 Migration] Emitted when a message is created and saved to DB. + // Used by Messages.tsx to optimistically update SWR cache for immediate UI display. + MESSAGE_CREATED: 'message:created' } diff --git a/src/renderer/src/services/messageStreaming/StreamingService.ts b/src/renderer/src/services/messageStreaming/StreamingService.ts index 4196c16225..91b0185de2 100644 --- a/src/renderer/src/services/messageStreaming/StreamingService.ts +++ b/src/renderer/src/services/messageStreaming/StreamingService.ts @@ -343,10 +343,20 @@ class StreamingService { // Merge changes - use type assertion since we're updating the same block type const updatedBlock = { ...existingBlock, ...changes } as MessageBlock - session.blocks[blockId] = updatedBlock - // Update caches - cacheService.set(getSessionKey(messageId), session, SESSION_TTL) + // IMPORTANT: Create new session object to trigger CacheService notification. + // CacheService uses Object.is() for value comparison - same reference = no notification. + // Without this, useCache subscribers won't re-render on updates. + const updatedSession: StreamingSession = { + ...session, + blocks: { + ...session.blocks, + [blockId]: updatedBlock + } + } + + // Update caches with new object references + cacheService.set(getSessionKey(messageId), updatedSession, SESSION_TTL) cacheService.set(getBlockKey(blockId), updatedBlock, SESSION_TTL) } diff --git a/src/renderer/src/store/thunk/messageThunk.ts b/src/renderer/src/store/thunk/messageThunk.ts index 58af404677..504c4d3fbe 100644 --- a/src/renderer/src/store/thunk/messageThunk.ts +++ b/src/renderer/src/store/thunk/messageThunk.ts @@ -22,6 +22,7 @@ import db from '@renderer/databases' import { fetchMessagesSummary, transformMessagesAndFetch } from '@renderer/services/ApiService' import { dbService } from '@renderer/services/db' import { DbService } from '@renderer/services/db/DbService' +import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService' import FileManager from '@renderer/services/FileManager' import { BlockManager } from '@renderer/services/messageStreaming/BlockManager' import { createCallbacks } from '@renderer/services/messageStreaming/callbacks' @@ -931,6 +932,7 @@ export const sendMessage = const stateBeforeSend = getState() let activeAgentSession = agentSession ?? findExistingAgentSessionContext(stateBeforeSend, topicId, assistant.id) + if (activeAgentSession) { const derivedSession = findExistingAgentSessionContext(stateBeforeSend, topicId, assistant.id) if (derivedSession?.agentSessionId && derivedSession.agentSessionId !== activeAgentSession.agentSessionId) { @@ -950,6 +952,13 @@ export const sendMessage = } else { // Normal topic: use Data API, get server-generated message ID finalUserMessage = await streamingService.createUserMessage(topicId, userMessage, userMessageBlocks) + + // NOTE: [v2 Migration] Emit event for Messages.tsx to optimistically update SWR cache + EventEmitter.emit(EVENT_NAMES.MESSAGE_CREATED, { + message: finalUserMessage, + blocks: userMessageBlocks, + topicId + }) } dispatch(newMessagesActions.addMessage({ topicId, message: finalUserMessage }))