feat(Messages): implement MESSAGE_CREATED event for immediate message updates

- Added a new MESSAGE_CREATED event to EventService to facilitate optimistic UI updates when a user sends a message.
- Enhanced the Messages component to listen for MESSAGE_CREATED events, ensuring new messages appear instantly in the UI.
- Updated StreamingService to create new session objects for cache notifications, improving reactivity in the message display.
- Emitted MESSAGE_CREATED event in the sendMessage thunk to trigger cache updates for user messages.

This commit improves user experience by ensuring timely updates in the message list during interactions.
This commit is contained in:
fullex 2026-01-05 23:52:57 +08:00
parent 16596fd9bb
commit 47f20c5663
4 changed files with 53 additions and 4 deletions

View File

@ -123,6 +123,33 @@ const Messages: React.FC<MessagesProps> = ({ 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(() => {

View File

@ -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'
}

View File

@ -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)
}

View File

@ -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 }))